Skip to content

Commit 050f8f5

Browse files
authored
otel: Add multi-standard propagator and fix gcp observability (#240)
* otel: Add gcp propagator * tests * go mod * add debug logging * Adds span force flushing on close Ensures all spans are exported before shutdown to prevent data loss, especially in environments like Cloud Run where containers can terminate abruptly. Introduces a `ForceFlush` method to the `Observer` interface and its implementations, which immediately exports all pending spans. The `Close` method now calls `ForceFlush` before shutting down the tracer provider. A default timeout for force flushing is introduced. * Adds ForceFlush method to Sentry provider This change adds a `ForceFlush` method to the Sentry provider. This method allows for explicitly flushing the Sentry client, ensuring that any pending events are sent to Sentry. This is useful in scenarios where immediate event delivery is required, such as before application shutdown. * Adds simple span export option Allows users to enable immediate span export without batching, providing real-time visibility at the cost of increased network overhead. This introduces a `use_simple_span` configuration option, enabling spans to be exported immediately when they finish, instead of being batched. * linting issue * Improves hostname detection for service identification Enhances hostname detection by prioritizing GCP metadata service, falling back to OS hostname, and finally using a default identifier. This ensures reliable service identification in multi-server and cloud environments. Removes unnecessary logging from span start and finish. * Enhances hostname detection for services Improves service identification by prioritizing environment variables for hostname detection. This change replaces reliance on GCP metadata service lookups with a more robust approach using environment variables. It constructs a meaningful hostname based on variables such as service name, revision, and project ID, improving accuracy and avoiding unnecessary external requests. It falls back to system hostname or a default "unknown-host" if no suitable environment variables are found. The hostname initialization is performed only once to cache results. * Adds span counting feature Introduces span counting to track and log the number of spans within a trace. This enables better observability by providing insights into trace complexity and performance. It introduces a new `SpanCountingProcessor` that wraps the existing span processor. The feature can be enabled or disabled via configuration. Adds a YAML parsing test for `SpanCountingConfig`. Updates hostname logic to avoid duplication when the revision already contains the service name. * Enables GCP sampler for cloud environments Configures the observability provider to prioritize local sampling decisions in cloud environments like Cloud Run to prevent trace loss. Logs additional debug information for configuration values. * Enables trace propagation configuration Adds support for configuring trace propagation formats (W3C, B3, Jaeger). This allows users to specify which propagation formats should be used when sending and receiving traces. Defaults to W3C if no formats are specified. This ensures interoperability with different tracing systems. Also includes integration tests and examples to verify proper propagation of trace context with different formats. * Configures propagation Configures OpenTelemetry propagation based on user-defined formats or defaults to W3C. Adds debug logging to track propagation configuration and span counting settings, aiding in troubleshooting and verification. * Refactors observability configuration and hostname detection This commit streamlines the observability configuration by removing the span counting feature and simplifying the propagation defaults. It introduces a centralized hostname detection mechanism to improve service identification across different GCP environments. The previous hostname detection logic was moved to `pkg/zobservability/hostname.go` for reuse and consistency. The propagation default is now B3 because it's the only one supported by GCP+Signoz. * Clean up * clean up
1 parent 3c0a8d8 commit 050f8f5

File tree

16 files changed

+1374
-46
lines changed

16 files changed

+1374
-46
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ require (
2525
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2
2626
go.opentelemetry.io/contrib/bridges/otelzap v0.12.0
2727
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0
28+
go.opentelemetry.io/contrib/propagators/b3 v1.37.0
29+
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0
2830
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0
2931
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0
3032
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
297297
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
298298
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
299299
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
300+
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA=
301+
go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg=
302+
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM=
303+
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM=
300304
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
301305
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
302306
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 h1:z6lNIajgEBVtQZHjfw2hAccPEBDs+nx58VemmXWa2ec=

pkg/zobservability/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import (
44
"fmt"
55
)
66

7+
// PropagationConfig controls trace propagation formats
8+
type PropagationConfig struct {
9+
Formats []string `yaml:"formats" mapstructure:"formats"` // ["w3c", "b3", "b3-single", "jaeger"]
10+
}
11+
712
// Config holds configuration for all observability features (tracing, logging, metrics)
813
type Config struct {
914
Provider string `yaml:"provider" mapstructure:"provider"`
@@ -15,6 +20,7 @@ type Config struct {
1520
SampleRate float64 `yaml:"sample_rate" mapstructure:"sample_rate"` // Common sampling rate
1621
Middleware MiddlewareConfig `yaml:"middleware" mapstructure:"middleware"`
1722
Metrics MetricsConfig `yaml:"metrics" mapstructure:"metrics"` // Metrics configuration
23+
Propagation PropagationConfig `yaml:"propagation" mapstructure:"propagation"` // Trace propagation configuration
1824
CustomConfig map[string]string `yaml:"custom_config" mapstructure:"custom_config"` // Provider-specific configuration
1925
}
2026

@@ -60,4 +66,9 @@ func (c *Config) SetDefaults() {
6066
if c.Metrics.Provider == "" {
6167
c.Metrics = DefaultMetricsConfig()
6268
}
69+
70+
// Set propagation defaults
71+
if len(c.Propagation.Formats) == 0 {
72+
c.Propagation.Formats = []string{PropagationB3} // Default to B3 because is the only one supported by GCP+Signoz
73+
}
6374
}

pkg/zobservability/config_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,203 @@ func TestConfig_WhenSetDefaultsMultipleTimes_ShouldBeIdempotent(t *testing.T) {
308308
assert.Equal(t, firstSampleRate, config.SampleRate)
309309
assert.Equal(t, firstCaptureErrors, config.Middleware.CaptureErrors)
310310
}
311+
312+
// Tests for PropagationConfig
313+
314+
func TestPropagationConfig(t *testing.T) {
315+
tests := []struct {
316+
name string
317+
config PropagationConfig
318+
expectedLength int
319+
expectedFirst string
320+
}{
321+
{
322+
name: "single format",
323+
config: PropagationConfig{
324+
Formats: []string{PropagationB3},
325+
},
326+
expectedLength: 1,
327+
expectedFirst: PropagationB3,
328+
},
329+
{
330+
name: "multiple formats",
331+
config: PropagationConfig{
332+
Formats: []string{PropagationB3, PropagationW3C, PropagationJaeger},
333+
},
334+
expectedLength: 3,
335+
expectedFirst: PropagationB3,
336+
},
337+
{
338+
name: "empty formats",
339+
config: PropagationConfig{
340+
Formats: []string{},
341+
},
342+
expectedLength: 0,
343+
},
344+
{
345+
name: "nil formats",
346+
config: PropagationConfig{
347+
Formats: nil,
348+
},
349+
expectedLength: 0,
350+
},
351+
}
352+
353+
for _, tt := range tests {
354+
t.Run(tt.name, func(t *testing.T) {
355+
assert.Len(t, tt.config.Formats, tt.expectedLength)
356+
if tt.expectedLength > 0 {
357+
assert.Equal(t, tt.expectedFirst, tt.config.Formats[0])
358+
}
359+
})
360+
}
361+
}
362+
363+
func TestConfig_WhenSetDefaultsPropagationFormats_ShouldSetB3Default(t *testing.T) {
364+
tests := []struct {
365+
name string
366+
initialConfig Config
367+
expectedLength int
368+
expectedFirst string
369+
}{
370+
{
371+
name: "empty propagation formats get B3 default",
372+
initialConfig: Config{
373+
Propagation: PropagationConfig{
374+
Formats: []string{},
375+
},
376+
},
377+
expectedLength: 1,
378+
expectedFirst: PropagationB3,
379+
},
380+
{
381+
name: "nil propagation formats get B3 default",
382+
initialConfig: Config{
383+
Propagation: PropagationConfig{
384+
Formats: nil,
385+
},
386+
},
387+
expectedLength: 1,
388+
expectedFirst: PropagationB3,
389+
},
390+
{
391+
name: "existing formats are preserved",
392+
initialConfig: Config{
393+
Propagation: PropagationConfig{
394+
Formats: []string{PropagationB3, PropagationJaeger},
395+
},
396+
},
397+
expectedLength: 2,
398+
expectedFirst: PropagationB3,
399+
},
400+
}
401+
402+
for _, tt := range tests {
403+
t.Run(tt.name, func(t *testing.T) {
404+
config := tt.initialConfig
405+
config.SetDefaults()
406+
407+
assert.Len(t, config.Propagation.Formats, tt.expectedLength)
408+
if tt.expectedLength > 0 {
409+
assert.Equal(t, tt.expectedFirst, config.Propagation.Formats[0])
410+
}
411+
})
412+
}
413+
}
414+
415+
func TestConfig_WhenValidateWithPropagation_ShouldSucceed(t *testing.T) {
416+
tests := []struct {
417+
name string
418+
config Config
419+
expectError bool
420+
}{
421+
{
422+
name: "valid config with propagation",
423+
config: Config{
424+
Provider: ProviderSigNoz,
425+
Enabled: true,
426+
Environment: "test",
427+
Address: "localhost:4317",
428+
Propagation: PropagationConfig{
429+
Formats: []string{PropagationB3},
430+
},
431+
},
432+
expectError: false,
433+
},
434+
{
435+
name: "valid config with multiple propagation formats",
436+
config: Config{
437+
Provider: ProviderSigNoz,
438+
Enabled: true,
439+
Environment: "test",
440+
Address: "localhost:4317",
441+
Propagation: PropagationConfig{
442+
Formats: []string{PropagationB3, PropagationW3C, PropagationJaeger},
443+
},
444+
},
445+
expectError: false,
446+
},
447+
{
448+
name: "valid config with empty propagation formats",
449+
config: Config{
450+
Provider: ProviderSigNoz,
451+
Enabled: true,
452+
Environment: "test",
453+
Address: "localhost:4317",
454+
Propagation: PropagationConfig{
455+
Formats: []string{},
456+
},
457+
},
458+
expectError: false,
459+
},
460+
{
461+
name: "disabled config with propagation doesn't validate",
462+
config: Config{
463+
Provider: ProviderSigNoz,
464+
Enabled: false,
465+
Environment: "",
466+
Address: "",
467+
Propagation: PropagationConfig{
468+
Formats: []string{PropagationB3},
469+
},
470+
},
471+
expectError: false,
472+
},
473+
}
474+
475+
for _, tt := range tests {
476+
t.Run(tt.name, func(t *testing.T) {
477+
err := tt.config.Validate()
478+
if tt.expectError {
479+
assert.Error(t, err)
480+
} else {
481+
assert.NoError(t, err)
482+
}
483+
})
484+
}
485+
}
486+
487+
func TestConfig_WhenPropagationIntegration_ShouldWorkEndToEnd(t *testing.T) {
488+
// Test that a full configuration works end-to-end
489+
config := Config{
490+
Provider: ProviderSigNoz,
491+
Enabled: true,
492+
Environment: "test",
493+
Release: "1.0.0",
494+
Address: "localhost:4317",
495+
SampleRate: 1.0,
496+
Propagation: PropagationConfig{
497+
Formats: []string{PropagationB3, PropagationW3C},
498+
},
499+
}
500+
501+
// Set defaults and validate
502+
config.SetDefaults()
503+
err := config.Validate()
504+
assert.NoError(t, err)
505+
506+
// Check that propagation formats are preserved
507+
assert.Len(t, config.Propagation.Formats, 2)
508+
assert.Equal(t, PropagationB3, config.Propagation.Formats[0])
509+
assert.Equal(t, PropagationW3C, config.Propagation.Formats[1])
510+
}

pkg/zobservability/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ const (
66
ProviderSigNoz = "signoz"
77
)
88

9+
// Propagation format constants
10+
const (
11+
PropagationW3C = "w3c"
12+
PropagationB3 = "b3"
13+
PropagationB3Single = "b3-single"
14+
PropagationJaeger = "jaeger"
15+
)
16+
917
// Environment constants for observability configuration
1018
const (
1119
EnvironmentProduction = "production"

pkg/zobservability/factory/factory.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ func newSigNozObserver(config *zobservability.Config, serviceName string) (zobse
9494
ignoreParentSampling = strings.ToLower(ignoreParentStr) == signoz.ConfigKeyTrueValue
9595
}
9696

97+
// Parse SimpleSpan configuration
98+
useSimpleSpan := false
99+
if simpleSpanStr, ok := config.CustomConfig[signoz.ConfigKeyUseSimpleSpan]; ok {
100+
useSimpleSpan = strings.ToLower(simpleSpanStr) == signoz.ConfigKeyTrueValue
101+
}
102+
97103
// Parse advanced batch configuration if present
98104
// BatchConfig controls performance and batching behavior
99105
batchConfig, err := parseBatchConfig(config.CustomConfig)
@@ -124,6 +130,8 @@ func newSigNozObserver(config *zobservability.Config, serviceName string) (zobse
124130
BatchConfig: batchConfig, // Optional: nil means use defaults
125131
ResourceConfig: resourceConfig, // Optional: nil means use defaults
126132
IgnoreParentSampling: ignoreParentSampling, // Critical for Google Cloud Run deployments
133+
UseSimpleSpan: useSimpleSpan, // Enable immediate span export
134+
Propagation: config.Propagation, // Copy propagation configuration
127135
}
128136

129137
return signoz.NewObserver(signozConfig)

0 commit comments

Comments
 (0)