Rename Java packages from com.cameleer3 to com.cameleer, module directories from cameleer3-* to cameleer-*, and all references throughout workflows, Dockerfiles, docs, and pom.xml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
36 KiB
Early Log Capture Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Capture application logs from Spring's ApplicationContext.refresh() onward (instead of after server connection) by buffering early entries and flushing them when the server connects.
Architecture: Three layers — BridgeAccess buffers Object[] entries before the handler is set; LogForwarder buffers LogEntry objects before an exporter is set; a new ByteBuddy SpringContextTransformer registers the appender at Spring context refresh time. For Quarkus extension mode, the appender registers in CameleerConfigAdapter's @PostConstruct.
Tech Stack: Java 17, ByteBuddy, SLF4J 2.x, Logback 1.5, Log4j2 2.24, JUL, Quarkus CDI
Task 1: BridgeAccess Early Buffer
Add buffering to BridgeAccess so entries are captured before the bridge handler is set, then drained when the handler becomes available.
Files:
-
Modify:
cameleer-log-appender/src/main/java/com/cameleer/appender/BridgeAccess.java -
Test:
cameleer-log-appender/src/test/java/com/cameleer/appender/BridgeAccessTest.java -
Step 1: Write the test
Create cameleer-log-appender/src/test/java/com/cameleer/appender/BridgeAccessTest.java:
package com.cameleer.appender;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class BridgeAccessTest {
@AfterEach
void reset() {
BridgeAccess.resetForTest();
}
@Test
void forward_buffersWhenNoHandler() {
Object[] data = {1L, "INFO", "com.example.App", "hello", "main", null, null, "app"};
BridgeAccess.forward(data);
assertEquals(1, BridgeAccess.getBufferSizeForTest());
}
@Test
void forward_drainsBufferWhenHandlerSet() {
List<Object> captured = new ArrayList<>();
Object[] early1 = {1L, "INFO", "com.example.A", "msg1", "main", null, null, "app"};
Object[] early2 = {2L, "WARN", "com.example.B", "msg2", "main", null, null, "app"};
// Buffer two entries before handler exists
BridgeAccess.forward(early1);
BridgeAccess.forward(early2);
assertEquals(2, BridgeAccess.getBufferSizeForTest());
// Set handler — should drain buffer
BridgeAccess.setHandler(captured::add);
// Trigger drain by forwarding one more entry
Object[] live = {3L, "DEBUG", "com.example.C", "msg3", "main", null, null, "app"};
BridgeAccess.forward(live);
// All 3 entries should arrive: 2 buffered + 1 live
assertEquals(3, captured.size());
assertEquals("msg1", ((Object[]) captured.get(0))[3]);
assertEquals("msg2", ((Object[]) captured.get(1))[3]);
assertEquals("msg3", ((Object[]) captured.get(2))[3]);
// Buffer should be empty now
assertEquals(0, BridgeAccess.getBufferSizeForTest());
}
@Test
void forward_liveModeAfterDrain() {
List<Object> captured = new ArrayList<>();
BridgeAccess.setHandler(captured::add);
Object[] data = {1L, "INFO", "com.example.App", "live", "main", null, null, "app"};
BridgeAccess.forward(data);
assertEquals(1, captured.size());
assertEquals(0, BridgeAccess.getBufferSizeForTest());
}
@Test
void forward_bufferCapped() {
for (int i = 0; i < 5100; i++) {
BridgeAccess.forward(new Object[]{(long) i, "INFO", "x", "m", "t", null, null, "app"});
}
assertTrue(BridgeAccess.getBufferSizeForTest() <= 5000,
"Buffer should cap at 5000 entries");
}
@Test
void setHandler_null_doesNotDrain() {
Object[] data = {1L, "INFO", "com.example.App", "buffered", "main", null, null, "app"};
BridgeAccess.forward(data);
BridgeAccess.setHandler(null);
// Buffer should still contain the entry
assertEquals(1, BridgeAccess.getBufferSizeForTest());
}
}
- Step 2: Run test to verify it fails
Run: mvn test -pl cameleer-log-appender -Dtest=BridgeAccessTest -B
Expected: FAIL — forward(), setHandler(), resetForTest(), getBufferSizeForTest() don't exist yet.
- Step 3: Implement BridgeAccess with buffer
Replace cameleer-log-appender/src/main/java/com/cameleer/appender/BridgeAccess.java with:
package com.cameleer.appender;
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* Resolves the LogEventBridge handler across classloader boundaries and buffers
* log entries received before the handler is available.
*
* <p>Two resolution strategies for reading the handler, tried in order:
* <ol>
* <li><b>Own classloader</b> ({@code Class.forName}) — extension mode</li>
* <li><b>System classloader</b> — agent mode</li>
* </ol>
*
* <p>Early buffer: entries forwarded before the handler is set are stored in a bounded
* queue (max 5000). When {@link #setHandler} is called, the buffer is drained through
* the handler before switching to live mode.
*/
final class BridgeAccess {
private static final String BRIDGE_CLASS = "com.cameleer.core.logging.LogEventBridge";
private static final int MAX_EARLY_BUFFER = 5000;
private static volatile Field cachedField;
private static volatile Consumer<Object> directHandler;
private static volatile boolean bufferDrained = false;
private static final ConcurrentLinkedQueue<Object[]> earlyBuffer = new ConcurrentLinkedQueue<>();
private BridgeAccess() {}
/**
* Forwards a log entry. If a handler is set, forwards directly (draining any
* buffered entries first). If no handler, buffers the entry for later drain.
*/
static void forward(Object[] data) {
Consumer<Object> h = directHandler;
if (h == null) {
h = resolveHandler();
}
if (h != null) {
if (!bufferDrained) {
drainBuffer(h);
}
h.accept(data);
} else {
if (earlyBuffer.size() < MAX_EARLY_BUFFER) {
earlyBuffer.add(data);
}
}
}
/**
* Sets the handler directly. Called by ServerSetup.setBridgeHandler() in extension mode,
* or via the LogEventBridge AtomicReference in agent mode.
* When a non-null handler is set, the early buffer is drained on the next forward() call.
*/
static void setHandler(Consumer<Object> handler) {
directHandler = handler;
if (handler != null) {
// Reset drain flag so next forward() triggers drain
bufferDrained = false;
}
}
/**
* Returns the current bridge handler, or null if not set or not resolvable.
* Checks both the direct handler and the LogEventBridge on system/own classloader.
*/
@SuppressWarnings("unchecked")
static Consumer<Object> getHandler() {
Consumer<Object> h = directHandler;
if (h != null) return h;
return resolveHandler();
}
@SuppressWarnings("unchecked")
private static Consumer<Object> resolveHandler() {
try {
Field f = cachedField;
if (f == null) {
f = resolveField();
cachedField = f;
}
AtomicReference<Consumer<Object>> ref =
(AtomicReference<Consumer<Object>>) f.get(null);
return ref.get();
} catch (Exception e) {
return null;
}
}
private static Field resolveField() throws Exception {
try {
Class<?> bridge = Class.forName(BRIDGE_CLASS);
return bridge.getField("handler");
} catch (ClassNotFoundException ignored) {
}
Class<?> bridge = ClassLoader.getSystemClassLoader().loadClass(BRIDGE_CLASS);
return bridge.getField("handler");
}
private static synchronized void drainBuffer(Consumer<Object> h) {
if (bufferDrained) return;
Object[] entry;
while ((entry = earlyBuffer.poll()) != null) {
h.accept(entry);
}
bufferDrained = true;
}
// -- Test support --
static void resetForTest() {
directHandler = null;
bufferDrained = false;
cachedField = null;
earlyBuffer.clear();
}
static int getBufferSizeForTest() {
return earlyBuffer.size();
}
}
- Step 4: Run test to verify it passes
Run: mvn test -pl cameleer-log-appender -Dtest=BridgeAccessTest -B
Expected: all 5 tests PASS
- Step 5: Update all three appenders to use
BridgeAccess.forward()
In CameleerLogbackAppender.java, change forwardViaBridge to:
private void forwardViaBridge(Object[] data) {
BridgeAccess.forward(data);
}
Remove the diagnostic forwardedCount and droppedNullHandler fields and their imports.
In CameleerLog4j2Appender.java, change forwardViaBridge to:
private void forwardViaBridge(Object[] data) {
BridgeAccess.forward(data);
}
In CameleerJulHandler.java, change forwardViaBridge to:
private void forwardViaBridge(Object[] data) {
BridgeAccess.forward(data);
}
- Step 6: Run all log-appender tests
Run: mvn test -pl cameleer-log-appender -B
Expected: all tests PASS (BridgeAccessTest + existing appender tests)
- Step 7: Commit
git add cameleer-log-appender/
git commit -m "feat: add early buffer to BridgeAccess for pre-handler log capture"
Task 2: LogForwarder Deferred Exporter
Make LogForwarder work without an exporter at construction time. The exporter is set later when the server connection is established.
Files:
-
Modify:
cameleer-core/src/main/java/com/cameleer/core/logging/LogForwarder.java -
Modify:
cameleer-core/src/test/java/com/cameleer/core/logging/LogForwarderTest.java -
Step 1: Write the test for deferred exporter
Add to LogForwarderTest.java:
@Test
void deferredExporter_buffersUntilExporterSet() throws Exception {
forwarder = new LogForwarder(); // no-arg — no exporter
forwarder.forward(objectArray("INFO", "com.example.App", "early msg"));
forwarder.forward(objectArray("WARN", "com.example.App", "early warn"));
// Wait for scheduler to attempt flush — should not throw or lose entries
Thread.sleep(1500);
// Now set the exporter
Exporter late = mock(Exporter.class);
forwarder.setExporter(late);
// Wait for scheduler to flush
Thread.sleep(1500);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<LogEntry>> captor = ArgumentCaptor.forClass(List.class);
verify(late, atLeastOnce()).exportLogs(captor.capture());
List<LogEntry> allEntries = captor.getAllValues().stream()
.flatMap(List::stream).toList();
assertTrue(allEntries.size() >= 2, "Both buffered entries should be flushed");
}
- Step 2: Run test to verify it fails
Run: mvn test -pl cameleer-core -Dtest=LogForwarderTest#deferredExporter_buffersUntilExporterSet -B
Expected: FAIL — no-arg constructor doesn't exist.
- Step 3: Implement deferred exporter support
Replace LogForwarder.java with:
package com.cameleer.core.logging;
import com.cameleer.common.model.LogEntry;
import com.cameleer.core.export.Exporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
* Receives log data (as Object[] from cross-classloader bridge or direct LogEntry),
* buffers, and delegates to Exporter for batched HTTP transport.
*
* <p>Supports deferred exporter: create with no-arg constructor, entries buffer in
* the queue, and flushing starts when {@link #setExporter} is called.
*/
public class LogForwarder {
private static final Logger LOG = LoggerFactory.getLogger(LogForwarder.class);
private static final int MAX_QUEUE_SIZE = 1000;
private static final int BATCH_SIZE = 50;
private final AtomicReference<Exporter> exporter = new AtomicReference<>();
private final ConcurrentLinkedQueue<LogEntry> queue = new ConcurrentLinkedQueue<>();
private final ScheduledExecutorService scheduler;
private final AtomicLong droppedLogs = new AtomicLong(0);
private volatile long lastDropWarningMs = 0;
/**
* Creates a LogForwarder with no exporter. Entries buffer in the queue
* until {@link #setExporter} is called.
*/
public LogForwarder() {
this(null);
}
/**
* Creates a LogForwarder with the given exporter. If null, entries buffer
* until {@link #setExporter} is called.
*/
public LogForwarder(Exporter exporter) {
this.exporter.set(exporter);
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "cameleer-log-forwarder");
t.setDaemon(true);
return t;
});
this.scheduler.scheduleAtFixedRate(this::flush, 1, 1, TimeUnit.SECONDS);
}
/**
* Sets or replaces the exporter. Buffered entries will be flushed on the
* next scheduler tick.
*/
public void setExporter(Exporter exporter) {
this.exporter.set(exporter);
}
/**
* Called from the cross-classloader bridge. Data is an Object[] with fields:
* [0] long timestampEpochMs, [1] String level, [2] String loggerName,
* [3] String message, [4] String threadName, [5] String stackTrace (nullable),
* [6] Map<String,String> mdc (nullable), [7] String source ("app"/"agent")
*/
@SuppressWarnings("unchecked")
public void forward(Object data) {
if (!(data instanceof Object[] arr) || arr.length < 7) return;
LogEntry entry = new LogEntry(
Instant.ofEpochMilli((long) arr[0]),
(String) arr[1],
(String) arr[2],
(String) arr[3],
(String) arr[4],
(String) arr[5],
(Map<String, String>) arr[6]
);
if (arr.length > 7 && arr[7] != null) {
entry.setSource((String) arr[7]);
}
enqueue(entry);
}
/**
* Direct forwarding for agent-internal logs (already on system CL).
*/
public void forwardDirect(LogEntry entry) {
enqueue(entry);
}
private void enqueue(LogEntry entry) {
if (queue.size() < MAX_QUEUE_SIZE) {
queue.add(entry);
} else {
long dropped = droppedLogs.incrementAndGet();
long now = System.currentTimeMillis();
if (now - lastDropWarningMs > 10_000) {
lastDropWarningMs = now;
LOG.warn("Cameleer: Log queue full, {} total dropped", dropped);
}
}
}
public void close() {
scheduler.shutdown();
try {
flush();
scheduler.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void flush() {
Exporter exp = exporter.get();
if (exp == null || queue.isEmpty()) return;
List<LogEntry> batch = new ArrayList<>(BATCH_SIZE);
for (int i = 0; i < BATCH_SIZE; i++) {
LogEntry entry = queue.poll();
if (entry == null) break;
batch.add(entry);
}
if (!batch.isEmpty()) {
exp.exportLogs(batch);
}
}
}
- Step 4: Run all LogForwarder tests
Run: mvn test -pl cameleer-core -Dtest=LogForwarderTest -B
Expected: all tests PASS (existing + new deferred test)
- Step 5: Commit
git add cameleer-core/src/
git commit -m "feat: support deferred exporter in LogForwarder for early log capture"
Task 3: Refactor ServerSetup.installLogForwarding()
Change installLogForwarding() to accept an existing LogForwarder (from early registration) and just activate its exporter, or create a new one as fallback.
Files:
-
Modify:
cameleer-core/src/main/java/com/cameleer/core/connection/ServerSetup.java -
Step 1: Refactor installLogForwarding
Change the method signature and body. The method now accepts an optional pre-existing LogForwarder:
private static LogForwarder installLogForwarding(CameleerAgentConfig config,
ChunkedExporter exporter,
CamelContext camelContext,
Map<String, Object> capabilities,
LogForwarder existingForwarder) {
if (!config.isLogForwardingEnabled()) {
return null;
}
if (existingForwarder != null) {
// Early registration already happened — just activate the exporter
existingForwarder.setExporter(exporter);
capabilities.put("logForwarding", true);
LOG.info("Cameleer: Log forwarding activated (early-registered, exporter connected)");
return existingForwarder;
}
// Fallback: no early registration — create LogForwarder and register appender now
LogForwarder logForwarder = new LogForwarder(exporter);
setBridgeHandler(logForwarder::forward);
ClassLoader appClassLoader = camelContext.getClass().getClassLoader();
String framework = registerAppenderViaReflection(appClassLoader);
if (framework != null) {
capabilities.put("logForwarding", true);
LOG.info("Cameleer: Log forwarding started (framework={})", framework);
return logForwarder;
}
LOG.warn("Cameleer: No supported logging framework found, log forwarding disabled");
setBridgeHandler(null);
logForwarder.close();
return null;
}
- Step 2: Update the call site in connect()
Update the ConnectionContext record to include an optional LogForwarder:
public record ConnectionContext(CameleerAgentConfig config, CamelContext camelContext,
ExecutionCollector collector, List<RouteGraph> graphs,
CameleerEventNotifier eventNotifier, CamelMetricsBridge metricsBridge,
PrometheusEndpoint prometheusEndpoint, LogForwarder earlyLogForwarder) {}
Update the installLogForwarding call at line 136:
LogForwarder logForwarder = installLogForwarding(config, exporter,
ctx.camelContext(), capabilities, ctx.earlyLogForwarder());
- Step 3: Update PostStartSetup.Context to carry earlyLogForwarder
In PostStartSetup.java, update the Context record:
public record Context(CameleerAgentConfig config, CamelContext camelContext,
ExecutionCollector collector, Exporter initialExporter,
CameleerEventNotifier eventNotifier,
boolean skipJmxMetrics, LogForwarder earlyLogForwarder) {}
And pass it through to ServerSetup.ConnectionContext:
ServerSetup.ConnectionContext connCtx = new ServerSetup.ConnectionContext(
config, ctx.camelContext(), ctx.collector(), graphs,
ctx.eventNotifier(), metricsBridge, prometheusEndpoint, ctx.earlyLogForwarder());
- Step 4: Update all callers of PostStartSetup.Context
In CameleerHookInstaller.postInstall():
PostStartSetup.Context ctx = new PostStartSetup.Context(
config, camelContext, sharedCollector, sharedExporter,
eventNotifier, false, logForwarder);
In CameleerLifecycle.onCamelContextStarted():
PostStartSetup.Context ctx = new PostStartSetup.Context(
config, camelContext, collector, exporter, eventNotifier, isNativeImage, earlyLogForwarder);
(The earlyLogForwarder field will be set in Tasks 4 and 5.)
- Step 5: Build to verify compilation
Run: mvn clean compile -B -pl cameleer-common,cameleer-core,cameleer-agent -am
Expected: BUILD SUCCESS (pass null for earlyLogForwarder temporarily)
- Step 6: Commit
git add cameleer-core/src/ cameleer-agent/src/
git commit -m "refactor: ServerSetup accepts existing LogForwarder for early registration"
Task 4: Early Appender Registration — Agent Mode (SpringContextTransformer)
Add a ByteBuddy transformer that registers the log appender when Spring's AbstractApplicationContext.refresh() is entered.
Files:
-
Create:
cameleer-agent/src/main/java/com/cameleer/agent/instrumentation/SpringContextTransformer.java -
Create:
cameleer-agent/src/main/java/com/cameleer/agent/instrumentation/SpringContextAdvice.java -
Modify:
cameleer-agent/src/main/java/com/cameleer/agent/CameleerAgent.java -
Modify:
cameleer-agent/src/main/java/com/cameleer/agent/instrumentation/CameleerHookInstaller.java -
Step 1: Create SpringContextTransformer
Create cameleer-agent/src/main/java/com/cameleer/agent/instrumentation/SpringContextTransformer.java:
package com.cameleer.agent.instrumentation;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import java.security.ProtectionDomain;
/**
* ByteBuddy transformer that instruments Spring's AbstractApplicationContext.refresh()
* to register the Cameleer log appender as early as possible in the Spring lifecycle.
*
* <p>At refresh() entry, Logback/Log4j2 is already initialized by Spring Boot.
* The appender starts buffering log entries; they are flushed when the server
* connection is established later.
*
* <p>For non-Spring apps, this transformer never matches — the appender is
* registered in preInstall() instead.
*/
public class SpringContextTransformer implements AgentBuilder.Transformer {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule module,
ProtectionDomain protectionDomain) {
return builder.visit(
Advice.to(SpringContextAdvice.class)
.on(ElementMatchers.named("refresh")
.and(ElementMatchers.takesNoArguments()))
);
}
}
- Step 2: Create SpringContextAdvice
Create cameleer-agent/src/main/java/com/cameleer/agent/instrumentation/SpringContextAdvice.java:
package com.cameleer.agent.instrumentation;
import net.bytebuddy.asm.Advice;
/**
* ByteBuddy advice that registers the Cameleer log appender on entry to
* {@code AbstractApplicationContext.refresh()}.
*
* <p>At this point in the Spring Boot lifecycle, the logging framework (Logback)
* is already fully initialized. The appender starts capturing all application
* logs — bean creation, autoconfiguration, Tomcat init, etc.
*
* <p>A {@code LogForwarder} is created without an exporter (deferred mode).
* Entries buffer in its queue until {@code ServerSetup.connect()} provides
* a {@code ChunkedExporter}.
*
* <p>All field access uses reflection to avoid classloader coupling —
* this advice class is loaded by the system classloader, but the target
* classes are on the app classloader.
*/
public class SpringContextAdvice {
public static volatile boolean initialized = false;
/** Stores the LogForwarder created during early registration. */
public static volatile Object earlyLogForwarder = null;
@Advice.OnMethodEnter
public static void onEnter(@Advice.This Object context) {
if (initialized) return;
initialized = true;
try {
ClassLoader appCL = context.getClass().getClassLoader();
// Register appender on the logging framework
Class<?> registrar = appCL.loadClass("com.cameleer.appender.LogAppenderRegistrar");
String framework = (String) registrar.getMethod("install", ClassLoader.class)
.invoke(null, appCL);
if (framework == null) {
System.err.println("Cameleer: Early log registration — no framework detected");
initialized = false;
return;
}
// Create LogForwarder in deferred mode (no exporter yet)
Class<?> forwarderClass = ClassLoader.getSystemClassLoader()
.loadClass("com.cameleer.core.logging.LogForwarder");
Object logForwarder = forwarderClass.getDeclaredConstructor().newInstance();
earlyLogForwarder = logForwarder;
// Get the forward method reference and set as bridge handler
java.lang.reflect.Method forwardMethod = forwarderClass.getMethod("forward", Object.class);
java.util.function.Consumer<Object> handler = data -> {
try {
forwardMethod.invoke(logForwarder, data);
} catch (Exception ignored) {
}
};
// Set bridge handler on system CL's LogEventBridge
Class<?> bridgeClass = ClassLoader.getSystemClassLoader()
.loadClass("com.cameleer.core.logging.LogEventBridge");
java.lang.reflect.Field handlerField = bridgeClass.getField("handler");
@SuppressWarnings("unchecked")
java.util.concurrent.atomic.AtomicReference<java.util.function.Consumer<Object>> ref =
(java.util.concurrent.atomic.AtomicReference<java.util.function.Consumer<Object>>)
handlerField.get(null);
ref.set(handler);
System.err.println("Cameleer: Early log appender registered (framework=" + framework + ")");
} catch (ClassNotFoundException e) {
// Log appender JAR not on classpath — will be handled later in preInstall
System.err.println("Cameleer: Early log registration skipped — log-appender not on classpath");
initialized = false;
} catch (Exception e) {
System.err.println("Cameleer: Early log registration failed: " + e.getMessage());
initialized = false;
}
}
}
- Step 3: Register SpringContextTransformer in premain()
In CameleerAgent.java, add the third transformer after the SendDynamic one (before the final LOG line):
// Instrument Spring's AbstractApplicationContext.refresh() to register log
// appender as early as possible — captures bean creation, autoconfiguration, etc.
new AgentBuilder.Default()
.type(ElementMatchers.named(
"org.springframework.context.support.AbstractApplicationContext"))
.transform(new SpringContextTransformer())
.with(AgentBuilder.Listener.StreamWriting.toSystemOut().withErrorsOnly())
.installOn(inst);
Add the import:
import com.cameleer.agent.instrumentation.SpringContextTransformer;
- Step 4: Wire early LogForwarder in CameleerHookInstaller
In CameleerHookInstaller.java, update preInstall() to check for early registration and register if it didn't happen:
After the existing MDC and InterceptStrategy setup (after line 65), add:
// Early log appender registration — if SpringContextAdvice didn't already do it
if (!SpringContextAdvice.initialized && config.isLogForwardingEnabled()) {
try {
ClassLoader appCL = camelContext.getClass().getClassLoader();
Class<?> registrar = appCL.loadClass("com.cameleer.appender.LogAppenderRegistrar");
String framework = (String) registrar.getMethod("install", ClassLoader.class)
.invoke(null, appCL);
if (framework != null) {
logForwarder = new LogForwarder();
com.cameleer.core.connection.ServerSetup.setBridgeHandler(logForwarder::forward);
LOG.info("Cameleer: Early log appender registered in preInstall (framework={})", framework);
}
} catch (ClassNotFoundException e) {
LOG.warn("Cameleer: cameleer-log-appender JAR not found on app classpath");
} catch (Exception e) {
LOG.warn("Cameleer: Failed to register early log appender: {}", e.getMessage());
}
}
Update postInstall() to use early LogForwarder if available:
Replace the logForwarder assignment at line 105:
// Use early-registered LogForwarder if available
if (logForwarder == null && SpringContextAdvice.earlyLogForwarder != null) {
logForwarder = (LogForwarder) SpringContextAdvice.earlyLogForwarder;
}
sharedExporter = result.activeExporter();
// logForwarder is already set from preInstall or SpringContextAdvice
if (result.logForwarder() != null) {
logForwarder = result.logForwarder();
}
Update the PostStartSetup.Context call to pass the early LogForwarder:
PostStartSetup.Context ctx = new PostStartSetup.Context(
config, camelContext, sharedCollector, sharedExporter,
eventNotifier, false, logForwarder);
- Step 5: Build to verify compilation
Run: mvn clean compile -B -pl cameleer-common,cameleer-core,cameleer-log-appender,cameleer-agent -am
Expected: BUILD SUCCESS
- Step 6: Run all agent tests
Run: mvn test -B -pl cameleer-agent
Expected: all tests PASS
- Step 7: Commit
git add cameleer-agent/src/
git commit -m "feat: register log appender at Spring context refresh via ByteBuddy"
Task 5: Early Appender Registration — Extension Mode (Quarkus)
Register the log appender in CameleerConfigAdapter.@PostConstruct and wire the LogForwarder through to CameleerLifecycle.
Files:
-
Modify:
cameleer-extension/runtime/pom.xml -
Modify:
cameleer-extension/runtime/src/main/java/com/cameleer/extension/CameleerConfigAdapter.java -
Modify:
cameleer-extension/runtime/src/main/java/com/cameleer/extension/CameleerLifecycle.java -
Step 1: Add log-appender dependency to extension runtime POM
In cameleer-extension/runtime/pom.xml, add in the <dependencies> section after camel-quarkus-core:
<dependency>
<groupId>com.cameleer</groupId>
<artifactId>cameleer-log-appender</artifactId>
<version>${project.version}</version>
</dependency>
- Step 2: Add early registration to CameleerConfigAdapter
Add to CameleerConfigAdapter.java:
import com.cameleer.appender.LogAppenderRegistrar;
import com.cameleer.core.connection.ServerSetup;
import com.cameleer.core.logging.LogForwarder;
Add a field:
private LogForwarder earlyLogForwarder;
Add at the end of init() (after config = CameleerAgentConfig.getInstance();):
// Register log appender early — captures CDI init, CamelContext creation, etc.
if (config.isLogForwardingEnabled()) {
String framework = LogAppenderRegistrar.install(
Thread.currentThread().getContextClassLoader());
if (framework != null) {
earlyLogForwarder = new LogForwarder();
ServerSetup.setBridgeHandler(earlyLogForwarder::forward);
System.err.println("Cameleer Extension: Early log appender registered (framework=" + framework + ")");
}
}
Add getter:
public LogForwarder getEarlyLogForwarder() {
return earlyLogForwarder;
}
- Step 3: Wire early LogForwarder in CameleerLifecycle
In CameleerLifecycle.onCamelContextStarted(), pass the early LogForwarder to PostStartSetup:
void onCamelContextStarted(@Observes CamelEvent.CamelContextStartedEvent event) {
CamelContext camelContext = event.getContext();
CameleerAgentConfig config = configAdapter.getConfig();
boolean isNativeImage = System.getProperty("org.graalvm.nativeimage.imagecode") != null;
PostStartSetup.Context ctx = new PostStartSetup.Context(
config, camelContext, collector, exporter, eventNotifier, isNativeImage,
configAdapter.getEarlyLogForwarder());
PostStartSetup.Result result = PostStartSetup.run(ctx);
exporter = result.activeExporter();
}
Add import:
import com.cameleer.core.logging.LogForwarder;
- Step 4: Build extension to verify compilation
Run: mvn clean compile -B -pl cameleer-extension/runtime -am
Expected: BUILD SUCCESS
- Step 5: Commit
git add cameleer-extension/
git commit -m "feat: register log appender early in Quarkus extension via @PostConstruct"
Task 6: Remove Diagnostic Lines
Remove all temporary System.err.println diagnostic counters added during classloader debugging.
Files:
-
Modify:
cameleer-log-appender/src/main/java/com/cameleer/appender/CameleerLogbackAppender.java -
Modify:
cameleer-core/src/main/java/com/cameleer/core/export/ChunkedExporter.java -
Step 1: Clean up CameleerLogbackAppender
Remove the forwardedCount and droppedNullHandler fields and their import. The forwardViaBridge method should now be just:
private void forwardViaBridge(Object[] data) {
BridgeAccess.forward(data);
}
(This was already done in Task 1 step 5 — verify no diagnostic lines remain.)
- Step 2: Clean up ChunkedExporter
In ChunkedExporter.java, remove:
- The
logsSentCountfield (around line 228) - The
System.err.println("Cameleer-DIAG: ChunkedExporter POST ...")line influshLogs()
The flushLogs() method should look like the original without diagnostics:
private void flushLogs() {
List<LogEntry> batch = new ArrayList<>(BATCH_SIZE);
for (int i = 0; i < BATCH_SIZE; i++) {
LogEntry entry = logQueue.poll();
if (entry == null) break;
batch.add(entry);
}
if (batch.isEmpty()) return;
try {
String json = MAPPER.writeValueAsString(batch);
int status = serverConnection.sendData("/api/v1/data/logs", json);
if (status >= 200 && status < 300) {
LOG.debug("Exported {} log entries (HTTP {})", batch.size(), status);
} else if (status == 503) {
pauseUntil.set(System.currentTimeMillis() + 10_000);
LOG.warn("Server overloaded (503), pausing export for 10 seconds");
} else {
LOG.warn("Log export returned HTTP {} ({} log entries lost)", status, batch.size());
}
} catch (JsonProcessingException e) {
LOG.error("Failed to serialize {} log entries", batch.size(), e);
} catch (Exception e) {
LOG.warn("Failed to send {} log entries: {}", batch.size(), e.getMessage());
}
}
- Step 3: Build full project
Run: mvn clean verify -B -pl cameleer-common,cameleer-core,cameleer-log-appender,cameleer-agent -am
Expected: BUILD SUCCESS, all tests pass
- Step 4: Commit
git add cameleer-log-appender/src/ cameleer-core/src/
git commit -m "chore: remove temporary diagnostic System.err.println lines"
Task 7: Full Build Verification
Final integration verification across all modules.
- Step 1: Full build with all modules
Run: mvn clean verify -B
Expected: BUILD SUCCESS across all modules
- Step 2: Verify no remaining diagnostic lines
Run: grep -r "Cameleer-DIAG" cameleer-*/src/main/ || echo "clean"
Expected: "clean" — no diagnostic lines remain
- Step 3: Final commit if any cleanup needed
If any remaining issues found, fix and commit.