diff --git a/go/adapter/datadog/adapter.go b/go/adapter/datadog/adapter.go index 0bacf54..2af71ec 100644 --- a/go/adapter/datadog/adapter.go +++ b/go/adapter/datadog/adapter.go @@ -88,7 +88,6 @@ func (d *DatadogAdapter) Flush(evts []observe.TraceEvent) error { } if len(allSpans) == 0 { - log.Println("No spans built for datadog trace") return nil } diff --git a/go/adapter/honeycomb/adapter.go b/go/adapter/honeycomb/adapter.go index b34f420..98aae93 100644 --- a/go/adapter/honeycomb/adapter.go +++ b/go/adapter/honeycomb/adapter.go @@ -18,7 +18,7 @@ import ( type HoneycombConfig struct { ApiKey string Dataset string - EmitTracesInterval uint32 + EmitTracesInterval time.Duration TraceBatchMax uint32 Host string } @@ -68,7 +68,6 @@ func (h *HoneycombAdapter) Flush(evts []observe.TraceEvent) error { } if len(allSpans) == 0 { - log.Println("No spans built for honeycomb") return nil } diff --git a/go/adapter/lightstep/adapter.go b/go/adapter/lightstep/adapter.go new file mode 100644 index 0000000..a0cf7dc --- /dev/null +++ b/go/adapter/lightstep/adapter.go @@ -0,0 +1,138 @@ +package lightstep + +import ( + "bytes" + "context" + "fmt" + "log" + "net/http" + "net/url" + "time" + + observe "github.com/dylibso/observe-sdk/go" + otel "github.com/dylibso/observe-sdk/go/adapter/otel_formatter" + trace "go.opentelemetry.io/proto/otlp/trace/v1" + proto "google.golang.org/protobuf/proto" +) + +type LightstepConfig struct { + ApiKey string + ServiceName string + EmitTracesInterval time.Duration + TraceBatchMax uint32 + Host string +} + +type LightstepAdapter struct { + *observe.AdapterBase + Config *LightstepConfig +} + +func NewLightstepAdapter(config *LightstepConfig) *LightstepAdapter { + base := observe.NewAdapterBase(1, 0) + adapter := &LightstepAdapter{ + AdapterBase: &base, + Config: config, + } + + adapter.AdapterBase.SetFlusher(adapter) + + return adapter +} + +func (h *LightstepAdapter) Start(ctx context.Context) { + h.AdapterBase.Start(h, ctx) +} + +func (h *LightstepAdapter) HandleTraceEvent(te observe.TraceEvent) { + h.AdapterBase.HandleTraceEvent(te) +} + +func (h *LightstepAdapter) Flush(evts []observe.TraceEvent) error { + for _, te := range evts { + traceId := te.TelemetryId.ToHex16() + + var allSpans []*trace.Span + for _, e := range te.Events { + switch event := e.(type) { + case observe.CallEvent: // TODO: consider renaming to FunctionCall for consistency across Rust & JS + spans := h.makeCallSpans(event, nil, traceId) + if len(spans) > 0 { + allSpans = append(allSpans, spans...) + } + case observe.MemoryGrowEvent: + log.Println("MemoryGrowEvent should be attached to a span") + case observe.CustomEvent: + log.Println("lightstep adapter does not respect custom events") + } + } + + if len(allSpans) == 0 { + return nil + } + + t := otel.NewTrace(traceId, h.Config.ServiceName, allSpans) + if te.AdapterMeta != nil { + meta, ok := te.AdapterMeta.(map[string]string) + if ok { + t.SetMetadata(&te, meta) + } else { + log.Println("metadata must be of type map[string]string") + } + } + data, err := proto.Marshal(t.TracesData) + if err != nil { + log.Println("failed to marshal TracesData:", err) + return nil + } + + url, err := url.JoinPath(h.Config.Host, "traces", "otlp", "v0.9") + if err != nil { + log.Println("failed to create lightstep endpoint url:", err) + return nil + } + + client := http.Client{ + Timeout: time.Second * 2, + } + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + log.Println("failed to create lightstep endpoint url:", err) + } + + req.Header = http.Header{ + "content-type": {"application/x-protobuf"}, + "lightstep-access-token": {h.Config.ApiKey}, + } + + resp, err := client.Do(req) + if err != nil { + log.Println("failed to send data to lightstep", err) + } + + if resp.StatusCode != http.StatusOK { + log.Println("unexpected status code from lightstep:", resp.StatusCode) + } + + } + + return nil +} + +func (h *LightstepAdapter) makeCallSpans(event observe.CallEvent, parentId []byte, traceId string) []*trace.Span { + name := event.FunctionName() + span := otel.NewSpan(traceId, parentId, name, event.Time, event.Time.Add(event.Duration)) + span.Attributes = append(span.Attributes, otel.NewKeyValueString("function-name", fmt.Sprintf("function-call-%s", name))) + + spans := []*trace.Span{span} + for _, ev := range event.Within() { + if call, ok := ev.(observe.CallEvent); ok { + spans = append(spans, h.makeCallSpans(call, span.SpanId, traceId)...) + } + if alloc, ok := ev.(observe.MemoryGrowEvent); ok { + last := spans[len(spans)-1] + last.Attributes = append(last.Attributes, otel.NewKeyValueInt64("allocation", int64(alloc.MemoryGrowAmount()))) + } + } + return spans +} diff --git a/go/adapter/otel_stdout/adapter.go b/go/adapter/otel_stdout/adapter.go index b8e4642..1c9389c 100644 --- a/go/adapter/otel_stdout/adapter.go +++ b/go/adapter/otel_stdout/adapter.go @@ -50,7 +50,6 @@ func (o *OtelStdoutAdapter) Flush(evts []observe.TraceEvent) error { } if len(allSpans) == 0 { - log.Println("No spans built for datadog trace") return nil } diff --git a/go/bin/honeycomb/main.go b/go/bin/honeycomb/main.go index 16362ae..065ddf3 100644 --- a/go/bin/honeycomb/main.go +++ b/go/bin/honeycomb/main.go @@ -18,7 +18,7 @@ func main() { conf := &honeycomb.HoneycombConfig{ ApiKey: os.Getenv("HONEYCOMB_API_KEY"), Dataset: "golang", - EmitTracesInterval: 1000, + EmitTracesInterval: time.Second * 1, TraceBatchMax: 100, Host: "https://api.honeycomb.io", } diff --git a/go/bin/lightstep/main.go b/go/bin/lightstep/main.go new file mode 100644 index 0000000..db48407 --- /dev/null +++ b/go/bin/lightstep/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/dylibso/observe-sdk/go/adapter/lightstep" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" +) + +func main() { + ctx := context.Background() + + // we only need to create and start once per instance of our host app + conf := &lightstep.LightstepConfig{ + ApiKey: os.Getenv("LIGHTSTEP_API_KEY"), + ServiceName: "golang", + EmitTracesInterval: time.Second * 1, + TraceBatchMax: 100, + Host: "https://ingest.lightstep.com", + } + adapter := lightstep.NewLightstepAdapter(conf) + defer adapter.Stop(true) + adapter.Start(ctx) + + // Load WASM from disk + wasm, err := os.ReadFile(os.Args[1]) + if err != nil { + log.Panicln(err) + } + + cfg := wazero.NewRuntimeConfig().WithCustomSections(true) + rt := wazero.NewRuntimeWithConfig(ctx, cfg) + traceCtx, err := adapter.NewTraceCtx(ctx, rt, wasm, nil) + if err != nil { + log.Panicln(err) + } + wasi_snapshot_preview1.MustInstantiate(ctx, rt) + + config := wazero.NewModuleConfig(). + WithStdin(os.Stdin). + WithStdout(os.Stdout). + WithStderr(os.Stderr). + WithArgs(os.Args[1:]...). + WithStartFunctions("_start") + m, err := rt.InstantiateWithConfig(ctx, wasm, config) + if err != nil { + log.Panicln(err) + } + defer m.Close(ctx) + + // normally this metadata would be in your web-server framework + // or derived when you need them + + meta := map[string]string{ + "http.url": "https://example.com/my-endpoint", + "http.status_code": "200", + "http.client_ip": "66.210.227.34", + } + traceCtx.Metadata(meta) + + traceCtx.Finish() + time.Sleep(2 * time.Second) +}