feat(action-log-sink): port ActionLogStreamSink to Go (ADR-0045 Phase B)#66
Merged
Conversation
Phase B of ADR-0045 replaces the Scala `com.openfoundry.audit.ActionLogStreamSink`
(Spark Structured Streaming) with a new Go service `action-log-sink`
that consumes `ontology.actions.applied.v1` Kafka topic and appends
each batch to `lakekeeper.default.action_log` Iceberg table via the
OpenFoundry Iceberg HTTP append adapter served by
iceberg-catalog-service. Same topic, table, and 16-column schema as
the Scala original.
Architecture mirrors services/audit-sink and services/ai-sink:
- internal/envelope decodes + validates the wire format (15 required
+ optional fields from libs/ontology-kernel side_effects.go).
- internal/writer wraps the HTTP append adapter and adds a JSONL
dev fallback selected via ACTION_LOG_SINK_JSONL_PATH.
- internal/runtime runs the Kafka -> batch -> Writer.Append ->
CommitMessages loop. At-least-once semantics: offsets advance only
after a successful Iceberg append. Poison records are committed
alongside the next successful flush to avoid head-of-line blocking.
- internal/server exposes /healthz + /metrics on a service-local
Prometheus registry (action_log_sink_lag_seconds / records_total /
batch_size_records / commits_total{outcome}).
The dev infra/dev/action-log-sink.yaml SparkApplication CR is
rewritten as an apps/v1 Deployment + ClusterIP Service with
terminationGracePeriodSeconds=90 so the final flush completes before
SIGKILL. Rolling update is configured with maxSurge=0 to prevent two
consumers in the same group racing partitions during a deploy.
Note vs the ADR-0045 text: Phase B description says "append via
iceberg-go writeBuilder" but the established repo pattern (audit-sink,
ai-sink) is the HTTP adapter; iceberg-go's write-side is not stable
enough today. The Iceberg write path will be re-evaluated when
iceberg-go upstream catches up (tracked alongside Phase A's
gocloud/S3-signing pins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase B of ADR-0045 — new Go service
services/action-log-sink/that replaces the Scalacom.openfoundry.audit.ActionLogStreamSink(Spark Structured Streaming) shipped inservices/pipeline-runner-spark/. Consumesontology.actions.applied.v1Kafka topic and appends batches to the Iceberglakekeeper.default.action_logtable via the OpenFoundry Iceberg HTTP append adapter (POST /openfoundry/iceberg/v1/append, served byservices/iceberg-catalog-service).The dev
SparkApplicationCR atinfra/dev/action-log-sink.yamlis rewritten as anapps/v1 Deployment+ClusterIP ServicewithterminationGracePeriodSeconds=90and a rolling strategy ofmaxSurge=0to prevent two consumers in the same group racing partitions during a deploy.Architecture
Mirrors the established
services/audit-sink/services/ai-sinkpattern:internal/envelopelibs/ontology-kernel/handlers/actions/side_effects.go::publishActionAuditToKafka).Decodereturns typed*DecodeError/*ValidateError;IsPoisonclassifies them.internal/writerWriterinterface +IcebergWriter(HTTP adapter) +JSONLWriter(dev fallback selected viaACTION_LOG_SINK_JSONL_PATH). Posts the canonicalTableSpecshape with 16-column schema, partitionday(applied_at_ms), sortapplied_at_ms ASC.internal/runtimeWriter.Append→CommitMessagesloop. At-least-once: offsets advance only after a successful append. Poison records carry their offset commit alongside the next good flush so the consumer does not head-of-line-block.internal/server/healthz+/metricson a service-local Prometheus registry.cmd/action-log-sink/main.goAt-least-once + dedup contract
Documented in
services/action-log-sink/README.mdand in the new ADR section is the deliberate downgrade from Spark Structured Streaming exactly-once to at-least-once with downstreamDISTINCT event_iddedup.event_idis the producer-generated UUID, immutable across replays.Any downstream query on
action_logthat aggregates withoutDISTINCT event_id(e.g. row-count metrics, audit-trail counters) must be reviewed before this sink ships to production. The README flags this explicitly.Test plan
go build ./...from the worktree (full repo)go vet ./...(full repo)go test -race ./services/action-log-sink/...(4 packages, ~14 sub-tests)envelope: happy decode, invalid JSON, every required field missing,applied_at_ms=0rejected.writer: 16-column body shape againsthttptest.Server, 404 →ErrTableNotFound, 409/422 →ErrSchemaMismatch, 5xx →ErrCommitFailed, empty-batch guard.config: env-var parsing happy path, missing required, JSONL skips catalog requirement, invalid integer.runtime: flush-by-MaxRecords with offset commits, poison records committed but not appended, writer error preserves batch (no commit), ctx-cancel triggers final flush.docker pushparallelism issue as Phase A, plus broader cluster stability work tracked separately). Validation steps once the cluster is healthy:Deviation from ADR-0045 text
ADR § Decision point 5 reads "append via
apache/iceberg-gowriteBuilder against the Iceberg catalog". The established repo pattern (audit-sink, ai-sink) is the HTTP append adapter because iceberg-go's write-side is not stable end-to-end (documented in both sinks' READMEs). This PR follows the repo pattern, not the ADR text. Phase A already had to pinsubstrait-protobufdown to ship the read path of iceberg-go; the write path is in worse shape. Re-evaluation tracked as a follow-up when iceberg-go upstream catches up.Follow-ups (out of scope)
services/pipeline-runner-spark/, Spark Operator chart, related docs.🤖 Generated with Claude Code