Skip to content

Commit 2d12586

Browse files
committed
feat: add WithLinks and WithOTELOptions to Tracer
1 parent 5acd9c2 commit 2d12586

File tree

4 files changed

+66
-4
lines changed

4 files changed

+66
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ go func(parentCtx context.Context) {
427427
link := trace.LinkFromContext(parentCtx)
428428

429429
// 3. Start a new Root Span with the link
430-
ctx, span := tracer.Start(newCtx, "AsyncJob", trace.WithLinks(link))
430+
ctx, span := tracer.Start(newCtx, "AsyncJob", ion.WithLinks(link))
431431
defer span.End()
432432

433433
// Now you have a safe, independent span correlated to the original request

docs/USER_GUIDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func (s *Service) AsyncEmail(reqCtx context.Context) {
133133
go func() {
134134
// 2. Start a FRESH root trace
135135
// This ensures the email trace is complete even if request finishes.
136-
ctx, span := s.tracer.Start(context.Background(), "SendEmail", trace.WithLinks(link))
136+
ctx, span := s.tracer.Start(context.Background(), "SendEmail", ion.WithLinks(link))
137137
defer span.End()
138138
139139
// 3. Work...
@@ -254,8 +254,8 @@ func (s *Syncer) Sync(ctx context.Context) {
254254
tracer := s.tracer
255255
// Create a fresh context for the batch
256256
spanCtx, span := tracer.Start(context.Background(), "ApplyBatch",
257-
trace.WithLinks(trace.LinkFromContext(ctx)), // Link to main sync job
258-
trace.WithAttributes(attribute.Int("batch_id", batch.ID)),
257+
ion.WithLinks(trace.LinkFromContext(ctx)), // Link to main sync job
258+
ion.WithAttributes(attribute.Int("batch_id", batch.ID)),
259259
)
260260

261261
if err := s.apply(spanCtx, batch); err != nil {

tracer.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type SpanOption interface {
3737
type spanOptions struct {
3838
kind trace.SpanKind
3939
attributes []attribute.KeyValue
40+
links []trace.Link
41+
otelOpts []trace.SpanStartOption
4042
}
4143

4244
type kindOption trace.SpanKind
@@ -53,6 +55,21 @@ func (a attrOption) apply(o *spanOptions) { o.attributes = append(o.attributes,
5355
// WithAttributes adds attributes to the span.
5456
func WithAttributes(attrs ...attribute.KeyValue) SpanOption { return attrOption(attrs) }
5557

58+
type linkOption []trace.Link
59+
60+
func (l linkOption) apply(o *spanOptions) { o.links = append(o.links, l...) }
61+
62+
// WithLinks adds links to the span.
63+
func WithLinks(links ...trace.Link) SpanOption { return linkOption(links) }
64+
65+
type otelOption []trace.SpanStartOption
66+
67+
func (t otelOption) apply(o *spanOptions) { o.otelOpts = append(o.otelOpts, t...) }
68+
69+
// WithOTELOptions allows passing raw OpenTelemetry options directly.
70+
// This is an escape hatch for advanced features not yet wrapped by Ion.
71+
func WithOTELOptions(opts ...trace.SpanStartOption) SpanOption { return otelOption(opts) }
72+
5673
// --- OTEL Tracer Implementation ---
5774

5875
type otelTracer struct {
@@ -73,6 +90,12 @@ func (t *otelTracer) Start(ctx context.Context, spanName string, opts ...SpanOpt
7390
if len(o.attributes) > 0 {
7491
traceOpts = append(traceOpts, trace.WithAttributes(o.attributes...))
7592
}
93+
if len(o.links) > 0 {
94+
traceOpts = append(traceOpts, trace.WithLinks(o.links...))
95+
}
96+
if len(o.otelOpts) > 0 {
97+
traceOpts = append(traceOpts, o.otelOpts...)
98+
}
7699

77100
ctx, span := t.tracer.Start(ctx, spanName, traceOpts...)
78101
return ctx, &otelSpan{span: span}

tracer_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package ion
2+
3+
import (
4+
"testing"
5+
6+
"go.opentelemetry.io/otel/trace"
7+
)
8+
9+
func TestTracer_WithLinks(t *testing.T) {
10+
// calling WithLinks should not panic
11+
opts := WithLinks(trace.Link{
12+
SpanContext: trace.SpanContext{},
13+
})
14+
15+
so := &spanOptions{}
16+
opts.apply(so)
17+
18+
if len(so.links) != 1 {
19+
t.Errorf("Expected 1 link, got %d", len(so.links))
20+
}
21+
}
22+
23+
func TestTracer_WithOTELOptions(t *testing.T) {
24+
// calling WithOTELOptions should not panic
25+
// We can't easily inspect the internal otel options without reflection or a mock,
26+
// but we can ensure it appends to our internal slice.
27+
28+
// Create a dummy option
29+
dummyOpt := trace.WithAttributes()
30+
31+
opts := WithOTELOptions(dummyOpt)
32+
33+
so := &spanOptions{}
34+
opts.apply(so)
35+
36+
if len(so.otelOpts) != 1 {
37+
t.Errorf("Expected 1 otel option, got %d", len(so.otelOpts))
38+
}
39+
}

0 commit comments

Comments
 (0)