Skip to content

Commit c309a32

Browse files
author
Felix Hildén
committed
WIP add span attributes to tracing module
1 parent 39ace45 commit c309a32

File tree

4 files changed

+209
-12
lines changed

4 files changed

+209
-12
lines changed

modules/caddyhttp/tracing/module.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ type Tracing struct {
2727
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span
2828
SpanName string `json:"span"`
2929

30+
// SpanAttributes are custom key-value pairs to be added to spans
31+
SpanAttributes map[string]string `json:"span_attributes,omitempty"`
32+
3033
// otel implements opentelemetry related logic.
3134
otel openTelemetryWrapper
3235

@@ -46,7 +49,7 @@ func (ot *Tracing) Provision(ctx caddy.Context) error {
4649
ot.logger = ctx.Logger()
4750

4851
var err error
49-
ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName)
52+
ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName, ot.SpanAttributes)
5053

5154
return err
5255
}
@@ -69,6 +72,10 @@ func (ot *Tracing) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
6972
//
7073
// tracing {
7174
// [span <span_name>]
75+
// [span_attributes {
76+
// attr1 value1
77+
// attr2 value2
78+
// }]
7279
// }
7380
func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
7481
setParameter := func(d *caddyfile.Dispenser, val *string) error {
@@ -94,12 +101,30 @@ func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
94101
}
95102

96103
for d.NextBlock(0) {
97-
if dst, ok := paramsMap[d.Val()]; ok {
98-
if err := setParameter(d, dst); err != nil {
99-
return err
104+
switch d.Val() {
105+
case "span_attributes":
106+
if ot.SpanAttributes == nil {
107+
ot.SpanAttributes = make(map[string]string)
108+
}
109+
for d.NextBlock(1) {
110+
key := d.Val()
111+
if !d.NextArg() {
112+
return d.ArgErr()
113+
}
114+
value := d.Val()
115+
if d.NextArg() {
116+
return d.ArgErr()
117+
}
118+
ot.SpanAttributes[key] = value
119+
}
120+
default:
121+
if dst, ok := paramsMap[d.Val()]; ok {
122+
if err := setParameter(d, dst); err != nil {
123+
return err
124+
}
125+
} else {
126+
return d.ArgErr()
100127
}
101-
} else {
102-
return d.ArgErr()
103128
}
104129
}
105130
return nil

modules/caddyhttp/tracing/module_test.go

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tracing
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"net/http"
78
"net/http/httptest"
@@ -15,17 +16,26 @@ import (
1516

1617
func TestTracing_UnmarshalCaddyfile(t *testing.T) {
1718
tests := []struct {
18-
name string
19-
spanName string
20-
d *caddyfile.Dispenser
21-
wantErr bool
19+
name string
20+
spanName string
21+
spanAttributes map[string]string
22+
d *caddyfile.Dispenser
23+
wantErr bool
2224
}{
2325
{
2426
name: "Full config",
2527
spanName: "my-span",
28+
spanAttributes: map[string]string{
29+
"attr1": "value1",
30+
"attr2": "value2",
31+
},
2632
d: caddyfile.NewTestDispenser(`
2733
tracing {
2834
span my-span
35+
span_attributes {
36+
attr1 value1
37+
attr2 value2
38+
}
2939
}`),
3040
wantErr: false,
3141
},
@@ -42,6 +52,21 @@ tracing {
4252
name: "Empty config",
4353
d: caddyfile.NewTestDispenser(`
4454
tracing {
55+
}`),
56+
wantErr: false,
57+
},
58+
{
59+
name: "Only span attributes",
60+
spanAttributes: map[string]string{
61+
"service.name": "my-service",
62+
"service.version": "1.0.0",
63+
},
64+
d: caddyfile.NewTestDispenser(`
65+
tracing {
66+
span_attributes {
67+
service.name my-service
68+
service.version 1.0.0
69+
}
4570
}`),
4671
wantErr: false,
4772
},
@@ -56,6 +81,20 @@ tracing {
5681
if ot.SpanName != tt.spanName {
5782
t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName)
5883
}
84+
85+
if len(tt.spanAttributes) > 0 {
86+
if ot.SpanAttributes == nil {
87+
t.Errorf("UnmarshalCaddyfile() SpanAttributes is nil, expected %v", tt.spanAttributes)
88+
} else {
89+
for key, expectedValue := range tt.spanAttributes {
90+
if actualValue, exists := ot.SpanAttributes[key]; !exists {
91+
t.Errorf("UnmarshalCaddyfile() SpanAttributes missing key %v", key)
92+
} else if actualValue != expectedValue {
93+
t.Errorf("UnmarshalCaddyfile() SpanAttributes[%v] = %v, want %v", key, actualValue, expectedValue)
94+
}
95+
}
96+
}
97+
}
5998
})
6099
}
61100
}
@@ -79,6 +118,26 @@ func TestTracing_UnmarshalCaddyfile_Error(t *testing.T) {
79118
d: caddyfile.NewTestDispenser(`
80119
tracing {
81120
span
121+
}`),
122+
wantErr: true,
123+
},
124+
{
125+
name: "Span attributes missing value",
126+
d: caddyfile.NewTestDispenser(`
127+
tracing {
128+
span_attributes {
129+
key
130+
}
131+
}`),
132+
wantErr: true,
133+
},
134+
{
135+
name: "Span attributes too many arguments",
136+
d: caddyfile.NewTestDispenser(`
137+
tracing {
138+
span_attributes {
139+
key value extra
140+
}
82141
}`),
83142
wantErr: true,
84143
},
@@ -181,6 +240,100 @@ func TestTracing_ServeHTTP_Next_Error(t *testing.T) {
181240
}
182241
}
183242

243+
func TestTracing_ServeHTTP_With_SpanAttributes(t *testing.T) {
244+
ot := &Tracing{
245+
SpanName: "mySpan",
246+
SpanAttributes: map[string]string{
247+
"service.name": "test-service",
248+
"service.version": "1.0.0",
249+
"custom.attr": "test-value",
250+
},
251+
}
252+
253+
req := createRequestWithContext("GET", "https://example.com/foo")
254+
w := httptest.NewRecorder()
255+
256+
var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error {
257+
traceparent := request.Header.Get("Traceparent")
258+
if traceparent == "" || strings.HasPrefix(traceparent, "00-00000000000000000000000000000000-0000000000000000") {
259+
t.Errorf("Invalid traceparent: %v", traceparent)
260+
}
261+
262+
return nil
263+
}
264+
265+
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
266+
defer cancel()
267+
268+
if err := ot.Provision(ctx); err != nil {
269+
t.Errorf("Provision error: %v", err)
270+
t.FailNow()
271+
}
272+
273+
if err := ot.ServeHTTP(w, req, handler); err != nil {
274+
t.Errorf("ServeHTTP error: %v", err)
275+
}
276+
277+
// Verify span attributes were configured
278+
if ot.otel.spanAttributes == nil {
279+
t.Error("Expected span attributes to be set on otel wrapper")
280+
}
281+
282+
expectedAttrs := map[string]string{
283+
"service.name": "test-service",
284+
"service.version": "1.0.0",
285+
"custom.attr": "test-value",
286+
}
287+
288+
for key, expectedValue := range expectedAttrs {
289+
if actualValue, exists := ot.otel.spanAttributes[key]; !exists {
290+
t.Errorf("Expected span attribute %s to exist", key)
291+
} else if actualValue != expectedValue {
292+
t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
293+
}
294+
}
295+
}
296+
297+
func TestTracing_JSON_Configuration(t *testing.T) {
298+
// Test that our struct correctly marshals to and from JSON
299+
original := &Tracing{
300+
SpanName: "test-span",
301+
SpanAttributes: map[string]string{
302+
"service.name": "test-service",
303+
"service.version": "1.0.0",
304+
"env": "test",
305+
},
306+
}
307+
308+
jsonData, err := json.Marshal(original)
309+
if err != nil {
310+
t.Fatalf("Failed to marshal to JSON: %v", err)
311+
}
312+
313+
var unmarshaled Tracing
314+
if err := json.Unmarshal(jsonData, &unmarshaled); err != nil {
315+
t.Fatalf("Failed to unmarshal from JSON: %v", err)
316+
}
317+
318+
if unmarshaled.SpanName != original.SpanName {
319+
t.Errorf("Expected SpanName %s, got %s", original.SpanName, unmarshaled.SpanName)
320+
}
321+
322+
if len(unmarshaled.SpanAttributes) != len(original.SpanAttributes) {
323+
t.Errorf("Expected %d span attributes, got %d", len(original.SpanAttributes), len(unmarshaled.SpanAttributes))
324+
}
325+
326+
for key, expectedValue := range original.SpanAttributes {
327+
if actualValue, exists := unmarshaled.SpanAttributes[key]; !exists {
328+
t.Errorf("Expected span attribute %s to exist", key)
329+
} else if actualValue != expectedValue {
330+
t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
331+
}
332+
}
333+
334+
t.Logf("JSON representation: %s", string(jsonData))
335+
}
336+
184337
func createRequestWithContext(method string, url string) *http.Request {
185338
r, _ := http.NewRequest(method, url, nil)
186339
repl := caddy.NewReplacer()

modules/caddyhttp/tracing/tracer.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
99
"go.opentelemetry.io/contrib/propagators/autoprop"
10+
"go.opentelemetry.io/otel/attribute"
1011
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
1112
"go.opentelemetry.io/otel/propagation"
1213
"go.opentelemetry.io/otel/sdk/resource"
@@ -37,20 +38,23 @@ type openTelemetryWrapper struct {
3738

3839
handler http.Handler
3940

40-
spanName string
41+
spanName string
42+
spanAttributes map[string]string
4143
}
4244

4345
// newOpenTelemetryWrapper is responsible for the openTelemetryWrapper initialization using provided configuration.
4446
func newOpenTelemetryWrapper(
4547
ctx context.Context,
4648
spanName string,
49+
spanAttributes map[string]string,
4750
) (openTelemetryWrapper, error) {
4851
if spanName == "" {
4952
spanName = defaultSpanName
5053
}
5154

5255
ot := openTelemetryWrapper{
53-
spanName: spanName,
56+
spanName: spanName,
57+
spanAttributes: spanAttributes,
5458
}
5559

5660
version, _ := caddy.Version()
@@ -99,6 +103,20 @@ func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request
99103
extra.Add(zap.String("spanID", spanID))
100104
}
101105
}
106+
107+
// Add custom span attributes to the current span
108+
span := trace.SpanFromContext(ctx)
109+
if span.IsRecording() && len(ot.spanAttributes) > 0 {
110+
replacer := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
111+
attributes := make([]attribute.KeyValue, 0, len(ot.spanAttributes))
112+
for key, value := range ot.spanAttributes {
113+
// Allow placeholder replacement in attribute values
114+
replacedValue := replacer.ReplaceAll(value, "")
115+
attributes = append(attributes, attribute.String(key, replacedValue))
116+
}
117+
span.SetAttributes(attributes...)
118+
}
119+
102120
next := ctx.Value(nextCallCtxKey).(*nextCall)
103121
next.err = next.next.ServeHTTP(w, r)
104122
}

modules/caddyhttp/tracing/tracer_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func TestOpenTelemetryWrapper_newOpenTelemetryWrapper(t *testing.T) {
1616

1717
if otw, err = newOpenTelemetryWrapper(ctx,
1818
"",
19+
nil,
1920
); err != nil {
2021
t.Errorf("newOpenTelemetryWrapper() error = %v", err)
2122
t.FailNow()

0 commit comments

Comments
 (0)