Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
62 changes: 62 additions & 0 deletions contrib/googleapis/go-genai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Google GenAI Integration

This integration provides Datadog LLM Observability tracing for the
[google.golang.org/genai](https://github.com/googleapis/go-genai) SDK.

## Usage

```go
import (
"context"

genaitrace "github.com/DataDog/dd-trace-go/contrib/googleapis/go-genai/v2"
"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
"google.golang.org/genai"
)

func main() {
tracer.Start()
defer tracer.Stop()

raw, err := genai.NewClient(context.Background(), &genai.ClientConfig{
APIKey: "...",
Backend: genai.BackendGeminiAPI,
})
if err != nil {
panic(err)
}
client := genaitrace.WrapClient(raw)

resp, err := client.Models.GenerateContent(
context.Background(),
"gemini-2.0-flash",
[]*genai.Content{{Role: "user", Parts: []*genai.Part{{Text: "Hello!"}}}},
nil,
)
_ = resp
_ = err
}
```

The returned `*Client` exposes the same `Models` and `Chats` services as the
upstream SDK. For services that are not (yet) instrumented, call `Raw()` to get
the underlying `*genai.Client`.

## Features

The integration automatically produces LLM Observability spans for:

- **`Models.GenerateContent`** — LLM span with prompt/response messages,
generation parameters (temperature, top_p, top_k, max_output_tokens, stop
sequences) and token usage metrics.
- **`Models.GenerateContentStream`** — LLM span covering the entire stream;
output text is reassembled from chunks and final usage metadata is captured.
- **`Models.EmbedContent`** — Embedding span with input documents and a summary
of the returned vectors.
- **`Chats.Create` / `Chat.SendMessage` / `Chat.SendMessageStream`** — LLM
spans whose input includes the existing chat history plus the new user
message.

Spans are tagged with the model provider derived from the underlying client's
backend (`google` for Gemini API, `google_vertexai` for Vertex AI,
`google_enterprise` for the Enterprise Agent Platform).
114 changes: 114 additions & 0 deletions contrib/googleapis/go-genai/chats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.

package genaitrace

import (
"context"
"iter"

"google.golang.org/genai"
)

// Chats wraps *genai.Client.Chats with LLM Observability tracing.
type Chats struct {
c *genai.Chats
provider string
}

// Create creates a new chat session. The returned *Chat is a tracing wrapper;
// its SendMessage and SendMessageStream methods produce LLM spans.
func (c *Chats) Create(ctx context.Context, model string, config *genai.GenerateContentConfig, history []*genai.Content) (*Chat, error) {
if c == nil || c.c == nil {
return nil, errNilClient
}
chat, err := c.c.Create(ctx, model, config, history)
if err != nil {
return nil, err
}
return &Chat{
chat: chat,
model: model,
provider: c.provider,
config: config,
}, nil
}

// Chat is a tracing wrapper around *genai.Chat.
type Chat struct {
chat *genai.Chat
model string
provider string
config *genai.GenerateContentConfig
}

// Raw returns the underlying *genai.Chat.
func (c *Chat) Raw() *genai.Chat {
if c == nil {
return nil
}
return c.chat
}

// History delegates to the underlying *genai.Chat.
func (c *Chat) History(curated bool) []*genai.Content {
return c.chat.History(curated)
}

// SendMessage sends a message to the chat and emits an LLM span. The input
// captured on the span is a pre-call snapshot of the chat history plus the
// new user message (genai.Chat mutates History during a successful call).
func (c *Chat) SendMessage(ctx context.Context, parts ...genai.Part) (*genai.GenerateContentResponse, error) {
if c == nil || c.chat == nil {
return nil, errNilClient
}
input := c.snapshotInput(parts)
span, ctx := startLLMSpan(ctx, "genai.chat.send_message", c.model, c.provider)

resp, err := c.chat.SendMessage(ctx, parts...)
finishLLMSpan(span, input, c.config, resp, err)
return resp, err
}

// SendMessageStream sends a message and streams the response, emitting an
// LLM span covering the stream.
func (c *Chat) SendMessageStream(ctx context.Context, parts ...genai.Part) iter.Seq2[*genai.GenerateContentResponse, error] {
return func(yield func(*genai.GenerateContentResponse, error) bool) {
if c == nil || c.chat == nil {
yield(nil, errNilClient)
return
}
input := c.snapshotInput(parts)
span, ctx := startLLMSpan(ctx, "genai.chat.send_message_stream", c.model, c.provider)

acc := newStreamAccumulator()
var lastErr error
for chunk, err := range c.chat.SendMessageStream(ctx, parts...) {
if err != nil {
lastErr = err
} else {
acc.add(chunk)
}
if !yield(chunk, err) {
break
}
}
finishLLMSpan(span, input, c.config, acc.response(), lastErr)
}
}

// snapshotInput returns the history at call time plus the new user message,
// taken before genai.Chat mutates History with the turn being sent.
func (c *Chat) snapshotInput(parts []genai.Part) []*genai.Content {
src := c.chat.History(false)
hist := make([]*genai.Content, len(src))
copy(hist, src)
userParts := make([]*genai.Part, 0, len(parts))
for i := range parts {
p := parts[i]
userParts = append(userParts, &p)
}
return append(hist, &genai.Content{Parts: userParts, Role: string(genai.RoleUser)})
}
59 changes: 59 additions & 0 deletions contrib/googleapis/go-genai/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.

package genaitrace

import (
"google.golang.org/genai"
)

// Client is a tracing wrapper around *genai.Client. Calls on its Models and
// Chats fields produce LLM Observability spans. Use Raw to access the
// underlying *genai.Client for services that are not (yet) instrumented.
type Client struct {
raw *genai.Client
provider string

// Models wraps the underlying *genai.Client.Models with tracing.
Models *Models
// Chats wraps the underlying *genai.Client.Chats with tracing.
Chats *Chats
}

// WrapClient returns a tracing wrapper around the given *genai.Client.
// The returned *Client exposes wrapper Models and Chats with the same
// method signatures as the upstream SDK.
func WrapClient(c *genai.Client) *Client {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

usually we accept here a variadic opts ...Option argument in case we want to provide configuration for the integration. Even if we don't have any option in mind right now, its worth it to add it to avoid introducing breaking changes in the api if we need it in the future.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 0e44ff9: added an Option type and WrapClient(c *genai.Client, opts ...Option) — no options exported yet, but the variadic form is in place to avoid a breaking change later.

if c == nil {
return nil
}
provider := backendProvider(c)
tc := &Client{
raw: c,
provider: provider,
}
tc.Models = &Models{m: c.Models, provider: provider}
tc.Chats = &Chats{c: c.Chats, provider: provider}
return tc
}

// Raw returns the underlying *genai.Client.
func (c *Client) Raw() *genai.Client {
if c == nil {
return nil
}
return c.raw
}

func backendProvider(c *genai.Client) string {
switch c.ClientConfig().Backend {
case genai.BackendVertexAI:
return "google_vertexai"
case genai.BackendEnterprise:
return "google_enterprise"
default:
return "google"
}
}
61 changes: 61 additions & 0 deletions contrib/googleapis/go-genai/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.

package genaitrace_test

import (
"context"

"google.golang.org/genai"

genaitrace "github.com/DataDog/dd-trace-go/contrib/googleapis/go-genai/v2"

"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
)

func Example() {
tracer.Start()
defer tracer.Stop()

raw, err := genai.NewClient(context.Background(), &genai.ClientConfig{
APIKey: "your-api-key",
Backend: genai.BackendGeminiAPI,
})
if err != nil {
panic(err)
}
client := genaitrace.WrapClient(raw)

resp, err := client.Models.GenerateContent(
context.Background(),
"gemini-2.0-flash",
[]*genai.Content{{Role: "user", Parts: []*genai.Part{{Text: "Hello!"}}}},
nil,
)
_ = resp
_ = err
}

func Example_chat() {
tracer.Start()
defer tracer.Stop()

raw, err := genai.NewClient(context.Background(), &genai.ClientConfig{
APIKey: "your-api-key",
Backend: genai.BackendGeminiAPI,
})
if err != nil {
panic(err)
}
client := genaitrace.WrapClient(raw)

chat, err := client.Chats.Create(context.Background(), "gemini-2.0-flash", nil, nil)
if err != nil {
panic(err)
}
resp, err := chat.SendMessage(context.Background(), genai.Part{Text: "Hello!"})
_ = resp
_ = err
}
22 changes: 22 additions & 0 deletions contrib/googleapis/go-genai/genai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.

// Package genaitrace provides Datadog LLM Observability tracing for the
// Google GenAI Go SDK (google.golang.org/genai).
//
// Wrap a *genai.Client with [WrapClient] and use the returned *Client in
// place of the original; calls on its Models and Chats fields produce
// LLM Observability spans automatically.
package genaitrace // import "github.com/DataDog/dd-trace-go/contrib/googleapis/go-genai/v2"

import (
"github.com/DataDog/dd-trace-go/v2/instrumentation"
)

var instr *instrumentation.Instrumentation

func init() {
instr = instrumentation.Load(instrumentation.PackageGoogleAPIsGoGenAI)
}
Loading
Loading