Files
cameleer/docs/superpowers/plans/2026-04-12-early-log-capture.md
hsiegeln f22203a14a
Some checks failed
CI / build (push) Successful in 7m38s
CI / docker (push) Successful in 43s
CI / deploy (push) Failing after 5s
chore: rename cameleer3 to cameleer
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>
2026-04-15 15:28:26 +02:00

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 logsSentCount field (around line 228)
  • The System.err.println("Cameleer-DIAG: ChunkedExporter POST ...") line in flushLogs()

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.