mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
* feat(telemetry): add OTLP HTTP exporter and print TraceID - Add HTTP/protobuf exporter support alongside existing gRPC exporter - Route based on OTEL_EXPORTER_OTLP_PROTOCOL config (http/protobuf vs grpc) - Print TraceID to stderr when telemetry is enabled for easier correlation - Add corresponding unit tests * feat(telemetry): add span coverage for LLM calls, tool execution, plan/filter phases - Add StartLLMSpan / RecordLLMResult helpers (span.go), symmetric with existing StartToolSpan / RecordToolResult - Wrap LLM completion calls in llmloop.RunPerFile with llm.request spans - Wrap all three tool execution paths in executeToolCall with tool.execute.* spans (dynamic tools, code_comment sync/async, other tools) - Add plan.execute span around executePlanPhase - Add main.loop span around RunPerFile call in executeSubtask - Add review_filter.execute span around executeReviewFilter, with comments.before / comments.filtered attributes - Record llm.error attribute on LLM failures for diagnosability - Record review.repo / review.from / review.to / review.model on the top-level review.run span - Metrics (RecordLLMRequest / RecordToolCall) are preserved alongside the new spans — they serve different purposes (aggregate dashboards vs per-run diagnosis) Verified end-to-end against Sunfire (OTLP HTTP gateway): full span tree observed for review.run -> subtask.execute -> plan.execute/main.loop/ review_filter.execute -> llm.request/tool.execute.* * fix(telemetry): address CR findings — span error handling, async span lifecycle, protocol robustness - Add span.RecordError(err) to RecordLLMResult and RecordToolResult for consistency with EndSpan - Use OTel standard pattern (span.SetStatus + span.RecordError) in error paths of review.run, plan.execute, main.loop, review_filter.execute - Move async code_comment span end into pool.Submit callback so span duration reflects actual execution time - Unify time.Since(startTime) in code_comment error path to a single dur - Remove http/json from supported OTLP protocols (not actually implemented) - Add stderr warning when unknown OTLP protocol falls back to gRPC * feat(telemetry): include trace_id in JSON output, restrict stderr to text format - Add trace_id as top-level field in jsonOutput struct (omitempty) - JSON format: trace_id in structured response for programmatic extraction - Text format: TraceID printed to stderr for human debugging - Telemetry disabled: trace_id field omitted entirely * fix: address PR review findings - loop.go: wrap async span lifecycle in defer to prevent leak on panic - exporter.go: update parseOTLPEndpoint comment to reflect gRPC+HTTP usage - scan_cmd.go: align traceID extraction and OTel error handling with review_cmd - output.go/shared.go: propagate traceID to outputJSONNoFiles for consistency - agent.go: move comments.filtered attribute before early return so 0 is distinguishable from not-executed * feat(telemetry): address PR review — http/json routing, LLM span coverage, trace_id tests - Route http/json to HTTP exporter (Go OTel SDK HTTP transport only supports protobuf serialization; users need HTTP transport, not JSON encoding) - Add llm.request spans to executePlanPhase, executeReviewFilter, and ReLocateComment with Usage nil-safety consistent with loop.go - Add trace_id assertions to output helper tests and emitRunResult end-to-end tests using real TracerProvider * docs: add OTLP protocol selection and endpoint format to telemetry section Sync across all 5 README language versions (en, zh-CN, ja-JP, ko-KR, ru-RU). * fix: unify time.Since in async code_comment defer to single dur variable
167 lines
6 KiB
Go
167 lines
6 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
"go.opentelemetry.io/otel/sdk/resource"
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
|
|
otlpmetricgrpc "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
|
otlpmetrichttp "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
|
otlptracegrpc "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
|
otlptracehttp "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
stdoutmetric "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
|
|
stdouttrace "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
|
)
|
|
|
|
// newStdoutTraceExporter creates a console trace exporter with pretty-print output.
|
|
func newStdoutTraceExporter() (*stdouttrace.Exporter, error) {
|
|
return stdouttrace.New(stdouttrace.WithPrettyPrint())
|
|
}
|
|
|
|
// newStdoutMetricExporter creates a console metric exporter with pretty-print output.
|
|
func newStdoutMetricExporter() (sdkmetric.Exporter, error) {
|
|
return stdoutmetric.New(stdoutmetric.WithPrettyPrint())
|
|
}
|
|
|
|
// parseOTLPEndpoint strips a http:// or https:// scheme from the endpoint and
|
|
// reports whether the connection should be plaintext (insecure).
|
|
// Scheme matching is case-insensitive per RFC 3986. A bare host:port (no
|
|
// scheme) is left unchanged and defaults to TLS. Any trailing slash left
|
|
// over from a URL-style endpoint (e.g. "http://localhost:4317/") is
|
|
// trimmed, since both gRPC and HTTP exporters' WithEndpoint expects a bare
|
|
// host:port with no path.
|
|
func parseOTLPEndpoint(endpoint string) (addr string, insecure bool) {
|
|
switch {
|
|
case len(endpoint) >= 7 && strings.EqualFold(endpoint[:7], "http://"):
|
|
return strings.TrimRight(endpoint[7:], "/"), true
|
|
case len(endpoint) >= 8 && strings.EqualFold(endpoint[:8], "https://"):
|
|
return strings.TrimRight(endpoint[8:], "/"), false
|
|
default:
|
|
return endpoint, false
|
|
}
|
|
}
|
|
|
|
// initOTLPProviders dispatches to the gRPC or HTTP exporter based on cfg.OTLPProtocol.
|
|
func initOTLPProviders(ctx context.Context, res *resource.Resource, cfg Config) {
|
|
switch cfg.OTLPProtocol {
|
|
case "http/protobuf", "http/json":
|
|
initOTLPHTTPProviders(ctx, res, cfg)
|
|
case "", "grpc":
|
|
initOTLPGRPCProviders(ctx, res, cfg)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "[ocr] WARNING: unsupported OTLP protocol %q, falling back to gRPC\n", cfg.OTLPProtocol)
|
|
initOTLPGRPCProviders(ctx, res, cfg)
|
|
}
|
|
}
|
|
|
|
// initOTLPGRPCProviders sets up OTLP gRPC exporters for traces and metrics.
|
|
func initOTLPGRPCProviders(ctx context.Context, res *resource.Resource, cfg Config) {
|
|
addr, insecure := parseOTLPEndpoint(cfg.OTLPEndpoint)
|
|
|
|
traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(addr)}
|
|
if insecure {
|
|
traceOpts = append(traceOpts, otlptracegrpc.WithInsecure())
|
|
}
|
|
traceExp, err := otlptracegrpc.New(ctx, traceOpts...)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to create OTLP trace exporter: %v\n", err)
|
|
return
|
|
}
|
|
|
|
tp := sdktrace.NewTracerProvider(
|
|
sdktrace.WithBatcher(traceExp),
|
|
sdktrace.WithResource(res),
|
|
)
|
|
tracerProvider = tp
|
|
shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error { return tp.Shutdown(ctx) })
|
|
|
|
metricOpts := []otlpmetricgrpc.Option{otlpmetricgrpc.WithEndpoint(addr)}
|
|
if insecure {
|
|
metricOpts = append(metricOpts, otlpmetricgrpc.WithInsecure())
|
|
}
|
|
metricExp, err := otlpmetricgrpc.New(ctx, metricOpts...)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to create OTLP metric exporter: %v\n", err)
|
|
return
|
|
}
|
|
|
|
mp := sdkmetric.NewMeterProvider(
|
|
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)),
|
|
sdkmetric.WithResource(res),
|
|
)
|
|
meterProvider = mp
|
|
shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error { return mp.Shutdown(ctx) })
|
|
}
|
|
|
|
// initOTLPHTTPProviders sets up OTLP HTTP exporters for traces and metrics.
|
|
func initOTLPHTTPProviders(ctx context.Context, res *resource.Resource, cfg Config) {
|
|
addr, insecure := parseOTLPEndpoint(cfg.OTLPEndpoint)
|
|
|
|
traceOpts := []otlptracehttp.Option{otlptracehttp.WithEndpoint(addr)}
|
|
if insecure {
|
|
traceOpts = append(traceOpts, otlptracehttp.WithInsecure())
|
|
}
|
|
traceExp, err := otlptracehttp.New(ctx, traceOpts...)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to create OTLP HTTP trace exporter: %v\n", err)
|
|
return
|
|
}
|
|
|
|
tp := sdktrace.NewTracerProvider(
|
|
sdktrace.WithBatcher(traceExp),
|
|
sdktrace.WithResource(res),
|
|
)
|
|
tracerProvider = tp
|
|
shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error { return tp.Shutdown(ctx) })
|
|
|
|
metricOpts := []otlpmetrichttp.Option{otlpmetrichttp.WithEndpoint(addr)}
|
|
if insecure {
|
|
metricOpts = append(metricOpts, otlpmetrichttp.WithInsecure())
|
|
}
|
|
metricExp, err := otlpmetrichttp.New(ctx, metricOpts...)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to create OTLP HTTP metric exporter: %v\n", err)
|
|
return
|
|
}
|
|
|
|
mp := sdkmetric.NewMeterProvider(
|
|
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)),
|
|
sdkmetric.WithResource(res),
|
|
)
|
|
meterProvider = mp
|
|
shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error { return mp.Shutdown(ctx) })
|
|
}
|
|
|
|
// initConsoleProviders sets up stdout exporters for traces and metrics.
|
|
func initConsoleProviders(res *resource.Resource) {
|
|
traceExp, err := newStdoutTraceExporter()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to create console trace exporter: %v\n", err)
|
|
return
|
|
}
|
|
|
|
tp := sdktrace.NewTracerProvider(
|
|
sdktrace.WithBatcher(traceExp),
|
|
sdktrace.WithResource(res),
|
|
)
|
|
tracerProvider = tp
|
|
shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error { return tp.Shutdown(ctx) })
|
|
|
|
metricExp, err := newStdoutMetricExporter()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to create console metric exporter: %v\n", err)
|
|
return
|
|
}
|
|
|
|
mp := sdkmetric.NewMeterProvider(
|
|
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)),
|
|
sdkmetric.WithResource(res),
|
|
)
|
|
meterProvider = mp
|
|
shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) error { return mp.Shutdown(ctx) })
|
|
}
|