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
103 lines
3 KiB
Go
103 lines
3 KiB
Go
package diff
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/open-code-review/open-code-review/internal/config/template"
|
|
"github.com/open-code-review/open-code-review/internal/llm"
|
|
"github.com/open-code-review/open-code-review/internal/model"
|
|
"github.com/open-code-review/open-code-review/internal/stdout"
|
|
"github.com/open-code-review/open-code-review/internal/telemetry"
|
|
)
|
|
|
|
// ReLocateComment calls the LLM to regenerate a precise existing_code snippet
|
|
// when text-based matching fails, then retries ResolveComment with the new snippet.
|
|
// Returns (success, response, requestMessages) so the caller can record session
|
|
// history and track token usage. Response and messages are nil on early exits.
|
|
func ReLocateComment(
|
|
ctx context.Context,
|
|
cm *model.LlmComment,
|
|
d *model.Diff,
|
|
client llm.LLMClient,
|
|
task *template.LlmConversation,
|
|
modelName string,
|
|
maxTokens int,
|
|
) (bool, *llm.ChatResponse, []llm.Message) {
|
|
if task == nil || len(task.Messages) == 0 {
|
|
return false, nil, nil
|
|
}
|
|
|
|
if task.Timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, time.Duration(task.Timeout)*time.Second)
|
|
defer cancel()
|
|
}
|
|
|
|
messages := make([]llm.Message, 0, len(task.Messages))
|
|
for _, m := range task.Messages {
|
|
content := m.Content
|
|
content = strings.ReplaceAll(content, "{diff}", d.Diff)
|
|
content = strings.ReplaceAll(content, "{existing_code}", cm.ExistingCode)
|
|
content = strings.ReplaceAll(content, "{suggestion_content}", cm.Content)
|
|
messages = append(messages, llm.NewTextMessage(m.Role, content))
|
|
}
|
|
|
|
startTime := time.Now()
|
|
_, llmSpan := telemetry.StartLLMSpan(ctx, modelName)
|
|
resp, err := client.CompletionsWithCtx(ctx, llm.ChatRequest{
|
|
Model: modelName,
|
|
Messages: messages,
|
|
MaxTokens: maxTokens,
|
|
})
|
|
duration := time.Since(startTime)
|
|
if err != nil {
|
|
telemetry.RecordLLMResult(llmSpan, duration, 0, err)
|
|
llmSpan.End()
|
|
fmt.Fprintf(stdout.Writer(), "[ocr] Re-location LLM call failed for %s: %v\n", cm.Path, err)
|
|
return false, nil, messages
|
|
}
|
|
var totalTokens int64
|
|
if resp.Usage != nil {
|
|
totalTokens = resp.Usage.TotalTokens
|
|
}
|
|
telemetry.RecordLLMResult(llmSpan, duration, totalTokens, nil)
|
|
llmSpan.End()
|
|
|
|
code := extractCodeBlock(resp.Content())
|
|
if code == "" {
|
|
return false, resp, messages
|
|
}
|
|
|
|
original := cm.ExistingCode
|
|
cm.ExistingCode = code
|
|
if ResolveComment(cm, d) {
|
|
return true, resp, messages
|
|
}
|
|
cm.ExistingCode = original
|
|
return false, resp, messages
|
|
}
|
|
|
|
// extractCodeBlock extracts the content of the first fenced code block from text.
|
|
// Returns empty string if no code block is found.
|
|
func extractCodeBlock(text string) string {
|
|
text = strings.TrimSpace(text)
|
|
start := strings.Index(text, "```")
|
|
if start < 0 {
|
|
return ""
|
|
}
|
|
afterOpen := start + 3
|
|
// Skip optional language tag on the opening fence line.
|
|
if nl := strings.IndexByte(text[afterOpen:], '\n'); nl >= 0 {
|
|
afterOpen += nl + 1
|
|
} else {
|
|
return ""
|
|
}
|
|
end := strings.Index(text[afterOpen:], "```")
|
|
if end < 0 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(text[afterOpen : afterOpen+end])
|
|
}
|