mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
hanzoai/o11y v1.5.41 shipped pkg/zapingest defaulting Enabled=true with 0.0.0.0:4317-4319 baked in, so importing the module SEIZED three well-known ports as a side effect of being linked. hanzoai/cloud embeds o11y and runs its own ingest on the same three ports in the same process; whichever bound first won, the loser dropped telemetry silently, and log ingest fell ~200x. The chart's O11Y_ZAPINGEST_ENABLED=false is a tourniquet on that default, and a flag that must be set correctly in every chart is a third way to get it wrong. The address IS the switch. `Enabled` is deleted: a boolean beside three addresses is a second way to state one decision, and the two can disagree — Validate() already had to reconcile them. Stating where spans arrive IS the act of deciding that spans arrive here. Every address now defaults to EMPTY, so linking this package binds nothing and the race cannot be reached by default. A signal with no address is simply not received; the conventional ports (4317/4318/4319) are documented rather than defaulted, so the deployment that publishes them is the one that chooses them. A collision is now LOUD instead of lost. holderOf() reads procfs to name the process holding the port, and says so explicitly when that process is THIS one — which is the actual production shape, two implementations in one binary, and the case where the answer is "delete one" rather than "retry" or "move the port". The error is returned (never swallowed) and logged, and a partial bind is rolled back: a half-bound edge is worse than none, because it reports partial traffic behind a healthy-looking surface. The diagnostic is built into the CAUSE, not passed to Wrap*f. errors.base.Error() returns the wrapped cause's text and DROPS the wrapper's own message, so a message handed to Wrap*f is retained on the value and never printed by anything rendering err.Error(). The first draft of this fix hit exactly that: a loud message that was invisible. Folding it into the cause makes it print while %w keeps errors.Is(err, syscall.EADDRINUSE) working. Removing the field is safe for consumers still setting the env: pkg/config's decoder does not set ErrorUnused, so a stale O11Y_ZAPINGEST_ENABLED is ignored rather than fatal. Tests cover the load-bearing default (binds nothing), signal selection, validation, holder identification, and that the collision message names this process. Co-authored-by: Hanzo Dev <[email protected]>
173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
package zapingest
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/o11y/pkg/factory"
|
|
)
|
|
|
|
// The default must bind NOTHING. This is the regression that caused the
|
|
// incident: a linked-in library that seizes :4317-:4319 as a side effect of
|
|
// being imported races the host for the same sockets.
|
|
func TestDefaultConfigBindsNothing(t *testing.T) {
|
|
c, ok := newConfig().(Config)
|
|
if !ok {
|
|
t.Fatalf("newConfig() is not a Config")
|
|
}
|
|
if got := c.signals(); len(got) != 0 {
|
|
t.Fatalf("default config receives %v; a linked library must take no port", got)
|
|
}
|
|
for name, addr := range map[string]string{"spans": c.Spans, "logs": c.Logs, "metrics": c.Metrics} {
|
|
if addr != "" {
|
|
t.Errorf("default %s address is %q; want empty", name, addr)
|
|
}
|
|
}
|
|
if err := c.Validate(); err != nil {
|
|
t.Errorf("default config must validate, got %v", err)
|
|
}
|
|
}
|
|
|
|
// A Service on the default config starts, binds nothing, and stops cleanly —
|
|
// the shape every process that merely links o11y gets.
|
|
func TestStartWithNoAddressBindsNothing(t *testing.T) {
|
|
s := New(settings(), Config{}, nil) // nil store: never dereferenced when nothing binds
|
|
done := make(chan error, 1)
|
|
go func() { done <- s.Start(context.Background()) }()
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
if len(s.started) != 0 {
|
|
t.Fatalf("started %d receivers on an empty config", len(s.started))
|
|
}
|
|
if err := s.Stop(context.Background()); err != nil {
|
|
t.Fatalf("Stop: %v", err)
|
|
}
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("Start returned %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Start did not return after Stop")
|
|
}
|
|
}
|
|
|
|
func TestSignalsOmitsUnconfigured(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
cfg Config
|
|
want []string
|
|
}{
|
|
{"none", Config{}, nil},
|
|
{"logs only", Config{Logs: ":4318"}, []string{"logs"}},
|
|
{"spans+metrics", Config{Spans: ":4317", Metrics: ":4319"}, []string{"spans", "metrics"}},
|
|
{"all", Config{Spans: ":4317", Logs: ":4318", Metrics: ":4319"}, []string{"spans", "logs", "metrics"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var got []string
|
|
for _, s := range tc.cfg.signals() {
|
|
got = append(got, s.name)
|
|
}
|
|
if strings.Join(got, ",") != strings.Join(tc.want, ",") {
|
|
t.Fatalf("signals()=%v want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
cfg Config
|
|
wantErr string
|
|
}{
|
|
{"empty ok", Config{}, ""},
|
|
{"partial ok", Config{Logs: "0.0.0.0:4318"}, ""},
|
|
{"bad addr", Config{Spans: "4317"}, "not host:port"},
|
|
{"duplicate", Config{Spans: "0.0.0.0:4317", Logs: "0.0.0.0:4317"}, "both use"},
|
|
// An unconfigured signal must not collide with another unconfigured
|
|
// one: three empty strings are equal but none of them is an address.
|
|
{"two empty are not a collision", Config{Spans: "0.0.0.0:4317"}, ""},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.cfg.Validate()
|
|
if tc.wantErr == "" {
|
|
if err != nil {
|
|
t.Fatalf("want ok, got %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Fatalf("want error containing %q, got %v", tc.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A bind collision must fail fast AND name the holder. Here the holder is this
|
|
// test process itself — the same shape as two ingest implementations linked
|
|
// into one binary, which is the case the message must call out by name.
|
|
func TestBindCollisionNamesTheHolder(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
addr := ln.Addr().String()
|
|
|
|
s := New(settings(), Config{Logs: addr}, nil)
|
|
sig := signal{name: "logs", addr: addr}
|
|
err = s.bindError(fmt.Errorf("zaplogreceiver: zap.Node start: listen tcp %s: bind: %w", addr, syscall.EADDRINUSE), sig)
|
|
if err == nil {
|
|
t.Fatal("bindError returned nil for an in-use address")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range []string{addr, "logs", "One ingest edge, not two"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("bind error missing %q:\n%s", want, msg)
|
|
}
|
|
}
|
|
// procfs is how the holder is identified; where it exists, the holder is us.
|
|
if _, statErr := os.Stat("/proc/net/tcp"); statErr == nil {
|
|
if !strings.Contains(msg, fmt.Sprintf("pid %d", os.Getpid())) {
|
|
t.Errorf("holder should be this process (pid %d):\n%s", os.Getpid(), msg)
|
|
}
|
|
if !strings.Contains(msg, "THIS SAME PROCESS") {
|
|
t.Errorf("an in-process collision must say so:\n%s", msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// holderOf finds the process listening on an address, and reports nothing for
|
|
// an address nobody holds.
|
|
func TestHolderOf(t *testing.T) {
|
|
if _, err := os.Stat("/proc/net/tcp"); err != nil {
|
|
t.Skip("no procfs")
|
|
}
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
addr := ln.Addr().String()
|
|
if got := holderOf(addr); !strings.Contains(got, fmt.Sprintf("pid %d", os.Getpid())) {
|
|
t.Errorf("holderOf(%s)=%q, want this pid", addr, got)
|
|
}
|
|
ln.Close()
|
|
// Give the socket a moment to leave LISTEN.
|
|
time.Sleep(50 * time.Millisecond)
|
|
if got := holderOf(addr); got != "" {
|
|
t.Errorf("holderOf(%s) after close = %q, want empty", addr, got)
|
|
}
|
|
}
|
|
|
|
func settings() factory.ProviderSettings {
|
|
return factory.ProviderSettings{Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))}
|
|
}
|