Files
trace/zap_native_test.go
Hanzo AI b78c90d90a exporter: Type=ZAP is now ZAP-native by default (no protobuf, no OTLP)
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)
2026-05-21 00:44:42 -07:00

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
}