Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,18 @@ username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap

replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.

Then you can run the tests in all modules or a specific one:

````bash
$ go test ./...
$ go test ./modules/caddyhttp/tracing/
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I thought for newbies like me, if you want to attract them 😅 this is a helpful first pointer after the build.

```

### With version information and/or plugins

Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...

```
```bash
$ xcaddy build
```

Expand Down
37 changes: 31 additions & 6 deletions modules/caddyhttp/tracing/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type Tracing struct {
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span
SpanName string `json:"span"`

// SpanAttributes are custom key-value pairs to be added to spans
SpanAttributes map[string]string `json:"span_attributes,omitempty"`

// otel implements opentelemetry related logic.
otel openTelemetryWrapper

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

var err error
ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName)
ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName, ot.SpanAttributes)

return err
}
Expand All @@ -69,6 +72,10 @@ func (ot *Tracing) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
//
// tracing {
// [span <span_name>]
// [span_attributes {
// attr1 value1
// attr2 value2
// }]
// }
func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
setParameter := func(d *caddyfile.Dispenser, val *string) error {
Expand All @@ -94,12 +101,30 @@ func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}

for d.NextBlock(0) {
if dst, ok := paramsMap[d.Val()]; ok {
if err := setParameter(d, dst); err != nil {
return err
switch d.Val() {
case "span_attributes":
if ot.SpanAttributes == nil {
ot.SpanAttributes = make(map[string]string)
}
for d.NextBlock(1) {
key := d.Val()
if !d.NextArg() {
return d.ArgErr()
}
value := d.Val()
if d.NextArg() {
return d.ArgErr()
}
ot.SpanAttributes[key] = value
}
default:
if dst, ok := paramsMap[d.Val()]; ok {
if err := setParameter(d, dst); err != nil {
return err
}
} else {
return d.ArgErr()
}
} else {
return d.ArgErr()
}
}
return nil
Expand Down
215 changes: 211 additions & 4 deletions modules/caddyhttp/tracing/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,43 @@ package tracing

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"

"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func TestTracing_UnmarshalCaddyfile(t *testing.T) {
tests := []struct {
name string
spanName string
d *caddyfile.Dispenser
wantErr bool
name string
spanName string
spanAttributes map[string]string
d *caddyfile.Dispenser
wantErr bool
}{
{
name: "Full config",
spanName: "my-span",
spanAttributes: map[string]string{
"attr1": "value1",
"attr2": "value2",
},
d: caddyfile.NewTestDispenser(`
tracing {
span my-span
span_attributes {
attr1 value1
attr2 value2
}
}`),
wantErr: false,
},
Expand All @@ -42,6 +55,21 @@ tracing {
name: "Empty config",
d: caddyfile.NewTestDispenser(`
tracing {
}`),
wantErr: false,
},
{
name: "Only span attributes",
spanAttributes: map[string]string{
"service.name": "my-service",
"service.version": "1.0.0",
},
d: caddyfile.NewTestDispenser(`
tracing {
span_attributes {
service.name my-service
service.version 1.0.0
}
}`),
wantErr: false,
},
Expand All @@ -56,6 +84,20 @@ tracing {
if ot.SpanName != tt.spanName {
t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName)
}

if len(tt.spanAttributes) > 0 {
if ot.SpanAttributes == nil {
t.Errorf("UnmarshalCaddyfile() SpanAttributes is nil, expected %v", tt.spanAttributes)
} else {
for key, expectedValue := range tt.spanAttributes {
if actualValue, exists := ot.SpanAttributes[key]; !exists {
t.Errorf("UnmarshalCaddyfile() SpanAttributes missing key %v", key)
} else if actualValue != expectedValue {
t.Errorf("UnmarshalCaddyfile() SpanAttributes[%v] = %v, want %v", key, actualValue, expectedValue)
}
}
}
}
})
}
}
Expand All @@ -79,6 +121,26 @@ func TestTracing_UnmarshalCaddyfile_Error(t *testing.T) {
d: caddyfile.NewTestDispenser(`
tracing {
span
}`),
wantErr: true,
},
{
name: "Span attributes missing value",
d: caddyfile.NewTestDispenser(`
tracing {
span_attributes {
key
}
}`),
wantErr: true,
},
{
name: "Span attributes too many arguments",
d: caddyfile.NewTestDispenser(`
tracing {
span_attributes {
key value extra
}
}`),
wantErr: true,
},
Expand Down Expand Up @@ -181,6 +243,151 @@ func TestTracing_ServeHTTP_Next_Error(t *testing.T) {
}
}

func TestTracing_JSON_Configuration(t *testing.T) {
// Test that our struct correctly marshals to and from JSON
original := &Tracing{
SpanName: "test-span",
SpanAttributes: map[string]string{
"service.name": "test-service",
"service.version": "1.0.0",
"env": "test",
},
}

jsonData, err := json.Marshal(original)
if err != nil {
t.Fatalf("Failed to marshal to JSON: %v", err)
}

var unmarshaled Tracing
if err := json.Unmarshal(jsonData, &unmarshaled); err != nil {
t.Fatalf("Failed to unmarshal from JSON: %v", err)
}

if unmarshaled.SpanName != original.SpanName {
t.Errorf("Expected SpanName %s, got %s", original.SpanName, unmarshaled.SpanName)
}

if len(unmarshaled.SpanAttributes) != len(original.SpanAttributes) {
t.Errorf("Expected %d span attributes, got %d", len(original.SpanAttributes), len(unmarshaled.SpanAttributes))
}

for key, expectedValue := range original.SpanAttributes {
if actualValue, exists := unmarshaled.SpanAttributes[key]; !exists {
t.Errorf("Expected span attribute %s to exist", key)
} else if actualValue != expectedValue {
t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
}
}

t.Logf("JSON representation: %s", string(jsonData))
}

func TestTracing_OpenTelemetry_Span_Attributes(t *testing.T) {
// Create an in-memory span recorder to capture actual span data
spanRecorder := tracetest.NewSpanRecorder()
provider := trace.NewTracerProvider(
trace.WithSpanProcessor(spanRecorder),
)

// Create our tracing module with span attributes that include placeholders
ot := &Tracing{
SpanName: "test-span",
SpanAttributes: map[string]string{
"placeholder": "{http.request.method}",
"static": "test-service",
"mixed": "prefix-{http.request.method}-suffix",
},
}

// Create a specific request to test against
req, _ := http.NewRequest("POST", "https://api.example.com/v1/users?id=123", nil)
req.Host = "api.example.com"

// Set up the request context with proper replacer and vars
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, make(map[string]any))
req = req.WithContext(ctx)
repl.Set("http.request.method", req.Method)

w := httptest.NewRecorder()

// Handler that ensures the request gets processed
var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error {
writer.WriteHeader(200)
return nil
}

// Set up Caddy context
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()

// Override the global tracer provider with our test provider
// This is a bit hacky but necessary to capture the actual spans
originalProvider := globalTracerProvider
globalTracerProvider = &tracerProvider{
tracerProvider: provider,
tracerProvidersCounter: 1, // Simulate one user
}
defer func() {
globalTracerProvider = originalProvider
}()

// Provision the tracing module
if err := ot.Provision(caddyCtx); err != nil {
t.Errorf("Provision error: %v", err)
t.FailNow()
}

// Execute the request
if err := ot.ServeHTTP(w, req, handler); err != nil {
t.Errorf("ServeHTTP error: %v", err)
}

// Get the recorded spans
spans := spanRecorder.Ended()
if len(spans) == 0 {
t.Fatal("Expected at least one span to be recorded")
}

// Find our span (should be the one with our test span name)
var testSpan trace.ReadOnlySpan
for _, span := range spans {
if span.Name() == "test-span" {
testSpan = span
break
}
}

if testSpan == nil {
t.Fatal("Could not find test span in recorded spans")
}

// Verify that the span attributes were set correctly with placeholder replacement
expectedAttributes := map[string]string{
"placeholder": "POST",
"static": "test-service",
"mixed": "prefix-POST-suffix",
"http.response.status_code": "200", // OTEL default
}

actualAttributes := make(map[string]string)
for _, attr := range testSpan.Attributes() {
actualAttributes[string(attr.Key)] = attr.Value.AsString()
}

for key, expectedValue := range expectedAttributes {
if actualValue, exists := actualAttributes[key]; !exists {
t.Errorf("Expected span attribute %s to be set", key)
} else if actualValue != expectedValue {
t.Errorf("Expected span attribute %s = %s, got %s", key, expectedValue, actualValue)
}
}

t.Logf("Recorded span attributes: %+v", actualAttributes)
}

func createRequestWithContext(method string, url string) *http.Request {
r, _ := http.NewRequest(method, url, nil)
repl := caddy.NewReplacer()
Expand Down
Loading
Loading