mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-11-12 21:44:22 +08:00
feat(otel): support standard OTLP/HTTP trace export
This commit is contained in:
@@ -6,10 +6,13 @@ package trace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
@@ -37,7 +40,7 @@ func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
|
||||
}
|
||||
|
||||
// 初始化 Exporter
|
||||
traceExporter, err := otlptracegrpc.New(context.Background())
|
||||
traceExporter, err := newTraceExporter(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,3 +53,20 @@ func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
|
||||
)
|
||||
return tracerProvider, nil
|
||||
}
|
||||
|
||||
// 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")))
|
||||
}
|
||||
switch protocol {
|
||||
case "", "grpc":
|
||||
return otlptracegrpc.New(ctx)
|
||||
case "http/protobuf":
|
||||
return otlptracehttp.New(ctx)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported OTLP trace protocol %q", protocol)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package trace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
collecttrace "go.opentelemetry.io/proto/otlp/collector/trace/v1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestOTLPHTTPTraceExportHeadersAndResources(t *testing.T) {
|
||||
type received struct {
|
||||
headers http.Header
|
||||
data []byte
|
||||
path string
|
||||
method string
|
||||
}
|
||||
requests := make(chan received, 1)
|
||||
responseBody := protoBytes(t, &collecttrace.ExportTraceServiceResponse{})
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
requests <- received{headers: r.Header.Clone(), data: body, path: r.URL.Path, method: r.Method}
|
||||
w.Header().Set("Content-Type", "application/x-protobuf")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = 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")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
wantPath string
|
||||
wantHeaders map[string]string
|
||||
}{
|
||||
{
|
||||
name: "base_endpoint",
|
||||
wantPath: "/v1/traces",
|
||||
wantHeaders: map[string]string{"X-API-Key": "test-key", "X-Client-Name": "test client"},
|
||||
},
|
||||
{
|
||||
name: "base_endpoint_path",
|
||||
env: map[string]string{"OTEL_EXPORTER_OTLP_ENDPOINT": server.URL + "/collector"},
|
||||
wantPath: "/collector/v1/traces",
|
||||
wantHeaders: map[string]string{"X-API-Key": "test-key", "X-Client-Name": "test client"},
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
wantPath: "/custom/ingest",
|
||||
wantHeaders: map[string]string{"X-API-Key": "trace-key", "X-Client-Name": "trace client"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for key, value := range tt.env {
|
||||
t.Setenv(key, value)
|
||||
}
|
||||
provider, err := newTracerProvider(Config{AppName: "openflare-control-plane", SamplingRate: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = provider.Shutdown(context.Background()) }()
|
||||
_, span := provider.Tracer("test").Start(context.Background(), "canary")
|
||||
span.End()
|
||||
if err := provider.ForceFlush(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case req := <-requests:
|
||||
for key, want := range tt.wantHeaders {
|
||||
if got := req.headers.Get(key); got != want {
|
||||
t.Errorf("OTLP request header %q = %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
if req.method != "POST" || req.path != tt.wantPath {
|
||||
t.Errorf("OTLP request = %s %s, want POST %s", req.method, req.path, tt.wantPath)
|
||||
}
|
||||
var payload collecttrace.ExportTraceServiceRequest
|
||||
if err := proto.Unmarshal(req.data, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(payload.ResourceSpans) == 0 {
|
||||
t.Fatal("missing resource spans")
|
||||
}
|
||||
attrs := make(map[string]string)
|
||||
for _, a := range payload.ResourceSpans[0].Resource.Attributes {
|
||||
attrs[a.Key] = a.Value.GetStringValue()
|
||||
}
|
||||
for k, v := range map[string]string{"service.name": "openflare-control-plane", "service.namespace": "example", "deployment.environment.name": "test"} {
|
||||
if attrs[k] != v {
|
||||
t.Errorf("resource attribute %s mismatch: got %q", k, attrs[k])
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Fatal("OTLP HTTP request was not received")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func protoBytes(t *testing.T, v proto.Message) []byte {
|
||||
t.Helper()
|
||||
data, err := proto.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestUnknownOTLPProtocolFails(t *testing.T) {
|
||||
t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/json")
|
||||
t.Setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "")
|
||||
_, err := newTraceExporter(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported OTLP") {
|
||||
t.Fatalf("expected protocol error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user