mirror of
https://github.com/luxfi/trace.git
synced 2026-07-27 01:48:15 +00:00
Previously Type=ZAP was OTLP/HTTP+protobuf misnamed, gated behind
-tags otlp. Per the project rule (ZAP internal, ZIP edge) the ZAP
exporter should be ZAP-native by default — no build tag required,
no protobuf in the dep graph.
Layout:
- exporter_zap.go (no tag, default): true ZAP-native span exporter.
Spans → JSON → ZAP envelope (MsgSpanBatch) → TCP → hanzo/o11y
zapreceiver. Implements sdktrace.SpanExporter, uses luxfi/zap.Node
for transport. Best-effort connect; reconnect on send failure;
fire-and-forget (no response wait).
- exporter_otlp.go (//go:build !grpc && otlp): the old 'exporter_zap.go'
body, renamed to its true identity. OTLP/HTTP+protobuf,
opt-in via -tags otlp for callers that need a real OTLP collector.
- exporter_grpc.go (//go:build grpc): legacy OTLP/gRPC, unchanged.
- exporter_noop.go (//go:build !grpc && !otlp): newExporter for
untagged builds when Type ∈ {HTTP, GRPC}; Type=ZAP and
Type=Disabled never reach this path (tracer.go dispatches first).
tracer.go: New() now dispatches Type=ZAP directly to
newZAPNativeExporter, passing appName + version through to the
ZAP envelope. Falls through to newExporter for the other types.
zap_native_test.go: round-trip — exporter ships a span over the
wire to a stand-in zap.Node receiver and the JSON payload parses
to the expected appName + span name.
Default-tag dep audit (no protobuf, no OTLP, no gRPC):
go list -deps ./... | grep -E 'protobuf|otlp|grpc-gateway|^google.golang.org/grpc$'
→ (empty)
118 lines
2.6 KiB
Go
118 lines
2.6 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package trace_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/trace"
|
|
"github.com/luxfi/zap"
|
|
)
|
|
|
|
// TestZAPNativeExporterRoundTrip wires the exporter against a stand-in
|
|
// ZAP receiver and proves spans flow over the wire as JSON inside a ZAP
|
|
// envelope tagged MsgSpanBatch.
|
|
//
|
|
// Stand-in (not the o11y receiver) so the trace package stays free of
|
|
// the o11y dependency.
|
|
func TestZAPNativeExporterRoundTrip(t *testing.T) {
|
|
var (
|
|
mu sync.Mutex
|
|
payload []byte
|
|
done = make(chan struct{}, 1)
|
|
)
|
|
|
|
port := freePort(t)
|
|
srv := zap.NewNode(zap.NodeConfig{
|
|
NodeID: "test-receiver",
|
|
ServiceType: "_o11y._tcp",
|
|
Port: port,
|
|
NoDiscovery: true,
|
|
})
|
|
srv.Handle(trace.MsgSpanBatch, func(_ context.Context, _ string, m *zap.Message) (*zap.Message, error) {
|
|
mu.Lock()
|
|
payload = append([]byte(nil), m.Root().Bytes(0)...)
|
|
mu.Unlock()
|
|
select {
|
|
case done <- struct{}{}:
|
|
default:
|
|
}
|
|
return nil, nil
|
|
})
|
|
if err := srv.Start(); err != nil {
|
|
t.Fatalf("server: %v", err)
|
|
}
|
|
defer srv.Stop()
|
|
|
|
tracer, err := trace.New(trace.Config{
|
|
ExporterConfig: trace.ExporterConfig{
|
|
Type: trace.ZAP,
|
|
Endpoint: fmt.Sprintf("127.0.0.1:%d", port),
|
|
},
|
|
AppName: "trace-test",
|
|
Version: "v0.0.0",
|
|
TraceSampleRate: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("tracer: %v", err)
|
|
}
|
|
defer tracer.Close()
|
|
|
|
_, span := tracer.Start(context.Background(), "round-trip")
|
|
span.End()
|
|
|
|
// Force a flush by closing the tracer; the BatchSpanProcessor drains
|
|
// to the exporter on shutdown.
|
|
if err := tracer.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("timed out waiting for span batch")
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(payload) == 0 {
|
|
t.Fatal("received empty payload")
|
|
}
|
|
|
|
var batch struct {
|
|
AppName string `json:"appName"`
|
|
Spans []struct {
|
|
Name string `json:"name"`
|
|
} `json:"spans"`
|
|
}
|
|
if err := json.Unmarshal(payload, &batch); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if batch.AppName != "trace-test" {
|
|
t.Fatalf("appName: got %q want %q", batch.AppName, "trace-test")
|
|
}
|
|
if len(batch.Spans) != 1 || batch.Spans[0].Name != "round-trip" {
|
|
t.Fatalf("spans: %+v", batch.Spans)
|
|
}
|
|
}
|
|
|
|
func freePort(t *testing.T) int {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
if err := l.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
return port
|
|
}
|