Skip to content
Closed
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
12 changes: 12 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ type Config struct {
// EnableOTELMetrics enables OpenTelemetry metrics export via OTLP alongside
// Prometheus. Does not replace Prometheus. Default false.
EnableOTELMetrics bool `envconfig:"ENABLE_OTEL_METRICS" default:"false"`

// 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:""`
Comment on lines +183 to +189
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.

// OTELMetricsInterval controls the export interval in seconds for OTEL
// metrics. Default 60.
OTELMetricsInterval int `envconfig:"OTEL_METRICS_INTERVAL" default:"60"`
Expand Down Expand Up @@ -291,5 +299,9 @@ func (c Config) Validate() []string {
warnings = append(warnings, "RateLimitBurst should be positive when RateLimitPerSecond is set")
}

if c.OTELGRPCSpanNameFormat != "" && c.OTELGRPCSpanNameFormat != "short" && c.OTELGRPCSpanNameFormat != "full" {
warnings = append(warnings, "OTELGRPCSpanNameFormat must be 'short' or 'full', got '"+c.OTELGRPCSpanNameFormat+"'; defaulting to 'full'")
}

return warnings
}
30 changes: 30 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,33 @@ func TestValidateTimeoutExceedsShutdown(t *testing.T) {
t.Error("timeout exceeding shutdown duration should produce a warning")
}
}

func TestValidateOTELGRPCSpanNameFormat(t *testing.T) {
// Invalid format
c := Config{
GRPCPort: 9090,
HTTPPort: 9091,
OTELGRPCSpanNameFormat: "invalid",
}
warnings := c.Validate()
found := false
for _, w := range warnings {
if strings.Contains(w, "OTELGRPCSpanNameFormat") {
found = true
}
}
if !found {
t.Error("invalid OTELGRPCSpanNameFormat should produce a warning")
}

// Valid formats should not warn
for _, format := range []string{"short", "full", ""} {
c.OTELGRPCSpanNameFormat = format
warnings = c.Validate()
for _, w := range warnings {
if strings.Contains(w, "OTELGRPCSpanNameFormat") {
t.Errorf("format %q should not produce a warning, got: %s", format, w)
}
}
}
}
49 changes: 22 additions & 27 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,38 +180,33 @@ func (c *cb) processConfig() {
if c.config.OTLPEndpoint != "" {
headers := parseHeaders(c.config.OTLPHeaders)
otlpConfig = OTLPConfig{
Endpoint: c.config.OTLPEndpoint,
Headers: headers,
ServiceName: c.config.AppName,
ServiceVersion: c.config.ReleaseName,
SamplingRatio: c.config.OTLPSamplingRatio,
Compression: c.config.OTLPCompression,
Insecure: c.config.OTLPInsecure,
Endpoint: c.config.OTLPEndpoint,
Headers: headers,
ServiceName: c.config.AppName,
ServiceVersion: c.config.ReleaseName,
SamplingRatio: c.config.OTLPSamplingRatio,
Compression: c.config.OTLPCompression,
Insecure: c.config.OTLPInsecure,
GRPCSpanNameFormat: c.config.OTELGRPCSpanNameFormat,
FilterSpanNames: c.config.OTELFilterSpanNames,
}
if err := SetupOpenTelemetry(otlpConfig); err != nil {
log.Error(context.Background(), "msg", "Failed to setup custom OTLP", "err", err)
}
} else if c.config.NewRelicOpentelemetry {
err := SetupNROpenTelemetry(
nrName,
c.config.NewRelicLicenseKey,
c.config.ReleaseName,
c.config.NewRelicOpentelemetrySample,
)
if err != nil {
log.Error(context.Background(), "msg", "Failed to setup New Relic OpenTelemetry", "err", err)
} else if c.config.NewRelicOpentelemetry && strings.TrimSpace(c.config.NewRelicLicenseKey) != "" {
// Build full config for NR path to include filter/transformer settings.
otlpConfig = OTLPConfig{
Endpoint: nrOTLPEndpoint,
Headers: map[string]string{"api-key": c.config.NewRelicLicenseKey},
ServiceName: nrName,
ServiceVersion: c.config.ReleaseName,
SamplingRatio: c.config.NewRelicOpentelemetrySample,
Compression: "gzip",
GRPCSpanNameFormat: c.config.OTELGRPCSpanNameFormat,
FilterSpanNames: c.config.OTELFilterSpanNames,
}
// Build otlpConfig for NR path so OTEL metrics can reuse the endpoint.
// Only populate when the license key is non-empty (SetupNROpenTelemetry
// no-ops without it, so metrics would just get auth failures).
if strings.TrimSpace(c.config.NewRelicLicenseKey) != "" {
otlpConfig = OTLPConfig{
Endpoint: nrOTLPEndpoint,
Headers: map[string]string{"api-key": c.config.NewRelicLicenseKey},
ServiceName: nrName,
ServiceVersion: c.config.ReleaseName,
Compression: "gzip",
}
if err := SetupOpenTelemetry(otlpConfig); err != nil {
log.Error(context.Background(), "msg", "Failed to setup New Relic OpenTelemetry", "err", err)
}
}

Expand Down
51 changes: 49 additions & 2 deletions initializers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

metricCollector "github.com/afex/hystrix-go/hystrix/metric_collector"
cbotel "github.com/go-coldbrew/core/otel"
"github.com/go-coldbrew/errors/notifier"
"github.com/go-coldbrew/hystrixprometheus" //nolint:staticcheck // deprecated but still in use
"github.com/go-coldbrew/interceptors"
Expand All @@ -26,9 +27,9 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
otelmetric "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
Expand Down Expand Up @@ -145,6 +146,15 @@ type OTLPConfig struct {
// Insecure disables TLS verification for the connection
// Only use this for local development or testing
Insecure bool

// GRPCSpanNameFormat controls gRPC span naming.
// "short" extracts just the method name (e.g., "V0GetStats")
// "full" keeps the full path (e.g., "/pkg.Service/V0GetStats") - default
GRPCSpanNameFormat string

// FilterSpanNames is a comma-separated string of span names to filter out.
// Common use: "ServeHTTP" to filter HTTP transport spans.
FilterSpanNames string
}

// nrOTLPEndpoint is the New Relic OTLP gRPC endpoint.
Expand All @@ -157,6 +167,9 @@ var otelResource *resource.Resource
// otelTracerProvider stores the concrete TracerProvider for shutdown.
var otelTracerProvider *sdktrace.TracerProvider

// otelSpanProcessor stores the custom span processor for runtime filter/transformer additions.
var otelSpanProcessor *cbotel.SpanProcessor

// buildOTELResource builds a resource with service name, version, build info,
// and VCS metadata. The result is cached in otelResource for reuse.
func buildOTELResource(serviceName, serviceVersion string) (*resource.Resource, error) {
Expand Down Expand Up @@ -268,9 +281,25 @@ func SetupOpenTelemetry(config OTLPConfig) error {
ratio = 0.2
}

// Wrap the batcher with custom SpanProcessor for filtering/transformation.
batcher := sdktrace.NewBatchSpanProcessor(otlpExporter)
var filterNames []string
if config.FilterSpanNames != "" {
for _, name := range strings.Split(config.FilterSpanNames, ",") {
if trimmed := strings.TrimSpace(name); trimmed != "" {
filterNames = append(filterNames, trimmed)
}
}
}
processor := cbotel.NewSpanProcessor(batcher, cbotel.SpanProcessorConfig{
GRPCSpanNameFormat: config.GRPCSpanNameFormat,
FilterSpanNames: filterNames,
})
otelSpanProcessor = processor

tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(ratio))),
sdktrace.WithBatcher(otlpExporter),
sdktrace.WithSpanProcessor(processor),
sdktrace.WithResource(r),
)
otelTracerProvider = tracerProvider
Expand Down Expand Up @@ -506,3 +535,21 @@ func (vtprotoCodec) Name() string {
// name registered for the proto compressor
return "proto"
}

// AddOTELSpanFilter adds a custom span filter at runtime.
// Filters are checked for each span; if any filter returns true, the span is dropped.
// Must be called after SetupOpenTelemetry; no-op if OTEL is not initialized.
func AddOTELSpanFilter(f cbotel.SpanFilter) {
if otelSpanProcessor != nil {
otelSpanProcessor.AddFilter(f)
}
}

// AddOTELSpanTransformer adds a custom span transformer at runtime.
// Transformers are applied in order; first non-empty result wins.
// Must be called after SetupOpenTelemetry; no-op if OTEL is not initialized.
func AddOTELSpanTransformer(t cbotel.SpanTransformer) {
if otelSpanProcessor != nil {
otelSpanProcessor.AddTransformer(t)
}
}
Loading