Skip to content
Merged
Show file tree
Hide file tree
Changes from 80 commits
Commits
Show all changes
82 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
15bb9d1
Merge branch 'main' into jh-go-telemetry
huangjeff5 Aug 18, 2025
0b29c40
fix aim issues
huangjeff5 Aug 19, 2025
2ba69ff
format
huangjeff5 Aug 19, 2025
7796149
fix
huangjeff5 Aug 19, 2025
044a299
refactor
huangjeff5 Aug 19, 2025
6265b0b
Update tracing.go
huangjeff5 Aug 19, 2025
47d70c9
fix snippets
huangjeff5 Aug 19, 2025
c0de6ce
Merge branch 'jh-go-telemetry' of https://github.com/firebase/genkit …
huangjeff5 Aug 19, 2025
a21c04b
Update generate_test.go
huangjeff5 Aug 19, 2025
b09eb06
Update generate_test.go
huangjeff5 Aug 19, 2025
be2fa45
Update generate.go
huangjeff5 Aug 19, 2025
d7f15ab
Update tracing_test.go
huangjeff5 Aug 19, 2025
3dd90f6
polish
huangjeff5 Aug 19, 2025
f5225e6
Merge branch 'jh-go-telemetry' of https://github.com/firebase/genkit …
huangjeff5 Aug 19, 2025
2e3e2b7
refactor
huangjeff5 Aug 20, 2025
ff94930
Update action_test.go
huangjeff5 Aug 20, 2025
524f339
Update slog_handler_test.go
huangjeff5 Aug 20, 2025
a78c468
Update simple_joke.go
huangjeff5 Aug 20, 2025
a5f90c3
Merge branch 'main' into jh-go-telemetry
huangjeff5 Aug 21, 2025
87c7945
Merge branch 'jh-go-telemetry' of https://github.com/firebase/genkit …
huangjeff5 Aug 21, 2025
2b8f149
fix conflicts
huangjeff5 Aug 27, 2025
a8f890c
fix
huangjeff5 Aug 27, 2025
3103f3e
Merge branch 'main' into jh-go-telemetry
huangjeff5 Sep 2, 2025
6033240
Merge branch 'main' into jh-go-telemetry
huangjeff5 Sep 3, 2025
bf9ad28
address comments
huangjeff5 Sep 3, 2025
1306b14
Update go.yml
huangjeff5 Sep 3, 2025
d1e9eec
fmt
huangjeff5 Sep 3, 2025
051570d
Merge branch 'jh-go-telemetry' of https://github.com/firebase/genkit …
huangjeff5 Sep 3, 2025
ef81ead
update test
huangjeff5 Sep 3, 2025
db2a8a3
Refactor ToReflectionError function
huangjeff5 Sep 3, 2025
8c5c4bb
Simplify TracerProvider creation in tracing.go
huangjeff5 Sep 3, 2025
960422b
update
huangjeff5 Sep 3, 2025
b5cba8c
Merge branch 'jh-go-telemetry' of https://github.com/firebase/genkit …
huangjeff5 Sep 3, 2025
f59f68d
fmt
huangjeff5 Sep 3, 2025
a69e048
Fix formatting for addAutomaticTelemetry function call
huangjeff5 Sep 4, 2025
1062001
Remove unnecessary comment in writeUserAcceptance
huangjeff5 Sep 4, 2025
2cf334a
Remove production handling comments in metrics.go
huangjeff5 Sep 4, 2025
077a30b
Change comment for payload structure compatibility
huangjeff5 Sep 4, 2025
7f08d9b
Remove redundant comments for constants
huangjeff5 Sep 4, 2025
d12fdb7
fix
huangjeff5 Sep 4, 2025
48f2591
Remove outdated comment in loggingDenied function
huangjeff5 Sep 4, 2025
e34b39b
Update comment to clarify metadata field removal
huangjeff5 Sep 4, 2025
0154083
Clean up comments in slog_handler
huangjeff5 Sep 4, 2025
937ce07
Mark indirect dependencies in go.mod
huangjeff5 Sep 4, 2025
2e44fbc
addres comments
huangjeff5 Sep 4, 2025
22dd347
Enhance error handling comments in error.go
huangjeff5 Sep 5, 2025
1981551
fmt
huangjeff5 Sep 5, 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 @@ -140,7 +140,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 @@ -149,7 +153,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 @@ -160,6 +161,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/")
}

// IsResource reports whether the [Part] contains a resource reference.
func (p *Part) IsResource() bool {
return p.Kind == PartResource
Expand Down Expand Up @@ -313,3 +341,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 @@ -196,7 +196,12 @@ func NewEvaluator(name string, opts *EvaluatorOptions, fn EvaluatorFunc) Evaluat
if datapoint.TestCaseId == "" {
datapoint.TestCaseId = uuid.New().String()
}
_, err := tracing.RunInNewSpan(ctx, fmt.Sprintf("TestCase %s", datapoint.TestCaseId), "evaluator", false, datapoint,
spanMetadata := &tracing.SpanMetadata{
Name: fmt.Sprintf("TestCase %s", datapoint.TestCaseId),
Type: "evaluator",
Subtype: "evaluator",
}
_, err := tracing.RunInNewSpan(ctx, spanMetadata, datapoint,
func(ctx context.Context, input *Example) (*EvaluatorCallbackResponse, error) {
traceId := trace.SpanContextFromContext(ctx).TraceID().String()
spanId := trace.SpanContextFromContext(ctx).SpanID().String()
Expand Down
212 changes: 211 additions & 1 deletion go/ai/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"slices"
"strings"
"time"

"github.com/firebase/genkit/go/core"
"github.com/firebase/genkit/go/core/api"
Expand Down Expand Up @@ -122,7 +123,12 @@ func DefineGenerateAction(ctx context.Context, r api.Registry) *generateAction {
"err", err)
}()

return tracing.RunInNewSpan(ctx, "generate", "util", false, actionOpts,
spanMetadata := &tracing.SpanMetadata{
Name: "generate",
Type: "util",
Subtype: "util",
}
return tracing.RunInNewSpan(ctx, spanMetadata, actionOpts,
func(ctx context.Context, actionOpts *GenerateActionOptions) (*ModelResponse, error) {
return GenerateWithRequest(ctx, r, actionOpts, nil, cb)
})
Expand Down Expand Up @@ -176,6 +182,7 @@ func NewModel(name string, opts *ModelOptions, fn ModelFunc) Model {
simulateSystemPrompt(opts, nil),
augmentWithContext(opts, nil),
validateSupport(name, opts),
addAutomaticTelemetry(),
}
fn = core.ChainMiddleware(mws...)(fn)

Expand Down Expand Up @@ -1073,6 +1080,209 @@ func handleResumeOption(ctx context.Context, r api.Registry, genOpts *GenerateAc
}, 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 {
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
}

// processResources processes messages to replace resource parts with actual content.
func processResources(ctx context.Context, r api.Registry, messages []*Message) ([]*Message, error) {
processedMessages := make([]*Message, len(messages))
Expand Down
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
23 changes: 18 additions & 5 deletions go/core/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ func newAction[In, Out, Stream any](

return &ActionDef[In, Out, Stream]{
fn: func(ctx context.Context, input In, cb StreamCallback[Stream]) (Out, error) {
tracing.SetCustomMetadataAttr(ctx, "subtype", string(atype))
return fn(ctx, input, cb)
},
desc: &api.ActionDesc{
Expand Down Expand Up @@ -185,7 +184,23 @@ func (a *ActionDef[In, Out, Stream]) Run(ctx context.Context, input In, cb Strea
"err", err)
}()

return tracing.RunInNewSpan(ctx, a.desc.Name, "action", false, input,
// Create span metadata and inject flow name if we're in a flow context
spanMetadata := &tracing.SpanMetadata{
Name: a.desc.Name,
Type: "action",
Subtype: string(a.desc.Type), // The actual action type becomes the subtype
// IsRoot will be automatically determined in tracing.go based on parent span presence
}

// Auto-inject flow name if we're in a flow context
if flowName := FlowNameFromContext(ctx); flowName != "" {
if spanMetadata.Metadata == nil {
spanMetadata.Metadata = make(map[string]string)
}
spanMetadata.Metadata["flow:name"] = flowName
}

return tracing.RunInNewSpan(ctx, spanMetadata, input,
func(ctx context.Context, input In) (Out, error) {
start := time.Now()
var err error
Expand Down Expand Up @@ -220,9 +235,7 @@ func (a *ActionDef[In, Out, Stream]) RunJSON(ctx context.Context, input json.Raw
}
var in In
if input != nil {
if err := json.Unmarshal(input, &in); err != nil {
return nil, err
}
json.Unmarshal(input, &in)
}
var callback func(context.Context, Stream) error
if cb != nil {
Expand Down
Loading
Loading