Go SDK

The 2Signal Go SDK instruments your Go AI agents with automatic tracing, token counting, and cost tracking. Go 1.21+. Uses context.Context for propagation and goroutines for background export.

Installation

go get github.com/BryceKeeler720/2Signal/sdks/go

Quick Start

package main

import (
    "context"
    "fmt"
    twosignal "github.com/BryceKeeler720/2Signal/sdks/go"
)

func main() {
    client := twosignal.New(twosignal.Options{
        APIKey: "ts_...", // or set TWOSIGNAL_API_KEY env var
    })
    defer client.Shutdown()

    ctx := context.Background()

    result, err := twosignal.Observe(ctx, "support-agent", twosignal.SpanTypeAgent, "What is your refund policy?",
        func(ctx context.Context) (string, error) {
            // your agent logic here
            return "Our refund policy is...", nil
        },
    )
    if err != nil {
        fmt.Println("Error:", err)
    }
    fmt.Println(result)
}

Initialization

client := twosignal.New(twosignal.Options{
    APIKey:        "ts_...",       // or TWOSIGNAL_API_KEY env var
    BaseURL:       "https://...", // or TWOSIGNAL_BASE_URL env var
    Enabled:       true,           // set false to disable all tracing
    FlushInterval: time.Second,    // duration between batch flushes
    MaxBatchSize:  100,            // max events per batch
})

// the constructor sets the global instance automatically
instance := twosignal.GetInstance()

The SDK is a singleton. It starts a background goroutine that batches and flushes events. Call Shutdown() before your process exits to flush remaining events.

Configuration

FieldEnv VarDefaultDescription
APIKeyTWOSIGNAL_API_KEYYour project API key
BaseURLTWOSIGNAL_BASE_URLhttp://localhost:3000API endpoint
EnabledtrueEnable/disable all tracing
FlushInterval1sDuration between background flushes
MaxBatchSize100Max events per HTTP request

Creating Spans

span := client.Span(twosignal.SpanOpts{
    Name:     "retrieve-docs",
    SpanType: twosignal.SpanTypeRetrieval,
    Input:    map[string]interface{}{"query": query},
    Metadata: map[string]interface{}{"db": "pinecone", "topK": 5},
})

err := span.Run(ctx, func(ctx context.Context, s *twosignal.Span) error {
    docs, err := vectorDB.Search(ctx, query)
    if err != nil {
        return err // span automatically marked as ERROR
    }
    s.Output = docs
    s.Metadata = map[string]interface{}{"resultCount": len(docs)}
    return nil
})

Fields you can set on the span inside Run():

FieldTypeDescription
s.OutputinterfaceSpan output (any JSON-serializable value)
s.Metadatamap[string]interfaceArbitrary metadata
s.ModelstringModel name (for LLM spans)
s.ModelParametersmap[string]interfaceTemperature, max_tokens, etc.
s.Usage*SpanUsagePromptTokens, CompletionTokens, TotalTokens
s.Cost*float64Cost in USD
s.StatusSpanStatusStatusOK or StatusError
s.ErrorMessagestringError description

If Run() returns an error, the span is automatically marked with StatusError and the error message is recorded. The error is still returned to the caller.

Observe Wrapper

Observe is a generic function that wraps any function call in a span:

result, err := twosignal.Observe(ctx, "classify", twosignal.SpanTypeTool, inputData,
    func(ctx context.Context) (Classification, error) {
        return classifier.Classify(ctx, inputData)
    },
)
// result is typed as Classification

The span is created, the function runs inside it, and the result is set as the span's output. If the function returns an error, the span is marked as ERROR and the error is returned.

Context Propagation

The SDK uses standard Go context.Context to propagate trace information. Nested spans automatically become children of their parent:

outer := client.Span(twosignal.SpanOpts{Name: "outer", SpanType: twosignal.SpanTypeAgent})
err := outer.Run(ctx, func(ctx context.Context, s *twosignal.Span) error {
    // ctx now carries the trace context

    // this span automatically becomes a child of "outer"
    inner := client.Span(twosignal.SpanOpts{Name: "inner", SpanType: twosignal.SpanTypeTool})
    return inner.Run(ctx, func(ctx context.Context, s *twosignal.Span) error {
        // nested correctly — same trace ID, parent set automatically
        return nil
    })
})

Span Types

ConstantValueUse for
SpanTypeAgentAGENTTop-level agent function
SpanTypeLLMLLMLLM API calls
SpanTypeToolTOOLTool / function calls
SpanTypeRetrievalRETRIEVALRAG / vector search
SpanTypeChainCHAINPipeline steps
SpanTypeCustomCUSTOMEverything else (default)

Cost Calculation

cost := twosignal.CalculateCost("gpt-4o", 100, 50)
// returns float64 (-1 if model not in pricing table)

Supports OpenAI, Anthropic, Google Gemini, Mistral, Cohere, and Groq models.

Manual LLM Span Example

Since the Go SDK doesn't include provider wrappers, create LLM spans manually:

span := client.Span(twosignal.SpanOpts{
    Name:     "openai-chat",
    SpanType: twosignal.SpanTypeLLM,
    Input:    messages,
})

err := span.Run(ctx, func(ctx context.Context, s *twosignal.Span) error {
    resp, err := openaiClient.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
        Model:    "gpt-4o",
        Messages: messages,
    })
    if err != nil {
        return err
    }

    s.Model = "gpt-4o"
    s.Output = resp.Choices[0].Message.Content
    s.Usage = &twosignal.SpanUsage{
        PromptTokens:     resp.Usage.PromptTokens,
        CompletionTokens: resp.Usage.CompletionTokens,
        TotalTokens:      resp.Usage.TotalTokens,
    }
    cost := twosignal.CalculateCost("gpt-4o", resp.Usage.PromptTokens, resp.Usage.CompletionTokens)
    if cost >= 0 {
        s.Cost = &cost
    }
    return nil
})

Lifecycle

// force-flush all pending events
client.Flush()

// graceful shutdown — flushes and stops the background goroutine (5s timeout)
client.Shutdown()

Always defer client.Shutdown() after creating the client to ensure events are flushed before exit.

Have questions? Join our community!

Connect with other developers and the 2Signal team.

Join Discord