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>
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:
SpringContextAdvicehooksAbstractApplicationContext.refresh()for Spring apps;CameleerHookInstaller.preInstall()has a fallback path for non-Spring apps BridgeAccessearly buffer (5000 entries) with synchronized drain logic and classloader reflectionLogForwarderdeferred exporter pattern (no-arg constructor,setExporter()called later)LogForwardingConsumerwrapper 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
- Move server connection to
preInstall— connect before routes start, so the exporter is live when the appender is registered. No deferred pattern needed. - 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.
- Remove all early buffering machinery — BridgeAccess buffer, deferred exporter, SpringContextAdvice log code, LogForwardingConsumer.
- 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
ServerConnectionwith endpoint + auth token - Registers with: instanceId, applicationId, environmentId, version, empty routeIds, minimal capabilities
- Creates
ChunkedExporterwired 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)
ConnectionContextgains anearlyConnectionfield (replacesearlyLogForwarder)- If
earlyConnectionis present: reusesServerConnectionandChunkedExporter, 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
earlyLogForwarderfield replaced byearlyConnection(ServerSetup.EarlyConnection)logForwarderpassed 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) removeddrainBuffer()removedMAX_BUFFER_SIZEremovedforward()calls handler directly; if no handler, entry is silently droppedsetHandler(),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
AbstractApplicationContexttransformer installation (lines 67-72)
Modified: CameleerHookInstaller
preInstall(): callsearlyConnect(), registers appender, createsLogForwarderwith live exporter, storesEarlyConnectionfor postInstallpostInstall(): no longer picks upSpringContextAdvice.earlyLogForwarder; passesEarlyConnection+logForwardertoPostStartSetup
Modified: CameleerConfigAdapter (Extension)
- Remove
earlyLogForwarderfield andgetEarlyLogForwarder() - Remove appender registration from
@PostConstruct
Modified: CameleerLifecycle (Extension)
configure(): callsearlyConnect(), registers appender, createsLogForwarderonCamelContextStarted(): passesEarlyConnection+logForwardertoPostStartSetup
Test Changes
LogForwarderTest: removedeferredExporter_buffersUntilExporterSettest (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