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>
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:
- Extracting shared observability code into
cameleer-core - Building a Quarkus extension (
cameleer-extension) using CDI lifecycle hooks - Creating a native-capable example app (
cameleer-quarkus-native-app) - 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,SubExchangeContextnotifier/—CameleerInterceptStrategy,CameleerEventNotifier(+InterceptCallbackinner class)export/—Exporter,ExporterFactory,LogExporter,ChunkedExporterconnection/—ServerConnection,SseClient,HeartbeatManager,ConfigVerifier,TokenRefreshFailedExceptioncommand/—CommandHandler,CommandResult,DefaultCommandHandlerconfig/—ConfigCacheManagerdiagram/—RouteModelExtractor,DebugSvgRenderer,RouteGraphVersionManagermetrics/—CamelMetricsBridge,PrometheusEndpoint,PrometheusFormattertap/—TapEvaluatorotel/—OtelBridgehealth/—HealthEndpoint,StartupReportlogging/—LogForwarder,LogEventBridge,LogEventConverter,LogLevelSetter,CameleerJulHandler
Stays in cameleer-agent (ByteBuddy-dependent):
CameleerAgent.java— premain entry pointinstrumentation/AgentClassLoader.java— classloader bridgeinstrumentation/CamelContextAdvice.java— ByteBuddy adviceinstrumentation/CamelContextTransformer.java— ByteBuddy transformerinstrumentation/CameleerHookInstaller.java— orchestrator (calls core classes)instrumentation/SendDynamicAwareTransformer.java— ByteBuddy transformerinstrumentation/SendDynamicUriAdvice.java— ByteBuddy adviceinstrumentation/ServerSetup.java— server connection setuplogging/LogForwardingInstaller.java— ByteBuddy ClassInjectorlogging/CameleerLogbackAppender.java— ByteBuddy ClassInjector targetlogging/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> CameleerHookInstallerimports fromcom.cameleer.core.*instead ofcom.cameleer.agent.*- Shade plugin relocations unchanged (ByteBuddy, SLF4J, OTel)
- Shade plugin must now include
cameleer-coreclasses 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 useCamelContextand route builders stay incameleer-agentsince they test the full agent instrumentation path. The agent tests will import fromcom.cameleer.core.*instead ofcom.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-pluginwithbuildgoal) native: adds-Dquarkus.native.enabled=true
- Default: JVM mode (
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.backendDockerfile.callerDockerfile.quarkusDockerfile.quarkus-native(new)
CI Workflow (.gitea/workflows/ci.yml)
- build job:
mvn clean verifyalready 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-nativeto package cleanup loop - deploy job: add kubectl apply + rollout for
cameleer-quarkus-native
Compatibility Workflow (.gitea/workflows/camel-compat.yml)
- Agent compat test: update
-plto includecameleer-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: publishcameleer-coreandcameleer-extensionto Maven registry alongsidecameleer-commonsonarqube.yml: no change (mvn clean verifycovers 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:
- Create
cameleer-core— new module, move classes, update packages - Refactor
cameleer-agent— depend on core, update imports, update shade config - Verify:
mvn clean verify— all existing tests must pass - Create
cameleer-extension— runtime + deployment modules - Create
cameleer-quarkus-native-app— example app with native profile - Update all Dockerfiles — add new POM COPY lines
- Create
Dockerfile.quarkus-native— native build Dockerfile - Create
deploy/quarkus-native-app.yaml— K8s manifest - Update CI/CD workflows — ci.yml, camel-compat.yml, release.yml
- Update CLAUDE.md — documentation
- Final verify:
mvn clean verify— all modules pass
7. Verification
mvn clean verify— all modules build and test (including agent regression)- Agent still works:
java -javaagent:...shaded.jar -jar ...sample-app.jar - Extension JVM mode: run
cameleer-quarkus-native-appwithjava -jar(no agent flag), verify execution chunks exported - Extension native mode:
mvn package -Pnative -pl ...quarkus-native-app, run binary, verify execution chunks exported - REST endpoint test:
curl -X POST http://localhost:8080/api/orders ... - Verify no agent hooks fire (no
-javaagent), only extension CDI hooks - Docker builds: all 6 Dockerfiles build successfully
8. Files Summary
New files
cameleer-core/pom.xmlcameleer-core/src/main/java/com/cameleer/core/**(moved from agent)cameleer-extension/pom.xml(parent)cameleer-extension/runtime/pom.xmlcameleer-extension/runtime/src/main/java/com/cameleer/extension/**cameleer-extension/deployment/pom.xmlcameleer-extension/deployment/src/main/java/com/cameleer/extension/deployment/**cameleer-quarkus-native-app/pom.xmlcameleer-quarkus-native-app/src/**Dockerfile.quarkus-nativedeploy/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 fromagent.*tocore.*)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