fix(otel): resolve exporter protocol through startup configuration

This commit is contained in:
ryan
2026-11-09 10:23:47 +08:00
parent b281b65e78
commit acd63ff410
10 changed files with 65 additions and 24 deletions
+1
View File
@@ -95,6 +95,7 @@ worker:
# ─── OpenTelemetry Tracing ────────────────────────────────────────────────────── # ─── OpenTelemetry Tracing ──────────────────────────────────────────────────────
otel: otel:
export_protocol: "grpc" # grpc 或 http/protobuf;OTEL_EXPORTER_OTLP_TRACES_PROTOCOL 优先于通用 OTLP 协议变量
sampling_rate: 0.0 # Trace sampling rate (0.0 – 1.0) sampling_rate: 0.0 # Trace sampling rate (0.0 – 1.0)
tracer_name: "github.com/Rain-kl/OpenFlare" # Global tracer instrumentation name tracer_name: "github.com/Rain-kl/OpenFlare" # Global tracer instrumentation name
+1 -1
View File
@@ -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 秒行为。 - Cloudflare 指向分组支持选择自动 TTL 或手动填写 60~86400 秒,适用于节点绑定与自定义 A、AAAA、CNAME;灰云成员使用分组配置,橙云成员始终自动,旧分组保留原有 300 秒行为。
## [v3.5.9] - 2026-11-07 ## [v3.5.9] - 2026-11-07
+1 -1
View File
@@ -282,7 +282,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT: https://collector.example.com
OTEL_SAMPLING_RATE: "0.1" 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` 优先于通用请求头配置。应用不预设鉴权方案;实际请求头与凭据由部署环境提供,不要将真实凭据写入仓库。 需要鉴权或其他请求头时,使用可选的 `OTEL_EXPORTER_OTLP_HEADERS`,格式为 `key=value,key2=value2`,值按标准进行百分号编码,例如 `X-API-Key=example-key,X-Client-Name=example%20client`。链路专用的 `OTEL_EXPORTER_OTLP_TRACES_HEADERS` 优先于通用请求头配置。应用不预设鉴权方案;实际请求头与凭据由部署环境提供,不要将真实凭据写入仓库。
+4 -3
View File
@@ -33,9 +33,10 @@ var rootCmd = &cobra.Command{
Compress: config.Config.Log.Compress, Compress: config.Config.Log.Compress,
}) })
trace.Init(trace.Config{ trace.Init(trace.Config{
AppName: config.Config.App.AppName, AppName: config.Config.App.AppName,
SamplingRate: config.Config.Otel.SamplingRate, SamplingRate: config.Config.Otel.SamplingRate,
TracerName: config.Config.Otel.TracerName, TracerName: config.Config.Otel.TracerName,
ExportProtocol: config.Config.Otel.ExportProtocol,
}) })
}, },
PreRun: func(_ *cobra.Command, _ []string) { PreRun: func(_ *cobra.Command, _ []string) {
+8
View File
@@ -273,6 +273,14 @@ func applyEnvOverrides(c *configModel) {
c.Log.Output = envStr("LOG_OUTPUT", c.Log.Output) c.Log.Output = envStr("LOG_OUTPUT", c.Log.Output)
// ─── OTel ─── // ─── 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.SamplingRate = envFloat64("OTEL_SAMPLING_RATE", c.Otel.SamplingRate)
c.Otel.TracerName = envStr("OTEL_TRACER_NAME", c.Otel.TracerName) c.Otel.TracerName = envStr("OTEL_TRACER_NAME", c.Otel.TracerName)
+21
View File
@@ -47,3 +47,24 @@ func TestApplyClickHouseDefaultsEnablesWhenConfigured(t *testing.T) {
t.Fatalf("hosts default not applied: %v", cfg.ClickHouse.Hosts) 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)
}
})
}
}
+3 -2
View File
@@ -138,6 +138,7 @@ type QueueConfig struct {
// otelConfig OpenTelemetry 配置 // otelConfig OpenTelemetry 配置
type otelConfig struct { type otelConfig struct {
SamplingRate float64 `mapstructure:"sampling_rate"` ExportProtocol string `mapstructure:"export_protocol"`
TracerName string `mapstructure:"tracer_name"` SamplingRate float64 `mapstructure:"sampling_rate"`
TracerName string `mapstructure:"tracer_name"`
} }
+4 -3
View File
@@ -27,9 +27,10 @@ func init() {
// Config 链路追踪配置 // Config 链路追踪配置
type Config struct { type Config struct {
AppName string AppName string
SamplingRate float64 SamplingRate float64
TracerName string TracerName string
ExportProtocol string
} }
// Init 初始化 Tracer Provider 并关联全局 Tracer 实例 // Init 初始化 Tracer Provider 并关联全局 Tracer 实例
+4 -6
View File
@@ -17,6 +17,7 @@ import (
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
) )
// newTracerProvider combines SDK and application resources with a batched exporter.
func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) { func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
// 获取主机名和容器信息 // 获取主机名和容器信息
hostname, err := os.Hostname() hostname, err := os.Hostname()
@@ -40,7 +41,7 @@ func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
} }
// 初始化 Exporter // 初始化 Exporter
traceExporter, err := newTraceExporter(context.Background()) traceExporter, err := newTraceExporter(context.Background(), cfg.ExportProtocol)
if err != nil { if err != nil {
return nil, err 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. // newTraceExporter selects the OTLP wire transport without changing existing gRPC defaults.
// The SDK exporters read standard OTEL_EXPORTER_OTLP_* endpoint and header settings. // The SDK exporters read standard OTEL_EXPORTER_OTLP_* endpoint and header settings.
func newTraceExporter(ctx context.Context) (sdktrace.SpanExporter, error) { func newTraceExporter(ctx context.Context, protocol string) (sdktrace.SpanExporter, error) {
protocol := strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"))) protocol = strings.ToLower(strings.TrimSpace(protocol))
if protocol == "" {
protocol = strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL")))
}
switch protocol { switch protocol {
case "", "grpc": case "", "grpc":
return otlptracegrpc.New(ctx) return otlptracegrpc.New(ctx)
+18 -8
View File
@@ -15,6 +15,7 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
// TestOTLPHTTPTraceExportHeadersAndResources verifies wire payloads against a local collector.
func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) { func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) {
type received struct { type received struct {
headers http.Header headers http.Header
@@ -37,10 +38,8 @@ func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) {
_, _ = w.Write(responseBody) _, _ = w.Write(responseBody)
})) }))
defer server.Close() defer server.Close()
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "")
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "") t.Setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "")
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "") 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_ENDPOINT", server.URL)
t.Setenv("OTEL_EXPORTER_OTLP_HEADERS", "X-API-Key=test-key,X-Client-Name=test%20client") 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") 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", name: "trace_specific_overrides",
env: map[string]string{ 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_ENDPOINT": server.URL + "/custom/ingest",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS": "X-API-Key=trace-key,X-Client-Name=trace%20client", "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 { for key, value := range tt.env {
t.Setenv(key, value) 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 { if err != nil {
t.Fatal(err) 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 { func protoBytes(t *testing.T, v proto.Message) []byte {
t.Helper() t.Helper()
data, err := proto.Marshal(v) data, err := proto.Marshal(v)
@@ -131,11 +129,23 @@ func protoBytes(t *testing.T, v proto.Message) []byte {
return data return data
} }
// TestUnknownOTLPProtocolFails rejects unsupported wire formats during initialization.
func TestUnknownOTLPProtocolFails(t *testing.T) { func TestUnknownOTLPProtocolFails(t *testing.T) {
t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/json") _, err := newTraceExporter(context.Background(), "http/json")
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "")
_, err := newTraceExporter(context.Background())
if err == nil || !strings.Contains(err.Error(), "unsupported OTLP") { if err == nil || !strings.Contains(err.Error(), "unsupported OTLP") {
t.Fatalf("expected protocol error, got %v", err) 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)
}
}
}