From acd63ff410cf4a26d1f58f1b07b5d591e414356b Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 9 Nov 2026 10:23:47 +0800 Subject: [PATCH] fix(otel): resolve exporter protocol through startup configuration --- config.example.yaml | 1 + docs/changelog/index.md | 2 +- docs/deployment/server.md | 2 +- internal/cmd/root.go | 7 ++++--- internal/infra/config/config.go | 8 ++++++++ internal/infra/config/config_test.go | 21 +++++++++++++++++++++ internal/infra/config/model.go | 5 +++-- pkg/trace/trace.go | 7 ++++--- pkg/trace/trace_provider.go | 10 ++++------ pkg/trace/trace_provider_test.go | 26 ++++++++++++++++++-------- 10 files changed, 65 insertions(+), 24 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index a35e1049..2c380989 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -95,6 +95,7 @@ worker: # ─── OpenTelemetry Tracing ────────────────────────────────────────────────────── otel: + export_protocol: "grpc" # grpc 或 http/protobuf;OTEL_EXPORTER_OTLP_TRACES_PROTOCOL 优先于通用 OTLP 协议变量 sampling_rate: 0.0 # Trace sampling rate (0.0 – 1.0) tracer_name: "github.com/Rain-kl/OpenFlare" # Global tracer instrumentation name diff --git a/docs/changelog/index.md b/docs/changelog/index.md index df0bd5eb..d5e3be55 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -19,7 +19,7 @@ sidebar: false ### ✨ 新功能 -- OpenTelemetry 链路追踪新增 OTLP/HTTP protobuf 导出,通过标准环境变量配置接收端地址和自定义请求头,保留原有 OTLP/gRPC 默认模式;未支持的协议在启动时明确报错。 +- OpenTelemetry 链路追踪新增 OTLP/HTTP protobuf 导出,通过标准环境变量配置接收端地址和自定义请求头,协议由统一启动配置解析并支持 `otel.export_protocol`,保留原有 OTLP/gRPC 默认模式;未支持的协议在启动时明确报错。 - Cloudflare 指向分组支持选择自动 TTL 或手动填写 60~86400 秒,适用于节点绑定与自定义 A、AAAA、CNAME;灰云成员使用分组配置,橙云成员始终自动,旧分组保留原有 300 秒行为。 ## [v3.5.9] - 2026-11-07 diff --git a/docs/deployment/server.md b/docs/deployment/server.md index 3e1fb3b8..9fd36e92 100644 --- a/docs/deployment/server.md +++ b/docs/deployment/server.md @@ -282,7 +282,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT: https://collector.example.com OTEL_SAMPLING_RATE: "0.1" ``` -标准 SDK 会在通用 `OTEL_EXPORTER_OTLP_ENDPOINT` 的路径后追加 `/v1/traces`;例如上述配置发送到 `https://collector.example.com/v1/traces`。如果接收端使用自定义路径,请设置完整的 `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,SDK 将直接使用该 URL,不再追加路径。链路专用的 `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` 优先于通用协议配置。 +标准 SDK 会在通用 `OTEL_EXPORTER_OTLP_ENDPOINT` 的路径后追加 `/v1/traces`;例如上述配置发送到 `https://collector.example.com/v1/traces`。如果接收端使用自定义路径,请设置完整的 `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,SDK 将直接使用该 URL,不再追加路径。链路专用的 `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` 优先于通用协议配置;两者均未配置时使用 `otel.export_protocol`,缺省为 `grpc`。协议在启动配置层统一解析,再传入追踪组件。 需要鉴权或其他请求头时,使用可选的 `OTEL_EXPORTER_OTLP_HEADERS`,格式为 `key=value,key2=value2`,值按标准进行百分号编码,例如 `X-API-Key=example-key,X-Client-Name=example%20client`。链路专用的 `OTEL_EXPORTER_OTLP_TRACES_HEADERS` 优先于通用请求头配置。应用不预设鉴权方案;实际请求头与凭据由部署环境提供,不要将真实凭据写入仓库。 diff --git a/internal/cmd/root.go b/internal/cmd/root.go index d30ea4b7..5e2880a4 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -33,9 +33,10 @@ var rootCmd = &cobra.Command{ Compress: config.Config.Log.Compress, }) trace.Init(trace.Config{ - AppName: config.Config.App.AppName, - SamplingRate: config.Config.Otel.SamplingRate, - TracerName: config.Config.Otel.TracerName, + AppName: config.Config.App.AppName, + SamplingRate: config.Config.Otel.SamplingRate, + TracerName: config.Config.Otel.TracerName, + ExportProtocol: config.Config.Otel.ExportProtocol, }) }, PreRun: func(_ *cobra.Command, _ []string) { diff --git a/internal/infra/config/config.go b/internal/infra/config/config.go index be3a1cae..8325f34c 100644 --- a/internal/infra/config/config.go +++ b/internal/infra/config/config.go @@ -273,6 +273,14 @@ func applyEnvOverrides(c *configModel) { c.Log.Output = envStr("LOG_OUTPUT", c.Log.Output) // ─── OTel ─── + protocol := strings.TrimSpace(envStr("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "")) + if protocol == "" { + protocol = strings.TrimSpace(envStr("OTEL_EXPORTER_OTLP_PROTOCOL", "")) + } + if protocol == "" { + protocol = c.Otel.ExportProtocol + } + c.Otel.ExportProtocol = strings.ToLower(strings.TrimSpace(protocol)) c.Otel.SamplingRate = envFloat64("OTEL_SAMPLING_RATE", c.Otel.SamplingRate) c.Otel.TracerName = envStr("OTEL_TRACER_NAME", c.Otel.TracerName) diff --git a/internal/infra/config/config_test.go b/internal/infra/config/config_test.go index d353490f..39303123 100644 --- a/internal/infra/config/config_test.go +++ b/internal/infra/config/config_test.go @@ -47,3 +47,24 @@ func TestApplyClickHouseDefaultsEnablesWhenConfigured(t *testing.T) { t.Fatalf("hosts default not applied: %v", cfg.ClickHouse.Hosts) } } + +// TestOTLPProtocolPrecedence covers signal-specific, general, YAML and default settings. +func TestOTLPProtocolPrecedence(t *testing.T) { + for _, tt := range []struct{ name, signal, general, configured, want string }{ + {name: "default"}, + {name: "yaml", configured: "http/protobuf", want: "http/protobuf"}, + {name: "general", general: "http/protobuf", configured: "grpc", want: "http/protobuf"}, + {name: "signal", signal: " HTTP/PROTOBUF ", general: "grpc", want: "http/protobuf"}, + {name: "empty_signal", signal: " ", general: "grpc", configured: "http/protobuf", want: "grpc"}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", tt.signal) + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", tt.general) + cfg := &configModel{Otel: otelConfig{ExportProtocol: tt.configured}} + applyEnvOverrides(cfg) + if cfg.Otel.ExportProtocol != tt.want { + t.Errorf("OTLP protocol = %q, want %q", cfg.Otel.ExportProtocol, tt.want) + } + }) + } +} diff --git a/internal/infra/config/model.go b/internal/infra/config/model.go index 7fe0b7c1..43e7c670 100644 --- a/internal/infra/config/model.go +++ b/internal/infra/config/model.go @@ -138,6 +138,7 @@ type QueueConfig struct { // otelConfig OpenTelemetry 配置 type otelConfig struct { - SamplingRate float64 `mapstructure:"sampling_rate"` - TracerName string `mapstructure:"tracer_name"` + ExportProtocol string `mapstructure:"export_protocol"` + SamplingRate float64 `mapstructure:"sampling_rate"` + TracerName string `mapstructure:"tracer_name"` } diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go index 9586e49c..03be4cf8 100644 --- a/pkg/trace/trace.go +++ b/pkg/trace/trace.go @@ -27,9 +27,10 @@ func init() { // Config 链路追踪配置 type Config struct { - AppName string - SamplingRate float64 - TracerName string + AppName string + SamplingRate float64 + TracerName string + ExportProtocol string } // Init 初始化 Tracer Provider 并关联全局 Tracer 实例 diff --git a/pkg/trace/trace_provider.go b/pkg/trace/trace_provider.go index 7ad86c1f..80b142a9 100644 --- a/pkg/trace/trace_provider.go +++ b/pkg/trace/trace_provider.go @@ -17,6 +17,7 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" ) +// newTracerProvider combines SDK and application resources with a batched exporter. func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) { // 获取主机名和容器信息 hostname, err := os.Hostname() @@ -40,7 +41,7 @@ func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) { } // 初始化 Exporter - traceExporter, err := newTraceExporter(context.Background()) + traceExporter, err := newTraceExporter(context.Background(), cfg.ExportProtocol) if err != nil { return nil, err } @@ -56,11 +57,8 @@ func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) { // newTraceExporter selects the OTLP wire transport without changing existing gRPC defaults. // The SDK exporters read standard OTEL_EXPORTER_OTLP_* endpoint and header settings. -func newTraceExporter(ctx context.Context) (sdktrace.SpanExporter, error) { - protocol := strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"))) - if protocol == "" { - protocol = strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL"))) - } +func newTraceExporter(ctx context.Context, protocol string) (sdktrace.SpanExporter, error) { + protocol = strings.ToLower(strings.TrimSpace(protocol)) switch protocol { case "", "grpc": return otlptracegrpc.New(ctx) diff --git a/pkg/trace/trace_provider_test.go b/pkg/trace/trace_provider_test.go index a640b559..a07f7e03 100644 --- a/pkg/trace/trace_provider_test.go +++ b/pkg/trace/trace_provider_test.go @@ -15,6 +15,7 @@ import ( "google.golang.org/protobuf/proto" ) +// TestOTLPHTTPTraceExportHeadersAndResources verifies wire payloads against a local collector. func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) { type received struct { headers http.Header @@ -37,10 +38,8 @@ func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) { _, _ = w.Write(responseBody) })) defer server.Close() - t.Setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "") t.Setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "") t.Setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "") - t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", server.URL) t.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "X-API-Key=test-key,X-Client-Name=test%20client") t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "service.namespace=example,deployment.environment.name=test") @@ -65,8 +64,6 @@ func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) { { name: "trace_specific_overrides", env: map[string]string{ - "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", - "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "http/protobuf", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": server.URL + "/custom/ingest", "OTEL_EXPORTER_OTLP_TRACES_HEADERS": "X-API-Key=trace-key,X-Client-Name=trace%20client", }, @@ -79,7 +76,7 @@ func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) { for key, value := range tt.env { t.Setenv(key, value) } - provider, err := newTracerProvider(Config{AppName: "openflare-control-plane", SamplingRate: 1}) + provider, err := newTracerProvider(Config{AppName: "openflare-control-plane", SamplingRate: 1, ExportProtocol: "http/protobuf"}) if err != nil { t.Fatal(err) } @@ -122,6 +119,7 @@ func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) { } } +// protoBytes encodes a collector response for the HTTP test server. func protoBytes(t *testing.T, v proto.Message) []byte { t.Helper() data, err := proto.Marshal(v) @@ -131,11 +129,23 @@ func protoBytes(t *testing.T, v proto.Message) []byte { return data } +// TestUnknownOTLPProtocolFails rejects unsupported wire formats during initialization. func TestUnknownOTLPProtocolFails(t *testing.T) { - t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/json") - t.Setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "") - _, err := newTraceExporter(context.Background()) + _, err := newTraceExporter(context.Background(), "http/json") if err == nil || !strings.Contains(err.Error(), "unsupported OTLP") { t.Fatalf("expected protocol error, got %v", err) } } + +// TestDefaultOTLPProtocolGRPC keeps empty and explicit gRPC configuration working. +func TestDefaultOTLPProtocolGRPC(t *testing.T) { + for _, protocol := range []string{"", "grpc"} { + exporter, err := newTraceExporter(context.Background(), protocol) + if err != nil { + t.Fatalf("newTraceExporter(%q): %v", protocol, err) + } + if err := exporter.Shutdown(context.Background()); err != nil { + t.Errorf("exporter.Shutdown(%q): %v", protocol, err) + } + } +}