Skip to content

feat(go): update telemetry #3264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 35 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e69efc6
feat(go): Update googlecloud telemetry plugin
huangjeff5 Jul 10, 2025
37bc233
Merge branch 'main' into jh-go-telemetry
huangjeff5 Jul 11, 2025
f2f66cc
Add googlecloud logging integration
huangjeff5 Jul 11, 2025
54a9f93
Update googlecloud plugin comments
huangjeff5 Jul 14, 2025
717206b
Updated googlecloud plugin
huangjeff5 Jul 15, 2025
a74096a
fix snippet
huangjeff5 Jul 15, 2025
4ef33ed
better naming
huangjeff5 Jul 15, 2025
0edc7e8
adding googlecloud tests
huangjeff5 Jul 18, 2025
f466bb2
tests and refactor
huangjeff5 Jul 22, 2025
1b6feee
defaults test
huangjeff5 Jul 22, 2025
b913133
Update RunInNewSpan method
huangjeff5 Jul 22, 2025
67542c6
Updates to go core to emit key metadata
huangjeff5 Jul 23, 2025
e7d87bf
fix
huangjeff5 Jul 23, 2025
8c2b245
fmt
huangjeff5 Jul 23, 2025
b90a809
fix defaults
huangjeff5 Jul 23, 2025
df00631
Add more tests, calculate genkit-level metrics, hook into global trac…
huangjeff5 Aug 6, 2025
97f6e9b
Update utils.go
huangjeff5 Aug 6, 2025
9ebf046
Add firebase interface + googlecloud interface
huangjeff5 Aug 7, 2025
a315320
Merge branch 'jh-go-telemetry' of https://github.com/firebase/genkit …
huangjeff5 Aug 7, 2025
0f83108
fix test
huangjeff5 Aug 7, 2025
8d1cbc3
update tests
huangjeff5 Aug 7, 2025
e0062a3
Update generate.go
huangjeff5 Aug 7, 2025
98945e5
Update document.go
huangjeff5 Aug 7, 2025
102c435
Update generate.go
huangjeff5 Aug 7, 2025
3d44916
Delete go/samples/telemetry-test/GUIDE.md
huangjeff5 Aug 7, 2025
8f4a6df
format
huangjeff5 Aug 7, 2025
d38a55f
Merge branch 'jh-go-telemetry' of https://github.com/firebase/genkit …
huangjeff5 Aug 7, 2025
08814f7
Update simple_joke.go
huangjeff5 Aug 7, 2025
e03835c
Update README.md
huangjeff5 Aug 7, 2025
a58cdb9
Remove extra logging, use remove firebase telemetry env variable
huangjeff5 Aug 7, 2025
46799db
refactor to simplify setup
huangjeff5 Aug 8, 2025
70c42be
Fix tests
huangjeff5 Aug 8, 2025
50aecfd
fix bugs from testing
huangjeff5 Aug 12, 2025
04b4a8e
clean up comments
huangjeff5 Aug 12, 2025
c1f849e
enhancements to fix aim
huangjeff5 Aug 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions go/ai/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ func TestGenerateAction(t *testing.T) {
t.Errorf("chunks mismatch (-want +got):\n%s", diff)
}

if diff := cmp.Diff(tc.ExpectResponse, resp, cmp.Options{cmpopts.EquateEmpty()}); diff != "" {
if diff := cmp.Diff(tc.ExpectResponse, resp, cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(ModelResponse{}, "LatencyMs"),
cmpopts.IgnoreFields(GenerationUsage{}, "InputCharacters", "OutputCharacters"),
}); diff != "" {
t.Errorf("response mismatch (-want +got):\n%s", diff)
}
} else {
Expand All @@ -151,7 +155,11 @@ func TestGenerateAction(t *testing.T) {
t.Fatalf("action failed: %v", err)
}

if diff := cmp.Diff(tc.ExpectResponse, resp, cmp.Options{cmpopts.EquateEmpty()}); diff != "" {
if diff := cmp.Diff(tc.ExpectResponse, resp, cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(ModelResponse{}, "LatencyMs"),
cmpopts.IgnoreFields(GenerationUsage{}, "InputCharacters", "OutputCharacters"),
}); diff != "" {
t.Errorf("response mismatch (-want +got):\n%s", diff)
}
}
Expand Down
46 changes: 46 additions & 0 deletions go/ai/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package ai
import (
"encoding/json"
"fmt"
"strings"
)

// A Document is a piece of data that can be embedded, indexed, or retrieved.
Expand Down Expand Up @@ -153,6 +154,33 @@ func (p *Part) IsReasoning() bool {
return p.Kind == PartReasoning
}

// IsImage reports whether the [Part] contains an image.
func (p *Part) IsImage() bool {
if p == nil || !p.IsMedia() {
return false
}
return IsImageContentType(p.ContentType) ||
strings.HasPrefix(p.Text, "data:image/")
}

// IsVideo reports whether the [Part] contains a video.
func (p *Part) IsVideo() bool {
if p == nil || !p.IsMedia() {
return false
}
return IsVideoContentType(p.ContentType) ||
strings.HasPrefix(p.Text, "data:video/")
}

// IsAudio reports whether the [Part] contains an audio file.
func (p *Part) IsAudio() bool {
if p == nil || !p.IsMedia() {
return false
}
return IsAudioContentType(p.ContentType) ||
strings.HasPrefix(p.Text, "data:audio/")
}

// MarshalJSON is called by the JSON marshaler to write out a Part.
func (p *Part) MarshalJSON() ([]byte, error) {
// This is not handled by the schema generator because
Expand Down Expand Up @@ -291,3 +319,21 @@ func DocumentFromText(text string, metadata map[string]any) *Document {
Metadata: metadata,
}
}

// IsImageContentType checks if the content type represents an image.
func IsImageContentType(contentType string) bool {
return strings.HasPrefix(contentType, "image/") ||
strings.HasPrefix(contentType, "data:image/")
}

// IsVideoContentType checks if the content type represents a video.
func IsVideoContentType(contentType string) bool {
return strings.HasPrefix(contentType, "video/") ||
strings.HasPrefix(contentType, "data:video/")
}

// IsAudioContentType checks if the content type represents an audio file.
func IsAudioContentType(contentType string) bool {
return strings.HasPrefix(contentType, "audio/") ||
strings.HasPrefix(contentType, "data:audio/")
}
7 changes: 6 additions & 1 deletion go/ai/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ func DefineEvaluator(r *registry.Registry, provider, name string, options *Evalu
if datapoint.TestCaseId == "" {
datapoint.TestCaseId = uuid.New().String()
}
_, err := tracing.RunInNewSpan(ctx, r.TracingState(), fmt.Sprintf("TestCase %s", datapoint.TestCaseId), "evaluator", false, datapoint,
_, err := tracing.RunInNewSpan(ctx, r.TracingState(), &tracing.SpanMetadata{
Name: fmt.Sprintf("TestCase %s", datapoint.TestCaseId),
Type: "action",
Subtype: "evaluator", // Evaluator action
IsRoot: false,
}, datapoint,
func(ctx context.Context, input *Example) (*EvaluatorCallbackResponse, error) {
traceId := trace.SpanContextFromContext(ctx).TraceID().String()
spanId := trace.SpanContextFromContext(ctx).SpanID().String()
Expand Down
236 changes: 230 additions & 6 deletions go/ai/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import (
"fmt"
"slices"
"strings"
"time"

"github.com/firebase/genkit/go/core"
"github.com/firebase/genkit/go/core/logger"
"github.com/firebase/genkit/go/core/tracing"
"github.com/firebase/genkit/go/internal/base"
"github.com/firebase/genkit/go/internal/registry"
)
Expand Down Expand Up @@ -106,10 +106,19 @@ func DefineGenerateAction(ctx context.Context, r *registry.Registry) *generateAc
"err", err)
}()

return tracing.RunInNewSpan(ctx, r.TracingState(), "generate", "util", false, actionOpts,
func(ctx context.Context, actionOpts *GenerateActionOptions) (*ModelResponse, error) {
return GenerateWithRequest(ctx, r, actionOpts, nil, cb)
})
// Get registry and middleware from context (set by ai.Generate)
registryToUse := r // fallback to original registry
if ctxRegistry := ctx.Value("genkit:registry"); ctxRegistry != nil {
registryToUse = ctxRegistry.(*registry.Registry)
}

var middleware []ModelMiddleware
if ctxMiddleware := ctx.Value("genkit:middleware"); ctxMiddleware != nil {
middleware = ctxMiddleware.([]ModelMiddleware)
}

// Use registry and middleware from context (includes dynamic tools)
return GenerateWithRequest(ctx, registryToUse, actionOpts, middleware, cb)
}))
}

Expand Down Expand Up @@ -154,6 +163,7 @@ func DefineModel(r *registry.Registry, provider, name string, info *ModelInfo, f
simulateSystemPrompt(info, nil),
augmentWithContext(info, nil),
validateSupport(name, info),
addAutomaticTelemetry(), // Add automatic timing and character counting
}
fn = core.ChainMiddleware(mws...)(fn)

Expand Down Expand Up @@ -477,7 +487,18 @@ func Generate(ctx context.Context, r *registry.Registry, opts ...GenerateOption)
}
}

return GenerateWithRequest(ctx, r, actionOpts, genOpts.Middleware, genOpts.Stream)
// Call the registered generate action instead of GenerateWithRequest directly
// This ensures proper span hierarchy: flow -> generate -> model
generateAction := core.ResolveActionFor[*GenerateActionOptions, *ModelResponse, *ModelResponseChunk](r, core.ActionTypeUtil, "", "generate")
if generateAction == nil {
return nil, core.NewError(core.INTERNAL, "generate action not found")
}

// Pass the modified registry and middleware through context for the action to use
ctxWithData := context.WithValue(ctx, "genkit:registry", r)
ctxWithData = context.WithValue(ctxWithData, "genkit:middleware", genOpts.Middleware)

return generateAction.Run(ctxWithData, actionOpts, genOpts.Stream)
}

// GenerateText run generate request for this model. Returns generated text only.
Expand Down Expand Up @@ -1053,3 +1074,206 @@ func handleResumeOption(ctx context.Context, r *registry.Registry, genOpts *Gene
toolMessage: toolMessage,
}, nil
}

// addAutomaticTelemetry creates middleware that automatically measures latency and calculates character and media counts.
func addAutomaticTelemetry() ModelMiddleware {
return func(fn ModelFunc) ModelFunc {
return func(ctx context.Context, req *ModelRequest, cb ModelStreamCallback) (*ModelResponse, error) {
startTime := time.Now()

// Call the underlying model function
resp, err := fn(ctx, req, cb)
if err != nil {
return nil, err
}

// Calculate latency
latencyMs := float64(time.Since(startTime).Nanoseconds()) / 1e6
if resp.LatencyMs == 0 {
resp.LatencyMs = latencyMs
}

// Calculate character and media counts automatically if Usage is available
if resp.Usage != nil {
if resp.Usage.InputCharacters == 0 {
resp.Usage.InputCharacters = calculateInputCharacters(req)
}
if resp.Usage.OutputCharacters == 0 {
resp.Usage.OutputCharacters = calculateOutputCharacters(resp)
}
if resp.Usage.InputImages == 0 {
resp.Usage.InputImages = calculateInputImages(req)
}
if resp.Usage.OutputImages == 0 {
resp.Usage.OutputImages = calculateOutputImages(resp)
}
if resp.Usage.InputVideos == 0 {
resp.Usage.InputVideos = float64(calculateInputVideos(req))
}
if resp.Usage.OutputVideos == 0 {
resp.Usage.OutputVideos = float64(calculateOutputVideos(resp))
}
if resp.Usage.InputAudioFiles == 0 {
resp.Usage.InputAudioFiles = float64(calculateInputAudio(req))
}
if resp.Usage.OutputAudioFiles == 0 {
resp.Usage.OutputAudioFiles = float64(calculateOutputAudio(resp))
}
} else {
// Create GenerationUsage if it doesn't exist
resp.Usage = &GenerationUsage{
InputCharacters: calculateInputCharacters(req),
OutputCharacters: calculateOutputCharacters(resp),
InputImages: calculateInputImages(req),
OutputImages: calculateOutputImages(resp),
InputVideos: float64(calculateInputVideos(req)),
OutputVideos: float64(calculateOutputVideos(resp)),
InputAudioFiles: float64(calculateInputAudio(req)),
OutputAudioFiles: float64(calculateOutputAudio(resp)),
}
}

return resp, nil
}
}
}

// calculateInputCharacters counts the total characters in the input request.
func calculateInputCharacters(req *ModelRequest) int {
if req == nil {
return 0
}

totalChars := 0
for _, msg := range req.Messages {
if msg == nil {
continue
}
for _, part := range msg.Content {
if part != nil && part.Text != "" {
totalChars += len(part.Text)
}
}
}
return totalChars
}

// calculateOutputCharacters counts the total characters in the output response.
func calculateOutputCharacters(resp *ModelResponse) int {
if resp == nil || resp.Message == nil {
return 0
}

totalChars := 0
for _, part := range resp.Message.Content {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

factor in all parts?

if part != nil && part.Text != "" {
totalChars += len(part.Text)
}
}
return totalChars
}

// calculateInputImages counts the total number of images in the input request.
func calculateInputImages(req *ModelRequest) int {
if req == nil {
return 0
}

imageCount := 0
for _, msg := range req.Messages {
if msg == nil {
continue
}
for _, part := range msg.Content {
if part != nil && part.IsImage() {
imageCount++
}
}
}
return imageCount
}

// calculateOutputImages counts the total number of images in the output response.
func calculateOutputImages(resp *ModelResponse) int {
if resp == nil || resp.Message == nil {
return 0
}

imageCount := 0
for _, part := range resp.Message.Content {
if part != nil && part.IsImage() {
imageCount++
}
}
return imageCount
}

// calculateInputVideos counts the total number of videos in the input request.
func calculateInputVideos(req *ModelRequest) int {
if req == nil {
return 0
}

videoCount := 0
for _, msg := range req.Messages {
if msg == nil {
continue
}
for _, part := range msg.Content {
if part != nil && part.IsVideo() {
videoCount++
}
}
}
return videoCount
}

// calculateOutputVideos counts the total number of videos in the output response.
func calculateOutputVideos(resp *ModelResponse) int {
if resp == nil || resp.Message == nil {
return 0
}

videoCount := 0
for _, part := range resp.Message.Content {
if part != nil && part.IsVideo() {
videoCount++
}
}
return videoCount
}

// calculateInputAudio counts the total number of audio files in the input request.
func calculateInputAudio(req *ModelRequest) int {
if req == nil {
return 0
}

audioCount := 0
for _, msg := range req.Messages {
if msg == nil {
continue
}
for _, part := range msg.Content {
if part != nil && part.IsAudio() {
audioCount++
}
}
}
return audioCount
}

// calculateOutputAudio counts the total number of audio files in the output response.
func calculateOutputAudio(resp *ModelResponse) int {
if resp == nil || resp.Message == nil {
return 0
}

audioCount := 0
for _, part := range resp.Message.Content {
if part != nil && part.IsAudio() {
audioCount++
}
}
return audioCount
}
2 changes: 2 additions & 0 deletions go/ai/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ var r, _ = registry.New()
func init() {
// Set up default formats
ConfigureFormats(r)
// Register the generate action that Generate() function expects
DefineGenerateAction(context.Background(), r)
}

// echoModel attributes
Expand Down
Loading
Loading