mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-08-14 11:14:17 +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
128 lines
3.6 KiB
Go
128 lines
3.6 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
func getTracer() trace.Tracer {
|
|
return otel.GetTracerProvider().Tracer(serviceName)
|
|
}
|
|
|
|
// StartSpan creates a new span from the given context. When telemetry is not enabled,
|
|
// it returns a no-op span so callers can safely defer .End().
|
|
func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
|
if !IsEnabled() {
|
|
return ctx, trace.SpanFromContext(ctx)
|
|
}
|
|
return getTracer().Start(ctx, name, opts...)
|
|
}
|
|
|
|
// TraceIDFromContext returns the hex-encoded trace ID of the span carried by
|
|
// ctx, or "" if ctx carries no valid span (e.g. telemetry is disabled).
|
|
func TraceIDFromContext(ctx context.Context) string {
|
|
sc := trace.SpanContextFromContext(ctx)
|
|
if !sc.TraceID().IsValid() {
|
|
return ""
|
|
}
|
|
return sc.TraceID().String()
|
|
}
|
|
|
|
// EndSpan ends the span and records error status if present.
|
|
func EndSpan(span trace.Span, err error) {
|
|
if err != nil {
|
|
span.SetStatus(codes.Error, err.Error())
|
|
span.RecordError(err)
|
|
}
|
|
span.End()
|
|
}
|
|
|
|
// SetAttr sets a single attribute on a span.
|
|
func SetAttr(span trace.Span, key string, value interface{}) {
|
|
if span == nil {
|
|
return
|
|
}
|
|
switch v := value.(type) {
|
|
case string:
|
|
span.SetAttributes(attribute.String(key, v))
|
|
case int:
|
|
span.SetAttributes(attribute.Int64(key, int64(v)))
|
|
case int64:
|
|
span.SetAttributes(attribute.Int64(key, v))
|
|
case bool:
|
|
span.SetAttributes(attribute.Bool(key, v))
|
|
case float64:
|
|
span.SetAttributes(attribute.Float64(key, v))
|
|
default:
|
|
span.SetAttributes(attribute.String(key, ""))
|
|
}
|
|
}
|
|
|
|
// StartToolSpan creates a span for a tool execution with standard attributes.
|
|
func StartToolSpan(ctx context.Context, toolName string) (context.Context, trace.Span) {
|
|
return StartSpan(ctx, "tool.execute."+toolName,
|
|
trace.WithAttributes(attribute.String("tool.name", toolName)))
|
|
}
|
|
|
|
// RecordToolResult sets the outcome of a tool execution on the span.
|
|
func RecordToolResult(span trace.Span, toolName string, durationMs int64, err error) {
|
|
if span == nil {
|
|
return
|
|
}
|
|
SetAttr(span, "tool.duration_ms", durationMs)
|
|
if err != nil {
|
|
SetAttr(span, "tool.status", "error")
|
|
SetAttr(span, "tool.error", err.Error())
|
|
span.SetStatus(codes.Error, err.Error())
|
|
span.RecordError(err)
|
|
} else {
|
|
SetAttr(span, "tool.status", "ok")
|
|
}
|
|
}
|
|
|
|
// StartLLMSpan creates a span for an LLM request with standard attributes.
|
|
func StartLLMSpan(ctx context.Context, model string) (context.Context, trace.Span) {
|
|
return StartSpan(ctx, "llm.request",
|
|
trace.WithAttributes(attribute.String("llm.model", model)))
|
|
}
|
|
|
|
// RecordLLMResult sets the outcome of an LLM request on the span.
|
|
func RecordLLMResult(span trace.Span, duration time.Duration, totalTokens int64, err error) {
|
|
if span == nil {
|
|
return
|
|
}
|
|
SetAttr(span, "llm.duration_ms", duration.Milliseconds())
|
|
SetAttr(span, "llm.total_tokens", totalTokens)
|
|
if err != nil {
|
|
SetAttr(span, "llm.status", "error")
|
|
SetAttr(span, "llm.error", err.Error())
|
|
span.SetStatus(codes.Error, err.Error())
|
|
span.RecordError(err)
|
|
} else {
|
|
SetAttr(span, "llm.status", "ok")
|
|
}
|
|
}
|
|
|
|
// AnyToAttr converts an arbitrary value to an OTel attribute.KeyValue.
|
|
func AnyToAttr(k string, v interface{}) attribute.KeyValue {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return attribute.String(k, val)
|
|
case int:
|
|
return attribute.Int64(k, int64(val))
|
|
case int64:
|
|
return attribute.Int64(k, val)
|
|
case bool:
|
|
return attribute.Bool(k, val)
|
|
case float64:
|
|
return attribute.Float64(k, val)
|
|
default:
|
|
return attribute.String(k, fmt.Sprintf("%v", v))
|
|
}
|
|
}
|