Files
cairnobs/ingest/cmd/ingest/main.go
T
jcoffey-dev 7a86008062 Complete the low-risk half of the Sentry -> Cairn OBS rebrand
Sweeps the references that carry no runtime coupling, and fixes one that
turned out to be a real bug rather than stale branding.

Docker network: sentry_default -> cairnobs_default across 23 runbook and
test-header `docker run` commands. Compose derives the network from the
directory name, so this lands together with renaming the working copy to
cairnobs/ -- the two are only correct as one change.

Stale references corrected: four Dockerfile "repo root (sentry/)"
headers; .env pointing at the long-renamed deploy/helm/sentry/ chart;
five Helm comments describing the topic as sentry.logs.raw when all four
code paths have defaulted to cairnobs.logs.raw for some time; an
absolute /home/john/Projects/sentry/ path in the operator's package doc,
now repo-relative; the hand-written Tenant CRD description in both of
its identical copies, whose Go source already said Cairn OBS.

Migration 0043 repoints the default tenant's data source. 0026 seeded it
with ('sentry', '/var/lib/sentry-search') to match what
api/internal/config then defaulted to; the rebrand later moved those
defaults to "cairnobs" and /var/lib/cairnobs-search without moving the
already-applied row, leaving the default tenant naming a ClickHouse
database nothing writes to. Scoped to the exact stale values so it is a
no-op on any deployment that set them deliberately. 0026's comment is
annotated as superseded; its applied SQL is untouched.

Deliberately not included: the gRPC wire packages (sentry.logs.v1,
sentry.agent.v1) and proto/sentry/ import paths, which cannot change
without a lockstep agent/server upgrade; the Helm chart's
sentry_metadata database and sentry role, which need a real Postgres
migration on existing deployments; and the compliance audit records in
docs/compliance/, which are a dated historical record.

go build, go vet, and go test pass for ingest and deploy/operator.
2026-08-22 18:39:25 -07:00

147 lines
5.3 KiB
Go

// Command ingest is the Cairn OBS ingest service. It has two halves that can
// run in one process or be split across deployments via --mode:
//
// - server: mTLS gRPC front end that agents push batches to; forwards
// them onto Redpanda unchanged.
// - consumer: reads back off Redpanda, normalizes, batch-writes to
// ClickHouse.
// - all (default): both, in one process — the Phase 0 / docker-compose
// shape. Splitting into separate deployments later is a k8s manifest
// change, not a code change.
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/sync/errgroup"
"github.com/cairnobs/cairnobs/ingest/clickhousewriter"
"github.com/cairnobs/cairnobs/ingest/consumer"
"github.com/cairnobs/cairnobs/ingest/internal/agentregistry"
"github.com/cairnobs/cairnobs/ingest/internal/config"
"github.com/cairnobs/cairnobs/ingest/internal/grpcserver"
"github.com/cairnobs/cairnobs/ingest/internal/producer"
"github.com/cairnobs/cairnobs/ingest/internal/tenantresolver"
logsv1 "github.com/cairnobs/cairnobs/proto/sentry/logs/v1"
)
// singleTenantWriter adapts *clickhousewriter.Writer -- which only
// knows how to write to the one ClickHouse database it was constructed
// with -- to consumer.chWriter's tenant-tagged signature, by simply
// ignoring the tag. This is this binary's single-tenant behavior,
// unchanged from before per-tenant ingest credentials existed: every
// record lands in the same shared database regardless of which tenant
// (if any) it was resolved to. enterprise/cmd/enterprise-ingest is
// where a tag-respecting writer (enterprise/internal/chwriter.Registry)
// actually routes per tenant instead.
type singleTenantWriter struct {
w *clickhousewriter.Writer
}
func (s singleTenantWriter) WriteBatch(ctx context.Context, records []consumer.Record) error {
plain := make([]*logsv1.LogRecord, len(records))
for i, r := range records {
plain[i] = r.Record
}
return s.w.WriteBatch(ctx, plain)
}
func main() {
mode := flag.String("mode", "all", "which half of ingest to run: server | consumer | all")
flag.Parse()
if *mode != "server" && *mode != "consumer" && *mode != "all" {
fmt.Fprintf(os.Stderr, "unknown --mode %q, must be server|consumer|all\n", *mode)
os.Exit(1)
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
cfg, err := config.Load()
if err != nil {
logger.Error("loading config", "error", err)
os.Exit(1)
}
for _, w := range cfg.DevCredentialWarnings() {
logger.Warn(w)
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
g, ctx := errgroup.WithContext(ctx)
if *mode == "server" || *mode == "all" {
p := producer.New(cfg.Redpanda)
defer p.Close()
// resolver stays nil (every batch's tenant_id header is simply
// never set) unless ENTERPRISE_AUTH_URL is configured -- matches
// every other "off unless configured" optional dependency in
// this codebase.
var resolver grpcserver.TenantResolver
if cfg.EnterpriseAuthURL != "" {
resolver = tenantresolver.New(cfg.EnterpriseAuthURL)
logger.Info("ingest tenant resolution configured", "enterprise_auth_url", cfg.EnterpriseAuthURL)
} else {
logger.Info("ENTERPRISE_AUTH_URL not set -- ingest records carry no tenant_id, single-tenant behavior")
}
// agents stays nil (CheckIn always reports "no override," nothing
// recorded) unless AGENT_REGISTRY_POSTGRES_ADDR is configured --
// same "off unless configured" shape as resolver above. Uses its
// own pgxpool rather than sharing one across mode=server/consumer
// -- consumer's half of this binary has no Postgres dependency at
// all today and shouldn't gain one just because server's did.
var agents grpcserver.AgentRegistry
if cfg.AgentRegistry.Postgres.Addr != "" {
dsn := fmt.Sprintf("postgres://%s:%s@%s/%s",
cfg.AgentRegistry.Postgres.Username, cfg.AgentRegistry.Postgres.Password,
cfg.AgentRegistry.Postgres.Addr, cfg.AgentRegistry.Postgres.Database)
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
logger.Error("opening agent registry postgres pool", "error", err)
os.Exit(1)
}
defer pool.Close()
agents = agentregistry.New(pool)
logger.Info("agent registry configured", "postgres_addr", cfg.AgentRegistry.Postgres.Addr)
} else {
logger.Info("AGENT_REGISTRY_POSTGRES_ADDR not set -- agent check-ins are accepted but not recorded, no remote config")
}
srv := grpcserver.New(logger, cfg.GRPC, cfg.TLS, p, resolver, agents)
g.Go(func() error { return srv.Run(ctx) })
}
if *mode == "consumer" || *mode == "all" {
chw, err := clickhousewriter.New(ctx, clickhousewriter.Config{
Addr: cfg.ClickHouse.Addr, Database: cfg.ClickHouse.Database,
Username: cfg.ClickHouse.Username, Password: cfg.ClickHouse.Password,
})
if err != nil {
logger.Error("connecting to clickhouse", "error", err)
os.Exit(1)
}
defer chw.Close()
c := consumer.New(logger, consumer.Config{
Brokers: cfg.Redpanda.Brokers, Topic: cfg.Redpanda.Topic, ConsumerGroup: cfg.Redpanda.ConsumerGroup,
BatchMaxSize: cfg.Batch.MaxSize, FlushIntervalMS: cfg.Batch.FlushIntervalMS,
}, singleTenantWriter{w: chw})
g.Go(func() error { return c.Run(ctx) })
}
logger.Info("ingest started", "mode", *mode)
if err := g.Wait(); err != nil {
logger.Error("ingest exited with error", "error", err)
os.Exit(1)
}
}