Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
105 changes: 105 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# traceAI Go SDK

OpenTelemetry instrumentation for AI/LLM frameworks in Go, following the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/).

## Packages

| Package | Description |
|---------|-------------|
| `traceai` | Core setup — TracerProvider, config, semantic conventions |
| `traceai_openai` | OpenAI Go client instrumentation |

## Quick Start

```go
package main

import (
"context"
"fmt"
"log"

"github.com/future-agi/traceAI/go/traceai"
"github.com/future-agi/traceAI/go/traceai_openai"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)

func main() {
provider, err := traceai.Register(
traceai.WithProjectName("my-go-service"),
)
if err != nil {
log.Fatal(err)
}
defer provider.Shutdown(context.Background())

client := openai.NewClient(
option.WithMiddleware(traceai_openai.Middleware()),
)

resp, err := client.Chat.Completions.New(context.Background(), openai.ChatCompletionNewParams{
Model: "gpt-4",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("What is OpenTelemetry?"),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Choices[0].Message.Content)
}
```

## Span Attributes

Spans follow the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) — `gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.*` tokens, `gen_ai.response.*`, etc. Prompt and completion content is captured by default; disable with `WithContentCapture(false)` for PII-sensitive workloads.

## Configuration

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `FI_API_KEY` | Future AGI API key | — |
| `FI_SECRET_KEY` | Future AGI secret key | — |
| `FI_BASE_URL` | OTLP HTTP collector endpoint | `https://api.futureagi.com` |
| `FI_GRPC_URL` | OTLP gRPC collector endpoint | `https://grpc.futureagi.com` |
| `FI_PROJECT_NAME` | Project name for trace grouping | `DEFAULT_PROJECT_NAME` |

### Options

```go
traceai.Register(traceai.WithTransport(traceai.TransportGRPC))
traceai.Register(traceai.WithShutdownTimeout(30 * time.Second))

// disable content capture for PII-sensitive workloads
traceai_openai.Middleware(traceai_openai.WithContentCapture(false))
traceai_openai.Middleware(traceai_openai.WithTracerProvider(myTP))
```

## Custom Backend

Standard OTLP, point it at any collector:

```go
provider, _ := traceai.Register(
traceai.WithBaseURL("https://your-otel-collector:4318"),
traceai.WithSetGlobal(true),
)
```

## Development

```bash
cd go/traceai_openai
go test ./...
```

## Adding a Framework

See `traceai_openai/` for the pattern — create `traceai_<name>/`, implement an HTTP middleware or client wrapper, wire up the semconv attributes.

## License

Apache 2.0 — same as the traceAI project.
205 changes: 205 additions & 0 deletions go/traceai/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Package traceai sets up OTel tracing with Future AGI defaults.
package traceai

import (
"context"
"fmt"
"os"
"time"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
)

const (
envAPIKey = "FI_API_KEY"
envSecretKey = "FI_SECRET_KEY"
envBaseURL = "FI_BASE_URL"
envGRPCURL = "FI_GRPC_URL"
envProjectName = "FI_PROJECT_NAME"

defaultBaseURL = "https://api.futureagi.com"
defaultGRPCURL = "https://grpc.futureagi.com"
defaultProjectName = "DEFAULT_PROJECT_NAME"

headerAPIKey = "X-Api-Key"
headerSecretKey = "X-Secret-Key"
)

// Transport specifies the OTLP export protocol.
type Transport int

const (
TransportHTTP Transport = iota
TransportGRPC
)

// Config holds the options for initializing a traceAI TracerProvider.
type Config struct {
ProjectName string
Transport Transport
BaseURL string
GRPCURL string
APIKey string
SecretKey string
BatchExport bool
SetGlobal bool
ShutdownTimeout time.Duration
}

// DefaultConfig returns a Config populated from environment variables.
func DefaultConfig() Config {
return Config{
ProjectName: envOrDefault(envProjectName, defaultProjectName),
Transport: TransportHTTP,
BaseURL: envOrDefault(envBaseURL, defaultBaseURL),
GRPCURL: envOrDefault(envGRPCURL, defaultGRPCURL),
APIKey: os.Getenv(envAPIKey),
SecretKey: os.Getenv(envSecretKey),
BatchExport: true,
SetGlobal: true,
ShutdownTimeout: 10 * time.Second,
}
}

type Option func(*Config)

func WithProjectName(name string) Option {
return func(c *Config) { c.ProjectName = name }
}

func WithTransport(t Transport) Option {
return func(c *Config) { c.Transport = t }
}

func WithBaseURL(url string) Option {
return func(c *Config) { c.BaseURL = url }
}

func WithCredentials(apiKey, secretKey string) Option {
return func(c *Config) {
c.APIKey = apiKey
c.SecretKey = secretKey
}
}

func WithBatchExport(batch bool) Option {
return func(c *Config) { c.BatchExport = batch }
}

func WithSetGlobal(global bool) Option {
return func(c *Config) { c.SetGlobal = global }
}

func WithShutdownTimeout(d time.Duration) Option {
return func(c *Config) { c.ShutdownTimeout = d }
}

// Provider wraps a TracerProvider with shutdown handling.
type Provider struct {
tp *sdktrace.TracerProvider
shutdownTimeout time.Duration
}

// Register creates and configures a new traceAI TracerProvider.
// It returns a Provider whose Shutdown method should be called on process exit.
//
// provider, err := traceai.Register(
// traceai.WithProjectName("my-llm-service"),
// )
// if err != nil {
// log.Fatal(err)
// }
// defer provider.Shutdown(context.Background())
func Register(opts ...Option) (*Provider, error) {
cfg := DefaultConfig()
for _, o := range opts {
o(&cfg)
}

ctx := context.Background()

exporter, err := newExporter(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("traceai: create exporter: %w", err)
}

res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(cfg.ProjectName),
),
)
if err != nil {
return nil, fmt.Errorf("traceai: create resource: %w", err)
}

var spanProcessor sdktrace.SpanProcessor
if cfg.BatchExport {
spanProcessor = sdktrace.NewBatchSpanProcessor(exporter)
} else {
spanProcessor = sdktrace.NewSimpleSpanProcessor(exporter)
}

tp := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(spanProcessor),
)

if cfg.SetGlobal {
otel.SetTracerProvider(tp)
}

return &Provider{
tp: tp,
shutdownTimeout: cfg.ShutdownTimeout,
}, nil
}

func (p *Provider) TracerProvider() trace.TracerProvider {
return p.tp
}

// Shutdown flushes pending spans and shuts down the exporter.
func (p *Provider) Shutdown(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, p.shutdownTimeout)
defer cancel()
return p.tp.Shutdown(ctx)
}

func newExporter(ctx context.Context, cfg Config) (sdktrace.SpanExporter, error) {
headers := make(map[string]string)
if cfg.APIKey != "" {
headers[headerAPIKey] = cfg.APIKey
}
if cfg.SecretKey != "" {
headers[headerSecretKey] = cfg.SecretKey
}

switch cfg.Transport {
case TransportGRPC:
return otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(cfg.GRPCURL),
otlptracegrpc.WithHeaders(headers),
)
default:
endpoint := cfg.BaseURL + "/tracer/v1/traces"
return otlptracehttp.New(ctx,
otlptracehttp.WithEndpointURL(endpoint),
otlptracehttp.WithHeaders(headers),
)
}
}

func envOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
30 changes: 30 additions & 0 deletions go/traceai/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module github.com/future-agi/traceAI/go/traceai

go 1.22.0

require (
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
)

require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)
Loading