diff --git a/host.go b/host.go index 28b7e32..e9b9ada 100644 --- a/host.go +++ b/host.go @@ -1,6 +1,10 @@ package provider -import "encoding/json" +import ( + "encoding/json" + "fmt" + "strings" +) type RedactedString string @@ -31,6 +35,118 @@ type OtelConfig struct { Protocol string `json:"protocol,omitempty"` } +type otelSignal int + +const ( + traces otelSignal = iota + metrics + logs + + // https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_endpoint + OtelExporterGrpcEndpoint = "http://localhost:4317" + OtelExporterHttpEndpoint = "http://localhost:4318" + + // https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_traces_endpoint + OtelExporterHttpTracesPath = "/v1/traces" + // https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_metrics_endpoint + OtelExporterHttpMetricsPath = "/v1/metrics" + // https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_logs_endpoint + OtelExporterHttpLogsPath = "/v1/logs" +) + +// OtelProtocol returns the configured OpenTelemetry protocol if one is provided, +// otherwise defaulting to http. +func (config *OtelConfig) OtelProtocol() string { + protocol := OtelProtocolHTTP + if config.Protocol != "" { + protocol = strings.ToLower(config.Protocol) + } + return protocol +} + +// TracesURL returns the configured TracesEndpoint as-is if one is provided, +// otherwise it resolves the URL based on ObservabilityEndpoint value and the +// Protocol appropriate path. +func (config *OtelConfig) TracesURL() string { + if config.TracesEndpoint != "" { + return config.TracesEndpoint + } + + return config.resolveSignalUrl(traces) +} + +// MetricsURL returns the configured MetricsEndpoint as-is if one is provided, +// otherwise it resolves the URL based on ObservabilityEndpoint value and the +// Protocol appropriate path. +func (config *OtelConfig) MetricsURL() string { + if config.MetricsEndpoint != "" { + return config.MetricsEndpoint + } + + return config.resolveSignalUrl(metrics) +} + +// LogsURL returns the configured LogsEndpoint as-is if one is provided, +// otherwise it resolves the URL based on ObservabilityEndpoint value and the +// Protocol appropriate path. +func (config *OtelConfig) LogsURL() string { + if config.LogsEndpoint != "" { + return config.LogsEndpoint + } + + return config.resolveSignalUrl(logs) +} + +// TracesEnabled returns whether emitting traces has been enabled. +func (config *OtelConfig) TracesEnabled() bool { + return config.EnableObservability || config.EnableTraces +} + +// MetricsEnabled returns whether emitting metrics has been enabled. +func (config *OtelConfig) MetricsEnabled() bool { + return config.EnableObservability || config.EnableMetrics +} + +// LogsEnabled returns whether emitting logs has been enabled. +func (config *OtelConfig) LogsEnabled() bool { + return config.EnableObservability || config.EnableLogs +} + +func (config *OtelConfig) resolveSignalUrl(signal otelSignal) string { + endpoint := config.defaultEndpoint() + if config.ObservabilityEndpoint != "" { + endpoint = config.ObservabilityEndpoint + } + endpoint = strings.TrimRight(endpoint, "/") + + return fmt.Sprintf("%s%s", endpoint, config.defaultSignalPath(signal)) +} + +func (config *OtelConfig) defaultEndpoint() string { + endpoint := OtelExporterHttpEndpoint + if config.OtelProtocol() == OtelProtocolGRPC { + endpoint = OtelExporterGrpcEndpoint + } + return endpoint +} + +func (config *OtelConfig) defaultSignalPath(signal otelSignal) string { + // In case of gRPC, we return empty string gRPC doesn't need a path to be set for it. + if config.OtelProtocol() == OtelProtocolGRPC { + return "" + } + + switch signal { + case traces: + return OtelExporterHttpTracesPath + case metrics: + return OtelExporterHttpMetricsPath + case logs: + return OtelExporterHttpLogsPath + } + return "" +} + type HostData struct { HostID string `json:"host_id,omitempty"` LatticeRPCPrefix string `json:"lattice_rpc_prefix,omitempty"` diff --git a/host_test.go b/host_test.go index de7ea96..5393750 100644 --- a/host_test.go +++ b/host_test.go @@ -28,3 +28,168 @@ func TestRedactedStringLogging(t *testing.T) { t.Error("text slog handler should not have contained the secret string") } } + +func TestOtelConfigProtocol(t *testing.T) { + type test struct { + name string + config OtelConfig + protocol string + } + + tests := []test{ + { + name: "Defaults to http", + config: OtelConfig{}, + protocol: "http", + }, + { + name: "Explicit Grpc Rust enum variant", + config: OtelConfig{Protocol: "Grpc"}, + protocol: "grpc", + }, + { + name: "Explicit Http Rust enum variant", + config: OtelConfig{Protocol: "Http"}, + protocol: "http", + }, + } + + for _, tc := range tests { + if tc.config.OtelProtocol() != tc.protocol { + t.Fatalf("%s / OtelProtocol: expected %q, got: %q", tc.name, tc.protocol, tc.config.OtelProtocol()) + } + } +} + +func TestOtelConfigURLs(t *testing.T) { + type test struct { + name string + config OtelConfig + tracesURL string + metricsURL string + logsURL string + } + + tests := []test{ + { + name: "Defaults with HTTP", + config: OtelConfig{}, + tracesURL: "http://localhost:4318/v1/traces", + metricsURL: "http://localhost:4318/v1/metrics", + logsURL: "http://localhost:4318/v1/logs", + }, + { + name: "Defaults with gRPC", + config: OtelConfig{Protocol: "Grpc"}, + tracesURL: "http://localhost:4317", + metricsURL: "http://localhost:4317", + logsURL: "http://localhost:4317", + }, + { + name: "Custom ObservabilityEndpoint", + config: OtelConfig{ObservabilityEndpoint: "https://api.opentelemetry.com"}, + tracesURL: "https://api.opentelemetry.com/v1/traces", + metricsURL: "https://api.opentelemetry.com/v1/metrics", + logsURL: "https://api.opentelemetry.com/v1/logs", + }, + { + name: "Custom ObservabilityEndpoint with gRPC", + config: OtelConfig{Protocol: "grpc", ObservabilityEndpoint: "https://api.opentelemetry.com"}, + tracesURL: "https://api.opentelemetry.com", + metricsURL: "https://api.opentelemetry.com", + logsURL: "https://api.opentelemetry.com", + }, + { + name: "Custom TracesEndpoint", + config: OtelConfig{TracesEndpoint: "https://api.opentelemetry.com/v1/traces"}, + tracesURL: "https://api.opentelemetry.com/v1/traces", + metricsURL: "http://localhost:4318/v1/metrics", + logsURL: "http://localhost:4318/v1/logs", + }, + { + name: "Custom MetricsEndpoint", + config: OtelConfig{MetricsEndpoint: "https://api.opentelemetry.com/v1/metrics"}, + tracesURL: "http://localhost:4318/v1/traces", + metricsURL: "https://api.opentelemetry.com/v1/metrics", + logsURL: "http://localhost:4318/v1/logs", + }, + { + name: "Custom LogsEndpoint", + config: OtelConfig{LogsEndpoint: "https://api.opentelemetry.com/v1/logs"}, + tracesURL: "http://localhost:4318/v1/traces", + metricsURL: "http://localhost:4318/v1/metrics", + logsURL: "https://api.opentelemetry.com/v1/logs", + }, + } + + for _, tc := range tests { + if tc.config.TracesURL() != tc.tracesURL { + t.Fatalf("%s / TracesURL: expected %s, got: %s", tc.name, tc.tracesURL, tc.config.TracesURL()) + } + if tc.config.MetricsURL() != tc.metricsURL { + t.Fatalf("%s / MetricsURL: expected %s, got: %s", tc.name, tc.metricsURL, tc.config.MetricsURL()) + } + if tc.config.LogsURL() != tc.logsURL { + t.Fatalf("%s / LogsURL: expected %s, got: %s", tc.name, tc.logsURL, tc.config.LogsURL()) + } + } +} + +func TestOtelConfigBooleans(t *testing.T) { + type test struct { + name string + config OtelConfig + tracesEnabled bool + metricsEnabled bool + logsEnabled bool + } + + tests := []test{ + { + name: "Defaults", + config: OtelConfig{}, + tracesEnabled: false, + metricsEnabled: false, + logsEnabled: false, + }, + { + name: "Enable all with EnableObservability", + config: OtelConfig{EnableObservability: true}, + tracesEnabled: true, + metricsEnabled: true, + logsEnabled: true, + }, + { + name: "Enable just traces", + config: OtelConfig{EnableTraces: true}, + tracesEnabled: true, + metricsEnabled: false, + logsEnabled: false, + }, + { + name: "Enable just metrics", + config: OtelConfig{EnableMetrics: true}, + tracesEnabled: false, + metricsEnabled: true, + logsEnabled: false, + }, + { + name: "Enable just logs", + config: OtelConfig{EnableLogs: true}, + tracesEnabled: false, + metricsEnabled: false, + logsEnabled: true, + }, + } + for _, tc := range tests { + if tc.config.TracesEnabled() != tc.tracesEnabled { + t.Fatalf("%s / TracesEnabled: expected %t, got: %t", tc.name, tc.tracesEnabled, tc.config.TracesEnabled()) + } + if tc.config.MetricsEnabled() != tc.metricsEnabled { + t.Fatalf("%s / MetricsEnabled: expected %t, got: %t", tc.name, tc.metricsEnabled, tc.config.MetricsEnabled()) + } + if tc.config.LogsEnabled() != tc.logsEnabled { + t.Fatalf("%s / LogsEnabled: expected %t, got: %t", tc.name, tc.logsEnabled, tc.config.LogsEnabled()) + } + } +} diff --git a/observability.go b/observability.go index 90d21ba..0b3cedc 100644 --- a/observability.go +++ b/observability.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "time" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" @@ -43,16 +42,11 @@ func newTracerProvider(ctx context.Context, config OtelConfig, serviceResource * var exporter trace.SpanExporter var err error - endpoint := config.TracesEndpoint - if endpoint == "" { - endpoint = config.ObservabilityEndpoint - } - - switch strings.ToLower(config.Protocol) { + switch config.OtelProtocol() { case OtelProtocolGRPC: - exporter, err = otlptracegrpc.New(ctx, otlptracegrpc.WithEndpointURL(endpoint)) + exporter, err = otlptracegrpc.New(ctx, otlptracegrpc.WithEndpointURL(config.TracesURL())) case OtelProtocolHTTP: - exporter, err = otlptracehttp.New(ctx, otlptracehttp.WithEndpointURL(endpoint)) + exporter, err = otlptracehttp.New(ctx, otlptracehttp.WithEndpointURL(config.TracesURL())) default: return nil, fmt.Errorf("unknown observability protocol %q", config.Protocol) } @@ -82,16 +76,11 @@ func newMeterProvider(ctx context.Context, config OtelConfig, serviceResource *r var exporter metric.Exporter var err error - endpoint := config.MetricsEndpoint - if endpoint == "" { - endpoint = config.ObservabilityEndpoint - } - - switch strings.ToLower(config.Protocol) { + switch config.OtelProtocol() { case OtelProtocolGRPC: - exporter, err = otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithEndpointURL(endpoint)) + exporter, err = otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithEndpointURL(config.MetricsURL())) case OtelProtocolHTTP: - exporter, err = otlpmetrichttp.New(ctx, otlpmetrichttp.WithEndpointURL(endpoint)) + exporter, err = otlpmetrichttp.New(ctx, otlpmetrichttp.WithEndpointURL(config.MetricsURL())) default: return nil, fmt.Errorf("unknown observability protocol %q", config.Protocol) } @@ -113,16 +102,11 @@ func newLoggerProvider(ctx context.Context, config OtelConfig, serviceResource * var exporter log.Exporter var err error - endpoint := config.LogsEndpoint - if endpoint == "" { - endpoint = config.ObservabilityEndpoint - } - - switch strings.ToLower(config.Protocol) { + switch config.OtelProtocol() { case OtelProtocolGRPC: - exporter, err = otlploggrpc.New(ctx, otlploggrpc.WithEndpointURL(endpoint)) + exporter, err = otlploggrpc.New(ctx, otlploggrpc.WithEndpointURL(config.LogsURL())) case OtelProtocolHTTP: - exporter, err = otlploghttp.New(ctx, otlploghttp.WithEndpointURL(endpoint)) + exporter, err = otlploghttp.New(ctx, otlploghttp.WithEndpointURL(config.LogsURL())) default: return nil, fmt.Errorf("unknown observability protocol %q", config.Protocol) } diff --git a/provider.go b/provider.go index c2984de..19418f3 100644 --- a/provider.go +++ b/provider.go @@ -125,7 +125,7 @@ func NewWithHostDataSource(source io.Reader, options ...ProviderHandler) (*Wasmc return nil, err } - if hostData.OtelConfig.EnableObservability || hostData.OtelConfig.EnableMetrics { + if hostData.OtelConfig.MetricsEnabled() { meterProvider, err := newMeterProvider(context.Background(), hostData.OtelConfig, serviceResource) if err != nil { return nil, err @@ -134,7 +134,7 @@ func NewWithHostDataSource(source io.Reader, options ...ProviderHandler) (*Wasmc internalShutdownFuncs = append(internalShutdownFuncs, func(c context.Context) error { return meterProvider.Shutdown(c) }) } - if hostData.OtelConfig.EnableObservability || hostData.OtelConfig.EnableTraces { + if hostData.OtelConfig.TracesEnabled() { tracerProvider, err := newTracerProvider(context.Background(), hostData.OtelConfig, serviceResource) if err != nil { return nil, err @@ -143,7 +143,7 @@ func NewWithHostDataSource(source io.Reader, options ...ProviderHandler) (*Wasmc internalShutdownFuncs = append(internalShutdownFuncs, func(c context.Context) error { return tracerProvider.Shutdown(c) }) } - if hostData.OtelConfig.EnableObservability || hostData.OtelConfig.EnableLogs { + if hostData.OtelConfig.LogsEnabled() { loggerProvider, err := newLoggerProvider(context.Background(), hostData.OtelConfig, serviceResource) if err != nil { return nil, err