Audit-logging pipeline: REST → broker → pipelines → transformer → sink. Polyglot monorepo (Java backend + Python transformer/sink).
For codebase questions, run graphify query "<question>" when graphify-out/graph.json exists. Use graphify path for relationships and graphify explain for concepts. After code changes, run graphify update ..
| Path | Service | Stack | Port |
|---|---|---|---|
auditflow-be/ |
Backend | Java 25, Spring Boot 4.1, Maven | 8080 |
auditflow-transformer/ |
Transformer | Python 3.13, FastAPI | 8081 |
auditflow-sink/ |
Sink | Python 3.13, FastAPI | 8082 |
auditflow-api/ |
API contract + client (shared, validated models) | Java 17, Maven | n/a |
Root files: justfile (task runner), docker-compose.yml (local stack), docker-compose-observability.yml (observability overlay via just up obs).
POST /audit/publish (direct; via gateway: /auditflow/api/v1/audit/publish — Traefik owns and strips the version prefix)
→ AuditPublisherService → RabbitMQ topic
→ AuditSubscriberService → AuditService.processAuditEvent()
→ for each enabled pipeline:
TransformationService → POST http://transformer:8081/transform/{name}
SinkService → POST http://sink:8082/sink/{name}
- AuditFlow is a router/pipeline, NOT a system of record — settled decision, do not reopen. It has no database of its own: no Postgres, no event store. Do not propose or add one. AuditFlow ingests → redacts → routes → delivers; the configured sinks (OpenSearch, S3/GCS/Azure Blob, Splunk, Snowflake, Database, …) are the systems of record and own persistence/retention/query. Durability = broker buffering + retry/circuit-breaker + DLQ (
/actuator/dlq, replayable) + the sink. Redis is dedup-only (idempotency TTL), not storage. "Store audit events in AuditFlow" is out of scope by design — point a pipeline at a durable sink instead. - Backend is both producer and consumer of the same topic.
- Pipelines are independent — one failing does not stop others.
- Consumer uses dead-letter queue (
autoBindDlq: true).
Multi-tenant by construction: every pipeline belongs to exactly one tenant, and events route only through the pipeline set owned by the event's tenant (TenantPipelineRegistry — no global list, no fall-through). Tenantless events belong to the reserved _platform pseudo-tenant.
- Tenant config source (
tenants.source.mode):local-dir(built-in default —<tenantId>.yamlfiles in a mounted dir,tenants.source.local-dir.path, 5s poll; compose mounts./tenants) orgitops-configmap(explicit; ConfigMaps labelledauditflow.io/tenant, fabric8 watch — set by the Helm chart). - Onboarding = add
tenants/<tenantId>.yaml(local) or a labelled ConfigMap (k8s):tenantId,enabled,quota(rateLimitPerSec/burst),pipelines(same shape as before). Picked up live, no restart. Offboarding = remove it; in-flight events then quarantine (TENANT_UNRESOLVED). - Ingest gate (
TenantGate, atPOST /audit/publish): unprovisioned →403 TENANT_NOT_PROVISIONED, disabled →403 TENANT_DISABLED, over per-tenant quota →429 TENANT_RATE_LIMITED+Retry-After. Rate-limit backend:tenants.ratelimit.backend=in-memory(default, single replica) orredis(multi-replica, Lua token bucket; Helm sets it). - Legacy
auditflow.pipelinesfails startup by design — move pipelines intotenants/_platform.yaml. - Sink credentials:
${secretRef:<key>}in sink properties, resolved at delivery from the tenant's own store —secretRef.resolver=env(default,AUDITFLOW_TENANT_<ID>_<KEY>) ork8s-secret(Secretauditflow-tenant-<id>-creds). Missing key ⇒ retryable failure → DLQ, never a blank or another tenant's credential. - Tenant-scoped DLQ:
/actuator/dlq/<tenantId>(inspect) and POST to replay — only that tenant's messages are touched; there is no un-scoped DLQ operation. - Telemetry:
auditflow.tenant.events{tenant,provider,outcome}counter (outcomes: routed/delivered/quarantined/rejected:*) + Grafanatenantvariable in the overview dashboard. - Core stays a router: it is read-only on tenant config in every profile and still has no database (see the settled decision above).
- OpenAPI-first: canonical spec at
auditflow-api/src/main/resources/openapi/openapi-audit-v1.yaml. Never edit generated Java undertarget/. - Pipelines are configuration, not code — per tenant (see "Tenant model" above). Each = name, enabled, condition, transformer.name, sink.name, sink.properties.
- Conditions:
ConditionEvaluatorwith operatorseq, neq, contains, startsWith, endsWith, in, notIn, exists, notExists, regex, gt, gte, lt, lte, eqIgnoreCase. Field paths support dot notation. - Service discovery: pluggable via
*.discovery.mode(local|kubernetes). - HTTP to Python: reactive
WebClient(5s connect / 10s response), cached per base URL. - Cross-cutting:
CorrelationIdFilter,GlobalExceptionHandler,JacksonConfig,OpenAPIConfig.
Both use the same plugin pattern — keep symmetric when editing one.
POST /transform/{id}or/sink/{id}→importlib.import_module(id)→ call function.- Transformer:
transform(input_data: dict) -> dict - Sink:
process(event_data: dict, properties: dict) -> dict - ID validated against
^[a-zA-Z0-9_]+$— keep this regex consistent with JavaTransformationService/SinkService. - Module resolution:
transformers//sinks/(shipped),transformers_bootstrap//sinks_bootstrap/(mounted at runtime, git-ignored). GET /registrylists available modules (also Docker healthcheck).telemetry.py= business telemetry abstraction (no OpenTelemetry imports in app code; auto-instrumentation viaentrypoint.shwhenOTEL_EXPORTER_OTLP_ENDPOINTis set).
- Drop
myname.pyintotransformers/orsinks/implementing the required function. - Add Python packages to
requirements.txtif needed. - Reference from pipeline:
transformer.name: myname/sink.name: myname. - No backend code change needed — name resolved dynamically.
just up # build + start stack
just up obs # stack + OTel + Tempo + Loki + Prometheus + Grafana
just log sink # tail a service (backend|transformer|sink|rabbitmq)
just down # stop | just clean = also remove volumesPer-service:
mvn -B clean package -DskipTests --file auditflow-be/pom.xml # build JAR
mvn -B verify --file auditflow-be/pom.xml # test backend
cd auditflow-transformer && just run-local # uvicorn :8081
cd auditflow-sink && just run-local # uvicorn :8082Local URLs: Swagger :8080/swagger-ui.html, transformer/sink :8081/docs / :8082/docs, RabbitMQ :15673 (guest/guest), Grafana :3000 (admin/admin).
- Java 25, Maven 3.6.3+ enforced by maven-enforcer-plugin.
timestampis server-assigned (readOnly) — set in controller, never trust client input.- Credentials from env vars only —
RABBITMQ_USERNAME/RABBITMQ_PASSWORDhave no defaults. - Backend tests: JUnit 5 + Spring Boot Test in
auditflow-be/src/test/java/. - Python tests: pytest in
tests/directories, run viajust test-transformer/just test-sink. - Logging: SLF4J/Logback with logstash JSON encoder to stdout;
io.labs64at DEBUG. trace_id/span_id MDC populated by the OTel Java Agent when observability is enabled.
| Goal | Where |
|---|---|
| API contract | auditflow-api/src/main/resources/openapi/openapi-audit-v1.yaml |
| Java client | auditflow-api/src/main/java/io/labs64/auditflow/client/ |
| Pipeline config | per-tenant tenants/<tenantId>.yaml (local-dir; compose mounts ./tenants) or labelled ConfigMap (k8s) — legacy global auditflow.pipelines fails startup |
| Tenant model (registry/gate/providers) | auditflow-be/src/main/java/io/labs64/audit/tenant/ |
| Condition operator | auditflow-be/.../service/ConditionEvaluator.java |
| Add transformer | auditflow-transformer/transformers/<name>.py |
| Add sink | auditflow-sink/sinks/<name>.py |
| OTel Collector config | observability/otel-collector/config.yaml |
| Grafana dashboard | observability/grafana/dashboards/*.json |
| Observability toggle (local) | docker-compose-observability.yml (just up obs) — env-only, no rebuild |
| Business telemetry | auditflow-be src/main/java/io/labs64/audit/telemetry/, transformer/sink telemetry.py |