Skip to content

feat: extensible OTEL span filtering and naming#81

Closed
thoriqzs25 wants to merge 2 commits intogo-coldbrew:mainfrom
thoriqzs25:feat/otel-span-customization
Closed

feat: extensible OTEL span filtering and naming#81
thoriqzs25 wants to merge 2 commits intogo-coldbrew:mainfrom
thoriqzs25:feat/otel-span-customization

Conversation

@thoriqzs25
Copy link
Copy Markdown

@thoriqzs25 thoriqzs25 commented Apr 13, 2026

Add configurable span filtering and transformation for OpenTelemetry traces:

  • SpanFilter/SpanTransformer interfaces for client extensibility
  • OTEL_GRPC_SPAN_NAME_FORMAT=short|full for gRPC span naming
  • OTEL_FILTER_SPAN_NAMES for exact-match span filtering
  • AddOTELSpanFilter() and AddOTELSpanTransformer() runtime APIs

Description

Adds extensible OTEL span customization to reduce trace noise and improve span readability.

Package(s) Affected

  • core
  • interceptors
  • errors
  • log
  • tracing
  • options
  • grpcpool
  • data-builder
  • cookiecutter-coldbrew
  • docs

Checklist

  • Tests pass (make test)
  • Linter passes (make lint)
  • README regenerated if exported APIs changed (make doc)
  • No breaking changes
  • Doc comments added for new exported APIs

Summary by CodeRabbit

  • New Features

    • Configurable OpenTelemetry gRPC span naming (full or short).
    • Configurable list to filter/exclude specific span names to reduce trace noise.
    • Runtime APIs to register custom span filters and transformers for advanced span processing.
  • Improvements

    • Validation now warns when an invalid span naming format is provided and falls back to the default.

Add configurable span filtering and transformation for OpenTelemetry traces:

- SpanFilter/SpanTransformer interfaces for client extensibility
- OTEL_GRPC_SPAN_NAME_FORMAT=short|full for gRPC span naming
- OTEL_FILTER_SPAN_NAMES for exact-match span filtering
- AddOTELSpanFilter() and AddOTELSpanTransformer() runtime APIs
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

Adds OpenTelemetry span filtering and transformation: new config fields for gRPC span-name format and filtered span names, a configurable SpanProcessor that filters/transforms spans before batching/export, runtime APIs to register filters/transformers, and tests covering behavior and concurrency.

Changes

Cohort / File(s) Summary
Configuration
config/config.go
Added OTELGRPCSpanNameFormat and OTELFilterSpanNames config fields and validation warning for invalid span-name-format values.
Core OTEL wiring
core.go, initializers.go
Extended OTLPConfig with GRPCSpanNameFormat and FilterSpanNames; pass these through during OTLP/NR OTEL setup; replaced previous NR setup path to use SetupOpenTelemetry(otlpConfig); log SetupOpenTelemetry failures.
Span processor implementation
otel/spanprocessor.go
New SpanProcessor that wraps an sdktrace.SpanProcessor to drop exact-match names, apply gRPC short-name extraction, run ordered transformers, and support thread-safe runtime AddFilter/AddTransformer APIs.
Runtime APIs
initializers.go
Added AddOTELSpanFilter and AddOTELSpanTransformer functions to register filters/transformers at runtime (no-ops if OTEL not initialized).
Tests
otel/spanprocessor_test.go, config/config_test.go
Comprehensive tests for span filtering, short/full gRPC name handling, transformers, concurrency, ForceFlush/Shutdown behavior, and config validation for OTELGRPCSpanNameFormat.

Sequence Diagram(s)

sequenceDiagram
  participant Tracer as Tracer/SDK
  participant SpanProc as Custom SpanProcessor
  participant Batch as OTLP Batch Processor
  participant Exporter as OTLP Exporter

  Tracer->>SpanProc: OnStart / OnEnd(span)
  SpanProc->>SpanProc: apply FilterSpanNames / custom Filters
  alt span not dropped
    SpanProc->>SpanProc: optionally rename (gRPC short / transformers)
    SpanProc->>Batch: forward span
    Batch->>Exporter: batch & export
  else span dropped
    SpanProc-->>Tracer: drop (no forward)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • vestor
  • fajran
  • kevinjom
  • svetha-cvl

Poem

"🐇 I hopped through traces, soft and spry,
I trimmed long names so spans could fly,
Filters and tweaks with a wiggle and hop,
Transforming each span till the export goes pop! ✨"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding extensible OpenTelemetry span filtering and transformation capabilities.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
initializers.go (1)

170-171: Synchronize access to the global span processor.

SetupOpenTelemetry replaces otelSpanProcessor while the runtime registration APIs read it without any synchronization. That gives you a -race hit if filters/transformers are added during startup or after a re-init; an atomic.Pointer or small mutex around load/store would make the runtime API actually thread-safe.

Also applies to: 542-555

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@initializers.go` around lines 170 - 171, The global variable
otelSpanProcessor is accessed concurrently by SetupOpenTelemetry (which replaces
it) and runtime registration APIs that read it, causing race conditions;
synchronize access by changing otelSpanProcessor to an
atomic.Pointer[*cbotel.SpanProcessor] or guard all loads/stores with a small
sync.Mutex. Update SetupOpenTelemetry to store the new processor via
atomic.StorePointer (or mutex-protected assignment) and update all runtime
readers (the registration paths that load otelSpanProcessor / functions that add
filters/transformers) to use atomic.LoadPointer (or lock the mutex) before
dereferencing; ensure any nil checks follow the same atomic/mutex pattern so
reads and writes are consistently synchronized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/config.go`:
- Around line 183-189: Add validation for the OTELGRPCSpanNameFormat config
field inside the existing Validate() function so values other than "short" or
"full" produce a warning and the code falls back to "full"; specifically, check
the OTELGRPCSpanNameFormat string (from the struct field named
OTELGRPCSpanNameFormat) and if it is non-empty and not equal to "short" or
"full", emit a warning via the same logger used by other Validate() checks and
set/keep the value as "full" to preserve current behavior.

In `@otel/spanprocessor.go`:
- Around line 160-171: Modify SpanProcessor.AddFilter and AddTransformer to
reject nil registrations up-front: check the incoming SpanFilter (and adapters
like SpanFilterFunc(nil)) and SpanTransformer (and SpanTransformerFunc(nil)) for
nil before locking or appending, and either return early (no-op) or return an
error/boolean to the caller; ensure you update the methods that call these (and
document behavior) so a nil filter/transformer is not added to p.config.Filters
or p.config.Transformers and thus cannot cause a panic in OnEnd. Use the
existing method names SpanProcessor.AddFilter, SpanProcessor.AddTransformer and
reference p.config.Filters / p.config.Transformers and OnEnd when making the
change.
- Around line 27-31: The doc comment for SpanTransformer is misleading because
the Transform method only returns a replacement name and cannot mutate span
attributes; update the comments to state this is a renaming-only contract:
change the top comment to say SpanTransformer allows clients to rename spans
before export (not add attributes or mutate spans), and update the Transform
method comment to explicitly state it returns a new span name (return empty
string to keep the original) and that no other span data is modified; reference
SpanTransformer and Transform (taking sdktrace.ReadOnlySpan) so readers know
which symbols are constrained.

---

Nitpick comments:
In `@initializers.go`:
- Around line 170-171: The global variable otelSpanProcessor is accessed
concurrently by SetupOpenTelemetry (which replaces it) and runtime registration
APIs that read it, causing race conditions; synchronize access by changing
otelSpanProcessor to an atomic.Pointer[*cbotel.SpanProcessor] or guard all
loads/stores with a small sync.Mutex. Update SetupOpenTelemetry to store the new
processor via atomic.StorePointer (or mutex-protected assignment) and update all
runtime readers (the registration paths that load otelSpanProcessor / functions
that add filters/transformers) to use atomic.LoadPointer (or lock the mutex)
before dereferencing; ensure any nil checks follow the same atomic/mutex pattern
so reads and writes are consistently synchronized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 042587bb-c128-4453-a400-83526e4a565c

📥 Commits

Reviewing files that changed from the base of the PR and between 9c4cb95 and 028aed0.

📒 Files selected for processing (5)
  • config/config.go
  • core.go
  • initializers.go
  • otel/spanprocessor.go
  • otel/spanprocessor_test.go

Comment thread config/config.go
Comment on lines +183 to +189
// OTELGRPCSpanNameFormat controls gRPC span naming.
// "short" extracts just the method name (e.g., "V0GetStats")
// "full" keeps the full path (e.g., "/pkg.Service/V0GetStats") - default
OTELGRPCSpanNameFormat string `envconfig:"OTEL_GRPC_SPAN_NAME_FORMAT" default:"full"`
// OTELFilterSpanNames is a comma-separated list of span names to filter out (exact match).
// Common use: "ServeHTTP" to filter HTTP transport spans.
OTELFilterSpanNames string `envconfig:"OTEL_FILTER_SPAN_NAMES" default:""`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Warn on unsupported OTEL_GRPC_SPAN_NAME_FORMAT values.

Anything other than "short" currently falls back to full naming silently, so a typo just disables the feature. Adding a Validate() warning here would match how other enum-like config fields are handled in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/config.go` around lines 183 - 189, Add validation for the
OTELGRPCSpanNameFormat config field inside the existing Validate() function so
values other than "short" or "full" produce a warning and the code falls back to
"full"; specifically, check the OTELGRPCSpanNameFormat string (from the struct
field named OTELGRPCSpanNameFormat) and if it is non-empty and not equal to
"short" or "full", emit a warning via the same logger used by other Validate()
checks and set/keep the value as "full" to preserve current behavior.

Comment thread otel/spanprocessor.go Outdated
Comment thread otel/spanprocessor.go
- Add config validation for OTELGRPCSpanNameFormat (warn if not short/full)
- Clarify SpanTransformer docs: renaming-only contract
- Nil-safe AddFilter/AddTransformer to prevent OnEnd panics
- Add tests for validation and nil handling
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
otel/spanprocessor.go (1)

73-82: ⚠️ Potential issue | 🔴 Critical

Typed-nil adapters can still panic at runtime.

Line 162 and Line 173 only reject interface-nil. SpanFilterFunc(nil) / SpanTransformerFunc(nil) pass these checks, get stored, and can panic when invoked in OnEnd/maybeTransform. The same risk exists for initial config.Filters/config.Transformers passed at construction.

💡 Proposed fix
 type SpanProcessor struct {
 	next        sdktrace.SpanProcessor
 	config      SpanProcessorConfig
 	filterNames map[string]struct{}
 	mu          sync.RWMutex
 }

+func isNilSpanFilter(f SpanFilter) bool {
+	if f == nil {
+		return true
+	}
+	if fn, ok := f.(SpanFilterFunc); ok && fn == nil {
+		return true
+	}
+	return false
+}
+
+func isNilSpanTransformer(t SpanTransformer) bool {
+	if t == nil {
+		return true
+	}
+	if fn, ok := t.(SpanTransformerFunc); ok && fn == nil {
+		return true
+	}
+	return false
+}
+
 // NewSpanProcessor creates a new SpanProcessor wrapping the given processor.
 func NewSpanProcessor(next sdktrace.SpanProcessor, config SpanProcessorConfig) *SpanProcessor {
 	filterNames := make(map[string]struct{}, len(config.FilterSpanNames))
 	for _, name := range config.FilterSpanNames {
 		filterNames[name] = struct{}{}
 	}
+	filters := make([]SpanFilter, 0, len(config.Filters))
+	for _, f := range config.Filters {
+		if !isNilSpanFilter(f) {
+			filters = append(filters, f)
+		}
+	}
+	transformers := make([]SpanTransformer, 0, len(config.Transformers))
+	for _, t := range config.Transformers {
+		if !isNilSpanTransformer(t) {
+			transformers = append(transformers, t)
+		}
+	}
+	config.Filters = filters
+	config.Transformers = transformers
 	return &SpanProcessor{
 		next:        next,
 		config:      config,
 		filterNames: filterNames,
 	}
 }
@@
 func (p *SpanProcessor) AddFilter(f SpanFilter) {
-	if f == nil {
+	if isNilSpanFilter(f) {
 		return
 	}
 	p.mu.Lock()
 	defer p.mu.Unlock()
 	p.config.Filters = append(p.config.Filters, f)
 }
@@
 func (p *SpanProcessor) AddTransformer(t SpanTransformer) {
-	if t == nil {
+	if isNilSpanTransformer(t) {
 		return
 	}
 	p.mu.Lock()
 	defer p.mu.Unlock()
 	p.config.Transformers = append(p.config.Transformers, t)
 }
#!/bin/bash
# Verify whether typed-nil adapters are currently guarded and whether tests cover them.

set -euo pipefail

echo "== Guards in registration paths =="
rg -n -C3 'func \(p \*SpanProcessor\) AddFilter|func \(p \*SpanProcessor\) AddTransformer|if f == nil|if t == nil' otel/spanprocessor.go

echo
echo "== Adapter definitions =="
rg -n -C2 'type SpanFilterFunc|type SpanTransformerFunc' otel/spanprocessor.go

echo
echo "== Typed-nil test coverage =="
rg -n -C2 'SpanFilterFunc\(nil\)|SpanTransformerFunc\(nil\)|typed nil|nil filter|nil transformer' --type go

Also applies to: 161-178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@otel/spanprocessor.go` around lines 73 - 82, NewSpanProcessor, AddFilter, and
AddTransformer currently accept SpanFilterFunc/SpanTransformerFunc values that
can be typed-nil and later panic when invoked in OnEnd/maybeTransform; update
all registration paths (NewSpanProcessor iterating
config.Filters/config.Transformers, and methods AddFilter/AddTransformer) to
reject typed-nil adapters by checking both interface-nil and typed-nil (use
reflect.ValueOf(x).IsValid()/IsNil() for the underlying value) and skip or
return an error for such nil functions, and also add a defensive nil check
before invoking stored adapters in maybeTransform/OnEnd to avoid panics
referencing SpanFilterFunc, SpanTransformerFunc, maybeTransform, OnEnd,
config.Filters and config.Transformers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@otel/spanprocessor.go`:
- Around line 73-82: NewSpanProcessor, AddFilter, and AddTransformer currently
accept SpanFilterFunc/SpanTransformerFunc values that can be typed-nil and later
panic when invoked in OnEnd/maybeTransform; update all registration paths
(NewSpanProcessor iterating config.Filters/config.Transformers, and methods
AddFilter/AddTransformer) to reject typed-nil adapters by checking both
interface-nil and typed-nil (use reflect.ValueOf(x).IsValid()/IsNil() for the
underlying value) and skip or return an error for such nil functions, and also
add a defensive nil check before invoking stored adapters in
maybeTransform/OnEnd to avoid panics referencing SpanFilterFunc,
SpanTransformerFunc, maybeTransform, OnEnd, config.Filters and
config.Transformers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c239ecee-b2b5-4ae1-87cf-2ea29b6c0be6

📥 Commits

Reviewing files that changed from the base of the PR and between 028aed0 and 3ef1438.

📒 Files selected for processing (4)
  • config/config.go
  • config/config_test.go
  • otel/spanprocessor.go
  • otel/spanprocessor_test.go
✅ Files skipped from review due to trivial changes (1)
  • otel/spanprocessor_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/config.go

@thoriqzs25
Copy link
Copy Markdown
Author

Raised issue #80

@ankurs
Copy link
Copy Markdown
Member

ankurs commented Apr 14, 2026

Thanks for this @thoriqzs25, a lot of these changes have been pushed in the last few release, please update coldbrew and retry

@thoriqzs25 thoriqzs25 closed this Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants