Files
cameleer/docs/superpowers/specs/2026-04-01-cameleer-extension-graalvm-design.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

17 KiB

Design Spec: Cameleer Extension for GraalVM Native Support

Context

The Cameleer Java agent uses ByteBuddy to instrument Apache Camel applications at runtime. This works in JVM mode but is incompatible with GraalVM native images (no runtime class transformation). Investigation revealed that 90% of the agent code is pure Camel SPI — ByteBuddy is only the entry mechanism. This spec defines a Quarkus extension that provides the same observability without ByteBuddy, enabling GraalVM native image support.

The work involves:

  1. Extracting shared observability code into cameleer-core
  2. Building a Quarkus extension (cameleer-extension) using CDI lifecycle hooks
  3. Creating a native-capable example app (cameleer-quarkus-native-app)
  4. Updating all pipelines, Dockerfiles, and existing apps for the new module structure

Module Architecture (after refactoring)

cameleer-common           ← Models (unchanged)
cameleer-core             ← NEW: observability logic extracted from agent
cameleer-agent            ← REFACTORED: thin ByteBuddy wrapper → depends on core
cameleer-extension/       ← NEW: Quarkus extension (CDI hooks → core)
  ├── runtime/
  └── deployment/
cameleer-sample-app       ← UPDATE: Dockerfiles for new module
cameleer-backend-app      ← UPDATE: Dockerfiles for new module
cameleer-caller-app       ← UPDATE: Dockerfiles for new module
cameleer-quarkus-app      ← UPDATE: Dockerfiles for new module
cameleer-quarkus-native-app ← NEW: native-capable example app

Dependency graph:

common ← core ← agent (ByteBuddy + core)
              ← extension/runtime (CDI + core)
                  ← extension/deployment (build-time processing)

1. cameleer-core — Shared Observability Logic

What moves from cameleer-agent to cameleer-core

Moves to core (no ByteBuddy dependency):

  • CameleerAgentConfig.java (config POJO, only has a ByteBuddy comment)
  • collector/ExecutionCollector, FlatExecutionCollector, ExchangeState, LoopTrackingState, PayloadCapture, SubExchangeContext
  • notifier/CameleerInterceptStrategy, CameleerEventNotifier (+ InterceptCallback inner class)
  • export/Exporter, ExporterFactory, LogExporter, ChunkedExporter
  • connection/ServerConnection, SseClient, HeartbeatManager, ConfigVerifier, TokenRefreshFailedException
  • command/CommandHandler, CommandResult, DefaultCommandHandler
  • config/ConfigCacheManager
  • diagram/RouteModelExtractor, DebugSvgRenderer, RouteGraphVersionManager
  • metrics/CamelMetricsBridge, PrometheusEndpoint, PrometheusFormatter
  • tap/TapEvaluator
  • otel/OtelBridge
  • health/HealthEndpoint, StartupReport
  • logging/LogForwarder, LogEventBridge, LogEventConverter, LogLevelSetter, CameleerJulHandler

Stays in cameleer-agent (ByteBuddy-dependent):

  • CameleerAgent.java — premain entry point
  • instrumentation/AgentClassLoader.java — classloader bridge
  • instrumentation/CamelContextAdvice.java — ByteBuddy advice
  • instrumentation/CamelContextTransformer.java — ByteBuddy transformer
  • instrumentation/CameleerHookInstaller.java — orchestrator (calls core classes)
  • instrumentation/SendDynamicAwareTransformer.java — ByteBuddy transformer
  • instrumentation/SendDynamicUriAdvice.java — ByteBuddy advice
  • instrumentation/ServerSetup.java — server connection setup
  • logging/LogForwardingInstaller.java — ByteBuddy ClassInjector
  • logging/CameleerLogbackAppender.java — ByteBuddy ClassInjector target
  • logging/CameleerLog4j2Appender.java — ByteBuddy ClassInjector target

Package rename

Classes move from com.cameleer.agent.* to com.cameleer.core.*. The agent's remaining classes keep com.cameleer.agent.* and import from com.cameleer.core.*.

cameleer-core/pom.xml

  • Parent: cameleer-parent
  • Dependencies: cameleer-common, Apache Camel (provided), Jackson, SLF4J, OTel SDK, Prometheus client
  • No ByteBuddy dependency
  • No shade plugin — this is a regular JAR

cameleer-agent refactored

  • POM adds <dependency>cameleer-core</dependency>
  • CameleerHookInstaller imports from com.cameleer.core.* instead of com.cameleer.agent.*
  • Shade plugin relocations unchanged (ByteBuddy, SLF4J, OTel)
  • Shade plugin must now include cameleer-core classes in the shaded JAR (add <include>com.cameleer:cameleer-core</include> to shade config)
  • Tests: unit tests for core classes (collector, notifier, exporter) move to cameleer-core. Integration tests that use CamelContext and route builders stay in cameleer-agent since they test the full agent instrumentation path. The agent tests will import from com.cameleer.core.* instead of com.cameleer.agent.*.

2. cameleer-extension — Quarkus Extension

cameleer-extension/runtime

CameleerLifecycle.java — replaces CameleerHookInstaller:

@ApplicationScoped
@Unremovable
public class CameleerLifecycle {
    @Inject CamelContext camelContext;

    // BEFORE routes start — register InterceptStrategy
    void onBeforeStart(@Observes @Priority(Interceptor.Priority.PLATFORM_BEFORE)
                       StartupEvent event) {
        camelContext.setUseMDCLogging(true);
        camelContext.getCamelContextExtension()
            .addInterceptStrategy(new CameleerInterceptStrategy(collector, config));
    }

    // AFTER CamelContext started — register EventNotifier, extract diagrams
    void onStarted(@Observes CamelEvent.CamelContextStartedEvent event) {
        camelContext.getManagementStrategy()
            .addEventNotifier(new CameleerEventNotifier(collector, config));
        RouteModelExtractor.extractAndExport(camelContext, exporter);
        if (!isNativeImage()) {
            startMetricsBridge();
        }
    }

    void onShutdown(@Observes ShutdownEvent event) {
        // graceful shutdown: flush, deregister, close
    }

    private static boolean isNativeImage() {
        return System.getProperty("org.graalvm.nativeimage.imagecode") != null;
    }
}

CameleerConfigMapping.java — CDI config mapping:

@ConfigMapping(prefix = "cameleer")
public interface CameleerConfigMapping {
    @WithDefault("LOG") Export export();
    @WithDefault("") Agent agent();
    @WithDefault("REGULAR") String engineLevel();
    // ... mirrors CameleerAgentConfig properties
}

Adapts to CameleerAgentConfig internally so all core classes work unchanged.

SendDynamicUri capture — replaces ByteBuddy SendDynamicUriAdvice: The CameleerEventNotifier already receives ExchangeSendingEvent which contains the resolved endpoint URI. In the extension, use this event to capture dynamic URIs instead of the ByteBuddy advice. The ExchangeSendingEvent.getEndpoint().getEndpointUri() provides the full resolved URI. This replaces the SendDynamicUriAdvice.CAPTURED_URIS map — the EventNotifier passes captured URIs directly to the collector.

Log forwarding — in the extension, log appenders are CDI beans registered directly (no ClassInjector needed). The Quarkus app's logging framework is accessible on the same classloader.

Dependencies: cameleer-core, io.quarkus:quarkus-arc, org.apache.camel.quarkus:camel-quarkus-core

cameleer-extension/deployment

CameleerExtensionProcessor.java:

public class CameleerExtensionProcessor {

    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem("cameleer");
    }

    @BuildStep
    AdditionalBeanBuildItem registerBeans() {
        return AdditionalBeanBuildItem.builder()
            .addBeanClasses(CameleerLifecycle.class, CameleerConfigMapping.class)
            .setUnremovable()
            .build();
    }

    @BuildStep
    ReflectiveClassBuildItem registerReflection() {
        return new ReflectiveClassBuildItem(true, true,
            ExecutionChunk.class, FlatProcessorRecord.class,
            RouteGraph.class, RouteNode.class, RouteEdge.class,
            AgentEvent.class, MetricsSnapshot.class, ExchangeSnapshot.class,
            ErrorInfo.class, LogEntry.class, LogBatch.class,
            ApplicationConfig.class, TapDefinition.class);
    }
}

Dependencies: cameleer-extension (runtime), io.quarkus:quarkus-arc-deployment, org.apache.camel.quarkus:camel-quarkus-core-deployment

User experience

<dependency>
    <groupId>com.cameleer</groupId>
    <artifactId>cameleer-extension</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
cameleer.export.type=HTTP
cameleer.export.endpoint=http://cameleer-server:8081
cameleer.agent.name=${HOSTNAME}
cameleer.engine.level=REGULAR

No -javaagent. Works in mvn quarkus:dev, java -jar, and native binary.

3. cameleer-quarkus-native-app — Example Application

Proves GraalVM native compilation works with the extension.

Routes (3 route classes, compact)

Class Route IDs Patterns
NativeRestRoute process-order, get-order REST DSL (platform-http), direct, processor
NativeSplitTimerRoute batch-split, timer-heartbeat timer, split, choice, log
NativeErrorRoute error-test, try-catch-test errorHandler (DLC), onException, doTry/doCatch

Reuses Order model (copy in own package, same as cameleer-quarkus-app pattern).

POM

  • Parent: cameleer-parent
  • Dependencies: cameleer-extension, camel-quarkus-* extensions, quarkus-junit5, rest-assured
  • Build profiles:
    • Default: JVM mode (quarkus-maven-plugin with build goal)
    • native: adds -Dquarkus.native.enabled=true

Dockerfile (Dockerfile.quarkus-native)

Multi-stage build with cross-compilation for ARM64 builder → amd64 native binary:

# Stage 1: Build native binary
# Use Mandrel builder (GraalVM distribution for Quarkus)
FROM --platform=$BUILDPLATFORM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21 AS build
WORKDIR /build
COPY . .
# Container-based native build produces a linux-amd64 binary regardless of host arch
RUN mvn clean package -Pnative -DskipTests -B \
    -Dquarkus.native.container-build=false \
    -pl cameleer-common,cameleer-core,cameleer-extension/runtime,cameleer-quarkus-native-app

# Stage 2: Minimal runtime
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /app
COPY --from=build /build/cameleer-quarkus-native-app/target/*-runner /app/application
EXPOSE 8080
ENTRYPOINT ["/app/application", \
    "-Dcameleer.export.type=${CAMELEER_EXPORT_TYPE}", \
    "-Dcameleer.export.endpoint=${CAMELEER_EXPORT_ENDPOINT}", \
    "-Dcameleer.agent.name=${HOSTNAME:-${CAMELEER_DISPLAY_NAME}}"]

ARM64/AMD64 strategy: Native compilation produces a platform-specific binary. Since our Gitea runner is ARM64 but deployment target is amd64, CI uses docker buildx build --platform linux/amd64 which runs the entire build stage emulated on amd64 — this produces a correct amd64 native binary. This is slower than native ARM64 compilation (~5min vs ~2min) but architecturally simple and consistent with how our other Dockerfiles work. The Mandrel builder image is multi-arch and works under emulation.

K8s manifest (deploy/quarkus-native-app.yaml)

  • Deployment cameleer-quarkus-native, 1 replica
  • NodePort 30085
  • Same env pattern as other apps

CI integration

Native builds are slow (~3-5 minutes). Options:

  • Add to main CI pipeline (increases build time significantly)
  • Run as separate nightly/weekly job
  • Run only on tag push (releases)

Recommend: separate nightly job or manual trigger, not on every push.

4. Pipeline & Infrastructure Updates

Root POM (pom.xml)

New modules added:

<modules>
    <module>cameleer-common</module>
    <module>cameleer-core</module>           <!-- NEW -->
    <module>cameleer-agent</module>
    <module>cameleer-extension</module>       <!-- NEW (parent POM for runtime + deployment) -->
    <module>cameleer-sample-app</module>
    <module>cameleer-backend-app</module>
    <module>cameleer-caller-app</module>
    <module>cameleer-quarkus-app</module>
    <module>cameleer-quarkus-native-app</module>  <!-- NEW -->
</modules>

Module order matters: core before agent and extension; extension before native app.

All Dockerfiles (5 existing + 1 new)

Every Dockerfile copies all module POMs in the build stage. Add:

COPY cameleer-core/pom.xml cameleer-core/
COPY cameleer-extension/pom.xml cameleer-extension/
COPY cameleer-extension/runtime/pom.xml cameleer-extension/runtime/
COPY cameleer-extension/deployment/pom.xml cameleer-extension/deployment/
COPY cameleer-quarkus-native-app/pom.xml cameleer-quarkus-native-app/

Files to update:

  • Dockerfile (sample app)
  • Dockerfile.backend
  • Dockerfile.caller
  • Dockerfile.quarkus
  • Dockerfile.quarkus-native (new)

CI Workflow (.gitea/workflows/ci.yml)

  • build job: mvn clean verify already builds all modules (no change needed for build command)
  • build job: add artifact upload for quarkus-native-app (if included)
  • docker job: add build+push step for Dockerfile.quarkus-native
  • docker cleanup: add cameleer-quarkus-native to package cleanup loop
  • deploy job: add kubectl apply + rollout for cameleer-quarkus-native

Compatibility Workflow (.gitea/workflows/camel-compat.yml)

  • Agent compat test: update -pl to include cameleer-core:
    mvn test "-Dcamel.version=$VERSION" -pl cameleer-common,cameleer-core,cameleer-agent
    
  • Quarkus compat test: no change needed (quarkus-app already tested)

Other Workflows

  • release.yml: publish cameleer-core and cameleer-extension to Maven registry alongside cameleer-common
  • sonarqube.yml: no change (mvn clean verify covers all modules)
  • benchmark.yml: no change (tests agent, not extension)
  • soak.yml: no change

CLAUDE.md

Update module list, add extension docs, update run commands.

5. Native Image Constraints

Feature JVM Mode Native Mode Approach
InterceptStrategy Works Works Pure Camel SPI, no reflection
EventNotifier Works Works Pure Camel SPI, no reflection
Route diagrams Works Works ReflectiveClassBuildItem in deployment
Exporters (HTTP/LOG) Works Works Standard HTTP client
JMX Metrics Works Skip Detect org.graalvm.nativeimage.imagecode, log warning
OTel spans Works Works OTel SDK supports native
Taps (expression eval) Works Partial simple/jsonpath work; groovy needs registration
Log forwarding Works Works CDI-based appender registration (no ClassInjector)
Prometheus endpoint Works Works Custom HTTP server, no JMX

6. Implementation Order

The work must be sequenced to keep the build green at each step:

  1. Create cameleer-core — new module, move classes, update packages
  2. Refactor cameleer-agent — depend on core, update imports, update shade config
  3. Verify: mvn clean verify — all existing tests must pass
  4. Create cameleer-extension — runtime + deployment modules
  5. Create cameleer-quarkus-native-app — example app with native profile
  6. Update all Dockerfiles — add new POM COPY lines
  7. Create Dockerfile.quarkus-native — native build Dockerfile
  8. Create deploy/quarkus-native-app.yaml — K8s manifest
  9. Update CI/CD workflows — ci.yml, camel-compat.yml, release.yml
  10. Update CLAUDE.md — documentation
  11. Final verify: mvn clean verify — all modules pass

7. Verification

  1. mvn clean verify — all modules build and test (including agent regression)
  2. Agent still works: java -javaagent:...shaded.jar -jar ...sample-app.jar
  3. Extension JVM mode: run cameleer-quarkus-native-app with java -jar (no agent flag), verify execution chunks exported
  4. Extension native mode: mvn package -Pnative -pl ...quarkus-native-app, run binary, verify execution chunks exported
  5. REST endpoint test: curl -X POST http://localhost:8080/api/orders ...
  6. Verify no agent hooks fire (no -javaagent), only extension CDI hooks
  7. Docker builds: all 6 Dockerfiles build successfully

8. Files Summary

New files

  • cameleer-core/pom.xml
  • cameleer-core/src/main/java/com/cameleer/core/** (moved from agent)
  • cameleer-extension/pom.xml (parent)
  • cameleer-extension/runtime/pom.xml
  • cameleer-extension/runtime/src/main/java/com/cameleer/extension/**
  • cameleer-extension/deployment/pom.xml
  • cameleer-extension/deployment/src/main/java/com/cameleer/extension/deployment/**
  • cameleer-quarkus-native-app/pom.xml
  • cameleer-quarkus-native-app/src/**
  • Dockerfile.quarkus-native
  • deploy/quarkus-native-app.yaml

Modified files

  • pom.xml (root — add modules)
  • cameleer-agent/pom.xml (add core dependency, update shade config)
  • cameleer-agent/src/** (update imports from agent.* to core.*)
  • Dockerfile, Dockerfile.backend, Dockerfile.caller, Dockerfile.quarkus (add POM COPY lines)
  • .gitea/workflows/ci.yml (docker build + deploy for native app)
  • .gitea/workflows/camel-compat.yml (add core to -pl)
  • .gitea/workflows/release.yml (publish core + extension)
  • CLAUDE.md