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
49 changes: 49 additions & 0 deletions examples/otel-receiver/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"log"
"net/http"
"os"
"strings"

raindrop "github.com/raindrop-ai/go"
)

// This example starts an HTTP server that receives OTLP/HTTP JSON traces
// and forwards them to Raindrop.
//
// Usage:
// RAINDROP_WRITE_KEY=rk_... go run .
//
// Then configure your OTLP trace source to send to:
// http://your-server:8090/v1/traces

func main() {
writeKey := strings.TrimSpace(os.Getenv("RAINDROP_WRITE_KEY"))
if writeKey == "" {
log.Fatal("RAINDROP_WRITE_KEY is required")
}

client, err := raindrop.New(
raindrop.WithWriteKey(writeKey),
raindrop.WithServiceName("my-go-proxy"),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()

mux := http.NewServeMux()
mux.Handle("/v1/traces", client.OTLPHandler())

addr := envOrDefault("ADDR", ":8090")
log.Printf("OTLP receiver listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}

func envOrDefault(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
244 changes: 244 additions & 0 deletions examples/otel-spans/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package main

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"

raindrop "github.com/raindrop-ai/go"
otelattribute "go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
oteltrace "go.opentelemetry.io/otel/trace"
)

// OpenAI-compatible chat types (works with any OpenAI-compatible provider).

type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}

type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
}

type chatResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []chatChoice `json:"choices"`
Usage *chatUsage `json:"usage"`
}

type chatChoice struct {
Message chatMessage `json:"message"`
}

type chatUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}

type weatherResult struct {
Forecast string `json:"forecast"`
TemperatureF int `json:"temperature_f"`
}

func main() {
writeKey := strings.TrimSpace(os.Getenv("RAINDROP_WRITE_KEY"))
if writeKey == "" {
log.Fatal("RAINDROP_WRITE_KEY is required")
}

llmAPIKey := strings.TrimSpace(os.Getenv("LLM_API_KEY"))
if llmAPIKey == "" {
log.Fatal("LLM_API_KEY is required")
}

llmEndpoint := envOrDefault("LLM_ENDPOINT", "https://api.openai.com/v1/chat/completions")
model := envOrDefault("LLM_MODEL", "gpt-4o-mini")
userID := "user-123"
convoID := "conv-123"
prompt := "Plan a calm Saturday morning in San Francisco."

client, err := raindrop.New(
raindrop.WithWriteKey(writeKey),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()

ctx := context.Background()
httpClient := &http.Client{Timeout: 30 * time.Second}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(client.OTelSpanExporter()),
sdktrace.WithResource(sdkresource.NewSchemaless(
otelattribute.String("service.name", "otel-spans-example"),
otelattribute.String("service.version", raindrop.Version),
)),
)
defer func() { _ = tp.Shutdown(ctx) }()

tracer := tp.Tracer("github.com/raindrop-ai/go/examples/otel-spans")
interaction := client.Begin(ctx, raindrop.BeginOptions{
UserID: userID,
Event: "chat_message",
Input: prompt,
ConvoID: convoID,
})

_, weatherSpan := tracer.Start(ctx, "weather_lookup",
oteltrace.WithAttributes(raindrop.OTelToolAttributes("weather_lookup", map[string]any{"location": "San Francisco"}, nil, map[string]any{
"event_id": interaction.EventID(),
})...),
)
weather, err := lookupWeather("San Francisco")
if err != nil {
weatherSpan.RecordError(err)
weatherSpan.SetStatus(otelcodes.Error, err.Error())
weatherSpan.End()
log.Fatal(err)
}
weatherSpan.SetAttributes(raindrop.OTelToolAttributes("weather_lookup", nil, weather, nil)...)
weatherSpan.End()

llmCtx, llmSpan := tracer.Start(ctx, "chat.completion",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
otelattribute.String("ai.telemetry.metadata.raindrop.eventId", interaction.EventID()),
otelattribute.String("gen_ai.request.model", model),
),
)
completion, err := createChatCompletion(llmCtx, httpClient, llmEndpoint, llmAPIKey, model, prompt, weather)
if err != nil {
llmSpan.RecordError(err)
llmSpan.SetStatus(otelcodes.Error, err.Error())
llmSpan.End()
log.Fatal(err)
}
if completion.Usage != nil {
llmSpan.SetAttributes(
otelattribute.String("gen_ai.response.model", completion.Model),
otelattribute.Int("gen_ai.usage.prompt_tokens", completion.Usage.PromptTokens),
otelattribute.Int("gen_ai.usage.completion_tokens", completion.Usage.CompletionTokens),
)
}
llmSpan.End()

reply, err := assistantText(completion)
if err != nil {
log.Fatal(err)
}

properties := map[string]any{}
if completion.Usage != nil {
properties["gen_ai.usage.prompt_tokens"] = completion.Usage.PromptTokens
properties["gen_ai.usage.completion_tokens"] = completion.Usage.CompletionTokens
}

if err := interaction.Finish(raindrop.FinishOptions{
Output: reply,
Model: completion.Model,
Properties: properties,
}); err != nil {
log.Fatal(err)
}

fmt.Println(reply)
}

func createChatCompletion(ctx context.Context, httpClient *http.Client, endpoint, apiKey, model, prompt string, weather weatherResult) (chatResponse, error) {
reqBody := chatRequest{
Model: model,
Messages: []chatMessage{
{
Role: "system",
Content: fmt.Sprintf("You are a helpful local planner. Weather context: %s, %dF.", weather.Forecast, weather.TemperatureF),
},
{
Role: "user",
Content: prompt,
},
},
MaxTokens: 300,
}

body, err := json.Marshal(reqBody)
if err != nil {
return chatResponse{}, err
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return chatResponse{}, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")

resp, err := httpClient.Do(req)
if err != nil {
return chatResponse{}, err
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return chatResponse{}, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return chatResponse{}, fmt.Errorf("chat completion failed: %s: %s", resp.Status, strings.TrimSpace(string(respBody)))
}

var decoded chatResponse
if err := json.Unmarshal(respBody, &decoded); err != nil {
return chatResponse{}, err
}
return decoded, nil
}

func assistantText(response chatResponse) (string, error) {
if len(response.Choices) == 0 {
return "", errors.New("provider returned no choices")
}

text := strings.TrimSpace(response.Choices[0].Message.Content)
if text == "" {
return "", errors.New("provider returned an empty assistant message")
}
return text, nil
}

func lookupWeather(location string) (weatherResult, error) {
switch location {
case "San Francisco":
return weatherResult{
Forecast: "sunny",
TemperatureF: 65,
}, nil
default:
return weatherResult{
Forecast: "mild",
TemperatureF: 68,
}, nil
}
}

func envOrDefault(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
module github.com/raindrop-ai/go

go 1.21

require (
go.opentelemetry.io/otel v1.28.0
go.opentelemetry.io/otel/sdk v1.28.0
go.opentelemetry.io/otel/trace v1.28.0
)

require (
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
go.opentelemetry.io/otel/metric v1.28.0 // indirect
golang.org/x/sys v0.21.0 // indirect
)
27 changes: 27 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading
Loading