Files
cameleer/docs/superpowers/specs/2026-04-14-simplified-log-forwarding-design.md
hsiegeln 0d0d4ab849
All checks were successful
CI / build (push) Successful in 4m52s
CI / docker (push) Successful in 40s
CI / deploy (push) Successful in 26s
SonarQube Analysis / sonarqube (push) Successful in 6m31s
Nightly Soak Test / soak (push) Successful in 36m33s
Camel Version Compatibility / compat (4.0.6) (push) Successful in 1m57s
Camel Version Compatibility / compat (4.1.0) (push) Successful in 1m43s
Camel Version Compatibility / compat (4.10.9) (push) Successful in 1m56s
Camel Version Compatibility / compat (4.11.0) (push) Successful in 1m50s
Camel Version Compatibility / compat (4.12.0) (push) Successful in 1m43s
Camel Version Compatibility / compat (4.13.0) (push) Successful in 1m46s
Camel Version Compatibility / compat (4.14.5) (push) Successful in 1m46s
Camel Version Compatibility / compat (4.15.0) (push) Successful in 1m46s
Camel Version Compatibility / compat (4.16.0) (push) Successful in 1m45s
Camel Version Compatibility / compat (4.17.0) (push) Successful in 1m46s
Camel Version Compatibility / compat (4.18.1) (push) Successful in 1m45s
Camel Version Compatibility / compat (4.2.0) (push) Successful in 1m44s
Camel Version Compatibility / compat (4.3.0) (push) Successful in 1m47s
Camel Version Compatibility / compat (4.4.5) (push) Successful in 1m46s
Camel Version Compatibility / compat (4.5.0) (push) Successful in 1m45s
Camel Version Compatibility / compat (4.6.0) (push) Successful in 1m46s
Camel Version Compatibility / compat (4.7.0) (push) Successful in 1m46s
Camel Version Compatibility / compat (4.8.9) (push) Successful in 1m42s
Camel Version Compatibility / quarkus-compat (3.11.0) (push) Successful in 1m22s
Camel Version Compatibility / compat (4.9.0) (push) Successful in 2m29s
Camel Version Compatibility / quarkus-compat (3.12.0) (push) Successful in 1m13s
Camel Version Compatibility / quarkus-compat (3.13.0) (push) Successful in 2m8s
Camel Version Compatibility / quarkus-compat (3.14.0) (push) Successful in 1m47s
Camel Version Compatibility / quarkus-compat (3.15.0) (push) Successful in 1m34s
Camel Version Compatibility / quarkus-compat (3.16.0) (push) Successful in 1m46s
Camel Version Compatibility / quarkus-compat (3.17.0) (push) Successful in 1m36s
Camel Version Compatibility / quarkus-compat (3.18.0) (push) Successful in 2m12s
Camel Version Compatibility / quarkus-compat (3.19.0) (push) Successful in 2m1s
Camel Version Compatibility / quarkus-compat (3.20.0) (push) Successful in 1m26s
Camel Version Compatibility / quarkus-compat (3.22.0) (push) Successful in 1m38s
Camel Version Compatibility / quarkus-compat (3.23.0) (push) Successful in 1m26s
Camel Version Compatibility / quarkus-compat (3.24.0) (push) Successful in 1m51s
Camel Version Compatibility / quarkus-compat (3.25.0) (push) Successful in 1m34s
Camel Version Compatibility / quarkus-compat (3.26.0) (push) Successful in 1m43s
Camel Version Compatibility / quarkus-compat (3.27.0) (push) Successful in 1m29s
Camel Version Compatibility / quarkus-compat (3.29.0) (push) Successful in 1m36s
Camel Version Compatibility / quarkus-compat (3.30.0) (push) Successful in 1m47s
Camel Version Compatibility / quarkus-compat (3.31.0) (push) Successful in 1m46s
Camel Version Compatibility / quarkus-compat (3.32.0) (push) Successful in 1m37s
Camel Version Compatibility / quarkus-compat (3.33.0) (push) Successful in 1m7s
Camel Version Compatibility / report (push) Successful in 7s
refactor: simplify log forwarding with early server connection in preInstall
Move server connection to preInstall/configure (before routes start) so
the log appender gets a live exporter immediately — eliminates the
three-layer buffering chain (BridgeAccess early buffer → LogForwarder
deferred exporter → ChunkedExporter).

Key changes:
- New ServerSetup.earlyConnect() for minimal pre-route registration
- New ServerSetup.setupLogForwarding() shared by agent and extension
- Unified appender registration path for all frameworks
- Remove BridgeAccess early buffer (silent drop if no handler)
- Remove LogForwarder no-arg constructor (exporter always required)
- Delete SpringContextAdvice, SpringContextTransformer, LogForwardingConsumer
- Remove Spring AbstractApplicationContext transformer from premain()
- Update CameleerConfigAdapter to remove early log forwarder setup
- Pre-agent logs (JVM bootstrap) deferred to server team via infra

Net reduction: ~126 lines. All 258 tests pass across 15 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:43:47 +02:00

8.3 KiB

Simplified Log Forwarding Architecture

Date: 2026-04-14 Status: Approved (conversation design review)

Problem

The current log forwarding has three buffering layers (BridgeAccess early buffer → LogForwarder deferred exporter → ChunkedExporter) to cover the timing gap between appender registration and server connection. This creates:

  • Framework-specific early hooks: SpringContextAdvice hooks AbstractApplicationContext.refresh() for Spring apps; CameleerHookInstaller.preInstall() has a fallback path for non-Spring apps
  • BridgeAccess early buffer (5000 entries) with synchronized drain logic and classloader reflection
  • LogForwarder deferred exporter pattern (no-arg constructor, setExporter() called later)
  • LogForwardingConsumer wrapper class to avoid ByteBuddy lambda inlining issues
  • Cross-classloader complexity in SpringContextAdvice (loads LogForwarder on system CL via reflection)

Despite this complexity, bootstrap failures before CamelContext.start() are still not captured — the server connection only happens in postInstall, so if the app dies during Spring context initialization, buffered logs die with the process.

Decision

  1. Move server connection to preInstall — connect before routes start, so the exporter is live when the appender is registered. No deferred pattern needed.
  2. External infrastructure handles pre-agent logs — Docker/K8s logging drivers, DaemonSet shippers, or kubelet events capture container lifecycle before the JVM starts. The server team will implement this.
  3. Remove all early buffering machinery — BridgeAccess buffer, deferred exporter, SpringContextAdvice log code, LogForwardingConsumer.
  4. Unified appender registration path — same code for Spring Boot, Quarkus, and Plain Java, executed in preInstall/configure.

Why preInstall instead of premain?

The original conversation explored moving the server connection to premain(). However, the agent uses an AgentClassLoader (child-first) that creates a separate class namespace from the system classloader. Objects created in premain() (system CL) cannot be passed to CameleerHookInstaller (AgentClassLoader) without complex cross-classloader reflection using only primitives. Since preInstall already fires before any route processes exchanges (it runs at the start of CamelContext.start()), it meets the requirement of "fully initialized before the first route executes" without the classloader complexity.

Architecture

Three-Phase Log Coverage Model

Phase Coverage Mechanism
Pre-JVM (container fails before Java runs) Server team / infra kubelet events, Docker logging drivers, DaemonSet shipper
JVM start → preInstall (framework bootstrap) Not captured by agent Infra-level capture; acceptable gap
preInstall onward (routes executing) Agent In-agent appender with structured log forwarding

New Startup Sequence (Agent Mode)

premain()
  → install CamelContext transformer (existing)
  → install SendDynamicAware transformer (existing)
  → [REMOVED: Spring AbstractApplicationContext transformer]

CamelContext.start() intercepted → preInstall()
  → install InterceptStrategy (existing)
  → earlyConnect: ServerConnection + register (minimal) + ChunkedExporter
  → register log appender on root logger (unified, all frameworks)
  → create LogForwarder WITH live exporter
  → set bridge handler on LogEventBridge
  → ✅ log forwarding LIVE before any route processes an exchange

CamelContext.start() completes → postInstall()
  → create EventNotifier (existing)
  → PostStartSetup: diagrams, metrics, re-register with full info, heartbeat, SSE

New Startup Sequence (Extension Mode)

@PostConstruct (CameleerConfigAdapter)
  → set system properties from Quarkus config
  → [REMOVED: appender registration and earlyLogForwarder]

configure() (CameleerLifecycle - before CamelContext.start())
  → earlyConnect: ServerConnection + register (minimal) + ChunkedExporter
  → create collector + InterceptStrategy + EventNotifier (existing)
  → register log appender on root logger
  → create LogForwarder WITH live exporter
  → set bridge handler
  → ✅ log forwarding LIVE

CamelContextStartedEvent
  → PostStartSetup: diagrams, metrics, re-register, heartbeat, SSE

Server-Side Cutover

The logForwarding capability in the registration/heartbeat capabilities map signals when agent log forwarding is active. The server team can use this to stop ingesting infra-level logs for that instance, avoiding duplicates.

Changes

New: ServerSetup.earlyConnect(CameleerAgentConfig)

Minimal server registration before routes start. Returns EarlyConnection(ServerConnection, ChunkedExporter) or null on failure.

  • Creates ServerConnection with endpoint + auth token
  • Registers with: instanceId, applicationId, environmentId, version, empty routeIds, minimal capabilities
  • Creates ChunkedExporter wired to the connection
  • On failure: logs warning, returns null (agent falls back to LogExporter)

New: ServerSetup.setupLogForwarding(config, exporter, appClassLoader)

Shared method for both agent and extension to register the appender and create a LogForwarder in one call.

Modified: ServerSetup.connect(ConnectionContext)

  • ConnectionContext gains an earlyConnection field (replaces earlyLogForwarder)
  • If earlyConnection is present: reuses ServerConnection and ChunkedExporter, re-registers with full route IDs and capabilities
  • If absent: full connection as before (backward compat for edge cases)
  • installLogForwarding() removed (log forwarding already set up in preInstall)

Modified: PostStartSetup.Context

  • earlyLogForwarder field replaced by earlyConnection (ServerSetup.EarlyConnection)
  • logForwarder passed separately (already created in preInstall)

Modified: LogForwarder

  • No-arg constructor removed
  • Constructor always requires a non-null Exporter
  • setExporter() kept for one case: early connect failed (LogExporter), late connect succeeded (swap to ChunkedExporter)
  • Internal queue and flush scheduler unchanged

Simplified: BridgeAccess

  • earlyBuffer (ConcurrentLinkedQueue) removed
  • drainBuffer() removed
  • MAX_BUFFER_SIZE removed
  • forward() calls handler directly; if no handler, entry is silently dropped
  • setHandler(), getHandler(), resolveField() unchanged
  • Test utility methods updated

Removed: SpringContextAdvice

  • Entire class deleted (only purpose was early log registration)

Removed: LogForwardingConsumer

  • Entire class deleted (was only needed by SpringContextAdvice)

Removed: SpringContextTransformer

  • Entire class deleted (ByteBuddy transformer for SpringContextAdvice)

Modified: CameleerAgent.premain()

  • Remove AbstractApplicationContext transformer installation (lines 67-72)

Modified: CameleerHookInstaller

  • preInstall(): calls earlyConnect(), registers appender, creates LogForwarder with live exporter, stores EarlyConnection for postInstall
  • postInstall(): no longer picks up SpringContextAdvice.earlyLogForwarder; passes EarlyConnection + logForwarder to PostStartSetup

Modified: CameleerConfigAdapter (Extension)

  • Remove earlyLogForwarder field and getEarlyLogForwarder()
  • Remove appender registration from @PostConstruct

Modified: CameleerLifecycle (Extension)

  • configure(): calls earlyConnect(), registers appender, creates LogForwarder
  • onCamelContextStarted(): passes EarlyConnection + logForwarder to PostStartSetup

Test Changes

  • LogForwarderTest: remove deferredExporter_buffersUntilExporterSet test (deferred pattern no longer exists)
  • BridgeAccessTest: remove buffer tests (forward_buffersWhenNoHandler, forward_drainsBufferWhenHandlerSet, forward_bufferCapped); add test for silent drop when no handler
  • Remaining tests unchanged (they already pass exporter to constructor)

Constraints

  • Must not steal the customer's log stream — appender adds alongside existing appenders, never replaces
  • Must work with Datadog, Elastic, and other APM agents that also add appenders
  • Extension path (Quarkus native, no agent) remains separate but follows the same simplified pattern
  • If server is unreachable in preInstall, graceful fallback to LogExporter; postInstall retries connection