mirror of
https://github.com/google/adk-go.git
synced 2026-10-28 16:44:38 +08:00
* feat(telemetry)!: default to experimental GenAI semconv, with ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN=otel_semconv_1_36 rollback ADK now emits the experimental OpenTelemetry GenAI semantic conventions by default, following adk-python's schema v2: one gen_ai.client.inference.operation.details event per model call, gen_ai.tool.call.arguments/result on execute_tool gated on span content capture, and an entrypoint invoke_workflow span. Message content is recorded as structured attribute values on spans as well as events, where adk-python records JSON strings on spans. ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN=otel_semconv_1_36 restores the previous format, and otel_semconv_1_44 names the default. Values are case-insensitive, and adk-python's 1 and 2 are accepted for them. No otel_semconv_1_36 golden changes in this commit. A model call and a runner invocation are traced only through InstrumentGenerateContent and InstrumentInvocation, so the generate_content span and its log records cannot drift apart. Closes #1634 * fix(telemetry): address review comments on docs and number conversion * fix(telemetry): describe the request as the model left it in the inference event * refactor(telemetry): record request messages on the span and the event through operationDetails * fix(telemetry): report dropped span attributes through the OpenTelemetry error handler * fix(telemetry): add error.type to workflow node and summarizer-panic spans * feat(telemetry): report a streamed inference as adk-python does in the default schema * test(telemetry): have MockModel report STOP on unstreamed responses, as a real model does
73 lines
2.5 KiB
Go
73 lines
2.5 KiB
Go
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package workflow
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
"go.opentelemetry.io/otel/trace/noop"
|
|
|
|
"google.golang.org/adk/v2/agent"
|
|
"google.golang.org/adk/v2/internal/telemetry"
|
|
)
|
|
|
|
// nodeSpan is the single invoke_node span shared by both schedulers and
|
|
// the parallel worker.
|
|
type nodeSpan struct {
|
|
span trace.Span
|
|
}
|
|
|
|
// startNodeSpan begins an invoke_node span for n and returns a context
|
|
// carrying it. It is a noop span for Start and for nodes that emit their
|
|
// own span (EmitsOwnSpan, e.g. an agent node's invoke_agent), so callers
|
|
// can unconditionally defer end / call recordError.
|
|
func startNodeSpan(ctx agent.Context, n Node) (nodeSpan, agent.Context) {
|
|
if n == Start || n.Config().EmitsOwnSpan {
|
|
return nodeSpan{span: noop.Span{}}, ctx
|
|
}
|
|
spanCtx, span := telemetry.StartNodeSpan(ctx, ctx, telemetry.OperationNode{Node: n})
|
|
return nodeSpan{span: span}, ctx.WithAgentContext(spanCtx)
|
|
}
|
|
|
|
// recordError marks the span failed with recordErr, unless the outcome is
|
|
// expected control flow (see isNodeControlFlow). classifyErr picks the
|
|
// error to classify — pass the unwrapped cause when recordErr hides the
|
|
// sentinel (the dynamic scheduler wraps with "%w: %v", dropping
|
|
// context.Canceled); nil classifies on recordErr. Does not end the span.
|
|
func (s nodeSpan) recordError(recordErr, classifyErr error) {
|
|
if recordErr == nil {
|
|
return
|
|
}
|
|
if classifyErr == nil {
|
|
classifyErr = recordErr
|
|
}
|
|
if isNodeControlFlow(classifyErr) {
|
|
return
|
|
}
|
|
telemetry.RecordError(s.span, recordErr)
|
|
}
|
|
|
|
func (s nodeSpan) end() { s.span.End() }
|
|
|
|
// isNodeControlFlow reports whether err is expected control flow
|
|
// (cancellation or a HITL / wait-for-output pause), not a node failure.
|
|
// ErrNodeWaitingForOutput wraps ErrNodeInterrupted, so one check covers
|
|
// both pauses.
|
|
func isNodeControlFlow(err error) bool {
|
|
return errors.Is(err, context.Canceled) || errors.Is(err, ErrNodeInterrupted)
|
|
}
|