SkyWalking GraalVM Distro - Distribution Policy
Goal
Build and package Apache SkyWalking OAP server as a GraalVM native image on JDK 25.
Architecture Constraints
- Submodule:
skywalking/is a git submodule ofapache/skywalking.git. All SkyWalking source changes go through upstream PRs. Minimize upstream changes. - This repo: Maven + Makefile to orchestrate building on top of the submodule. Pre-compilation, GraalVM config, native-image wiring, and the fixed module manager live here.
- JDK 25: Already compiles and runs. Not an issue.
Module Selection (Fixed at Build Time)
| Category | Module | Provider |
|---|---|---|
| Core | CoreModule | default |
| Storage | StorageModule | BanyanDB |
| Cluster | ClusterModule | Standalone, Kubernetes |
| Configuration | ConfigurationModule | Kubernetes |
| Receivers | SharingServerModule, TraceModule, JVMModule, MeterReceiverModule, LogModule, RegisterModule, ProfileModule, BrowserModule, EventModule, OtelMetricReceiverModule, MeshReceiverModule, EnvoyMetricReceiverModule, ZipkinReceiverModule, ZabbixReceiverModule, TelegrafReceiverModule, AWSFirehoseReceiverModule, CiliumFetcherModule, EBPFReceiverModule, AsyncProfilerModule, PprofModule, CLRModule, ConfigurationDiscoveryModule, KafkaFetcherModule | default providers |
| Analyzers | AnalyzerModule, LogAnalyzerModule, EventAnalyzerModule | default providers |
| Query | QueryModule (GraphQL), PromQLModule, LogQLModule, ZipkinQueryModule, StatusQueryModule | default providers |
| Alarm | AlarmModule | default |
| Telemetry | TelemetryModule | Prometheus |
| Other | ExporterModule, HealthCheckerModule, AIPipelineModule | default providers |
Full feature set. Work around issues as they arise.
Core Strategy
-
Build-Time Class Export: All runtime code generation (OAL/MAL/LAL/Hierarchy via ANTLR4 + Javassist) runs at build time. Export
.classfiles and package into native-image classpath. Classpath scanning also runs here as a verification gate. -
Fixed Module Wiring: Module/provider selection is hardcoded in this distro (no SPI discovery). Simplified config file for selected providers only.
-
Separation: SkyWalking upstream changes tracked separately, go through upstream PRs.
DSL Runtime Class Generation (Unified V2 Engines)
Since upstream PR #13723, all four SkyWalking DSL compilers (OAL, MAL, LAL, Hierarchy) use a unified pipeline: ANTLR4 parse → immutable AST → Javassist bytecode. This eliminated the need for the complex, 10,000-line custom Groovy-to-Java transpiler previously maintained in this distro.
Approach (Simplified)
Run all four v2 compilers at build time via their native exposed APIs (setClassOutputDir(), setClassNameHint(), setGeneratedFilePath()). Capture generated .class files into the precompiler output JAR. At runtime, same-FQCN replacement loaders find these classes by name instead of compiling them.
Details: dsl-immigration.md | oal-immigration.md
OAL
OALEngineV2.start()processes all 9 OAL defines at build time.- Exports ~620 metrics classes, ~620 builder classes, ~45 dispatchers.
- 3 manifest files:
oal-metrics-classes.txt,oal-dispatcher-classes.txt,oal-disabled-sources.txt. - Same-FQCN
OALEngineLoaderServiceloads pre-compiled classes from manifests.
MAL
MALClassGeneratorcompiles ~1250 MAL expressions from 71 YAML rule files at build time.- Uses
setClassNameHint(yamlSource + metricName)for deterministic naming. - Same-FQCN v2
DSL.javaloads pre-compiledMalExpressionclasses by computed name. MeterSystem.create()also runs at build time → exports ~1188 Javassist meter classes.
LAL
LALClassGeneratorcompiles 10 LAL scripts (8 YAML files) at build time.- Uses
setClassNameHint(yamlSource + ruleName)for deterministic naming. - Same-FQCN v2
DSL.javaloads pre-compiledLalExpressionclasses by computed name.
Hierarchy
HierarchyRuleClassGeneratorcompiles 4 hierarchy matching rules at build time.- Same-FQCN
CompiledHierarchyRuleProviderloads pre-compiledBiFunctionclasses by rule name.
Upstream Changes & Simplification
- Groovy Removed: Groovy was completely removed from upstream production code (PR #13723).
- Transpiler Deleted: The legacy Groovy-to-Java transpiler (~9,800 lines of complex AST walking logic) has been deleted from this distro.
- Native Alignment: We now use the exact same compilation pipeline as upstream, just executed at build-time. This ensures 100% behavioral parity and zero maintenance lag for new DSL features.
Classpath Scanning (Guava ClassPath)
What Happens
ClassPath.from() used in SourceReceiverImpl.scan(), AnnotationScan, MeterSystem, DefaultMetricsFunctionRegistry, FilterMatchers, MetricsHolder.
What Was Solved
AnnotationScanandSourceReceiverImplreplaced with same-FQCN classes that read from build-time manifests. 6 annotation/interface manifests underMETA-INF/annotation-scan/:ScopeDeclaration,Stream,Disable,MultipleDisable,SourceDispatcher,ISourceDecorator.DefaultMetricsFunctionRegistry,FilterMatchers,MetricsHolder— these only run inside the OAL engine at build time, not at runtime. Automatically solved.MeterSystemreplaced with same-FQCN class that reads fromMETA-INF/annotation-scan/MeterFunction.txtmanifest (16 meter function classes). Solved as part of MAL immigration.
Module System & Configuration
Current Behavior
ModuleManager uses ServiceLoader (SPI). application.yml selects providers. Config loaded via reflection (Field.setAccessible + field.set in YamlConfigLoaderUtils.copyProperties).
Approach (this repo)
- New module manager: Directly constructs chosen
ModuleDefine/ModuleProvider— no SPI - Simplified config file: Only knobs for selected providers
- Config loading: No reflection. Build-time tool scans all
ModuleConfigsubclass fields → generates same-FQCN replacement ofYamlConfigLoaderUtilsthat uses Lombok setters and VarHandle to set config fields directly. EliminatesField.setAccessible/field.setand the need forreflect-config.jsonfor config classes.
Details: config-init-immigration.md
What Was Built
FixedModuleManager— direct module/provider construction viaModuleDefine.prepare()overload, no SPIGraalVMOAPServerStartUp— entry point withconfiguration.has()guards for 6 optional modulesapplication.yml— simplified config for selected providersConfigInitializerGenerator— build-time tool that scans config classes and generatesYamlConfigLoaderUtilsreplacementYamlConfigLoaderUtils— same-FQCN replacement using type-dispatch + setter/VarHandle instead of reflectionModuleDefine— same-FQCN replacement (library-module-for-graalvm) addingprepare(ModuleManager, ModuleProvider, ...)overload for direct provider wiring without ServiceLoader
Same-FQCN Packaging (Repackaged Modules)
Problem
Same-FQCN replacement classes need to shadow upstream originals. Classpath ordering tricks confuse developers and AI tools.
Solution: Per-JAR Repackaged Modules (oap-libs-for-graalvm)
Each upstream JAR that has replacement classes gets a corresponding *-for-graalvm module under oap-libs-for-graalvm/. The module uses maven-shade-plugin to:
- Include only the upstream JAR in the shade
- Exclude the specific
.classfiles being replaced - Produce a JAR containing: all upstream classes MINUS replaced ones PLUS our replacements
oap-graalvm-server depends on *-for-graalvm JARs instead of originals. Original upstream JARs are forced to provided scope via <dependencyManagement> to prevent transitive leakage.
Same-FQCN Replacement Classes
Non-trivial replacements (load pre-compiled assets from manifests):
| Module | Replacement Classes | Purpose |
|---|---|---|
library-module-for-graalvm |
ModuleDefine |
Add prepare() overload for direct provider wiring (bypasses ServiceLoader) |
server-core-for-graalvm |
OALEngineLoaderService, AnnotationScan, SourceReceiverImpl, MeterSystem, CoreModuleConfig, HierarchyDefinitionService, CompiledHierarchyRuleProvider |
Load from manifests instead of Javassist/ClassPath; config with @Setter; pre-compiled hierarchy rules |
library-util-for-graalvm |
YamlConfigLoaderUtils |
Set config fields via setter instead of reflection |
meter-analyzer-for-graalvm |
DSL (v2), Rules |
Load pre-compiled v2 MalExpression classes; load rule data from JSON config-data manifests |
log-analyzer-for-graalvm |
DSL (v2), LogAnalyzerModuleConfig, LALConfigs |
Load pre-compiled v2 LalExpression classes; config with @Setter; load LAL config data from JSON config-data manifests |
agent-analyzer-for-graalvm |
AnalyzerModuleConfig, MeterConfigs |
Config with @Setter; load meter config data from JSON config-data manifests |
Config-only replacements (add @Setter for reflection-free config):
| Module | Replacement Class |
|---|---|
envoy-metrics-receiver-for-graalvm |
EnvoyMetricReceiverConfig |
otel-receiver-for-graalvm |
OtelMetricReceiverConfig |
ebpf-receiver-for-graalvm |
EBPFReceiverModuleConfig |
aws-firehose-receiver-for-graalvm |
AWSFirehoseReceiverModuleConfig |
cilium-fetcher-for-graalvm |
CiliumFetcherConfig |
status-query-for-graalvm |
StatusQueryConfig |
health-checker-for-graalvm |
HealthCheckerConfig |
No Classpath Ordering Required
No duplicate FQCNs on the classpath. The startup script (oapService.sh) uses a simple flat classpath. The oap-graalvm-native uber JAR also has no FQCN conflicts.
Adding New Replacements
To add a new same-FQCN replacement:
- Create a new
*-for-graalvmmodule underoap-libs-for-graalvm/(or add to existing one) - Add the replacement
.javafile with the same FQCN - Configure shade plugin to exclude the original
.classfrom the upstream JAR - Add the
-for-graalvmartifact to rootpom.xml<dependencyManagement> - In
oap-graalvm-server/pom.xml: add the original JAR to<dependencyManagement>asprovided, add-for-graalvmto<dependencies> - Add the original JAR to
distribution.xml<excludes>
Additional GraalVM Risks
| Risk | Status | Mitigation |
|---|---|---|
| Reflection (annotations, OAL enricher, HTTP handlers, GraphQL types) | SOLVED | Auto-generated by precompiler from manifests; log4j2-reflect-config.json for Log4j2 plugins |
| gRPC / Netty / Armeria | SOLVED | GraalVM reachability metadata repo handles these automatically |
Resource loading (ResourceUtils, config files) |
SOLVED | resource-config.json via tracing agent |
| Log4j2 | SOLVED | Console-only log4j2.xml avoids RollingFile reflection chain; Log4j2 plugin classes in log4j2-reflect-config.json |
| Kafka client (for Kafka fetcher) | Untested | Known GraalVM support, may need config |
| Kubernetes client 6.7.1 (for cluster + config) | Untested | Has GraalVM support, may need config at runtime |
Distro Resource Files
Upstream server-starter/src/main/resources/ contains 236 files. They fall into
two categories: files included directly in the distro config/ directory (loaded
at runtime via file I/O), and files consumed by the precompiler at build time
(not needed at runtime — their logic is baked into pre-compiled .class files).
Directly Included in Distro (config/)
These files are loaded at runtime via ResourceUtils.read(), Files.walk(), or
YAML parsing. No reflection involved — safe for GraalVM native image as-is.
| File / Directory | Count | Loaded By | Purpose |
|---|---|---|---|
application.yml |
1 | Custom (distro’s own, not upstream) | Module/provider config |
bydb.yml |
1 | BanyanDBConfigLoader |
BanyanDB storage base config |
bydb-topn.yml |
1 | BanyanDBConfigLoader |
BanyanDB TopN aggregation config |
log4j2.xml |
1 | Log4j2 framework | Logging configuration |
alarm-settings.yml |
1 | AlarmModuleProvider via ResourceUtils.read() |
Alarm rules |
component-libraries.yml |
1 | ComponentLibraryCatalogService via ResourceUtils.read() |
Component ID mapping |
endpoint-name-grouping.yml |
1 | EndpointNameGroupingRuleWatcher via ResourceUtils.read() |
Endpoint grouping rules |
gateways.yml |
1 | UninstrumentedGatewaysConfig via ResourceUtils.read() |
Gateway definitions |
hierarchy-definition.yml |
1 | HierarchyDefinitionService via ResourceUtils.read() |
Layer hierarchy |
metadata-service-mapping.yaml |
1 | ResourceUtils.read() |
Metadata service mapping |
service-apdex-threshold.yml |
1 | ApdexThresholdConfig via ResourceUtils.read() |
APDEX thresholds |
trace-sampling-policy-settings.yml |
1 | TraceSamplingPolicyWatcher via ResourceUtils.read() |
Trace sampling |
ui-initialized-templates/** |
131 | UITemplateInitializer via Files.walk() |
UI dashboard JSON templates |
cilium-rules/** |
2 | CiliumFetcherProvider via ResourceUtils.getPathFiles() |
Cilium flow rules |
openapi-definitions/** |
1 | EndpointNameGrouping via ResourceUtils.getPathFiles() |
OpenAPI grouping definitions |
Total: 146 files included in the distro config/ directory.
Pre-compiled at Build Time (NOT in distro)
These files are consumed by build-tools/precompiler during the build. Their
expressions, scripts, and metric definitions are compiled into .class files
packaged in JARs. The YAML source files are not needed at runtime.
| Category | Count | Pre-compiled Into | Tool |
|---|---|---|---|
oal/*.oal |
9 | ~620 metrics + ~620 builders + ~45 dispatchers (Javassist) | OAL v2 engine |
meter-analyzer-config/*.yaml |
11 | ~147 MalExpression classes (ANTLR4+Javassist) + meter classes |
MAL v2 compiler |
otel-rules/**/*.yaml |
55 | ~1039 MalExpression classes + meter classes |
MAL v2 compiler |
log-mal-rules/*.yaml |
2 | ~2 MalExpression classes |
MAL v2 compiler |
envoy-metrics-rules/*.yaml |
2 | ~26 MalExpression classes + meter classes |
MAL v2 compiler |
telegraf-rules/*.yaml |
1 | ~20 MalExpression classes + meter classes |
MAL v2 compiler |
zabbix-rules/*.yaml |
1 | ~15 MalExpression classes + meter classes |
MAL v2 compiler |
lal/*.yaml |
8 | ~10 LalExpression classes (ANTLR4+Javassist) |
LAL v2 compiler |
hierarchy-definition.yml |
1 | ~4 BiFunction hierarchy rule classes |
Hierarchy v2 compiler |
Total: 90 files consumed at build time, producing ~1285 OAL classes, ~1250 MAL expression classes, ~1188 meter classes, ~10 LAL expression classes, and ~4 hierarchy rule classes.
Additionally, the precompiler serializes parsed config POJOs as JSON manifests in
META-INF/config-data/ (7 JSON files for meter-analyzer-config, otel-rules,
envoy-metrics-rules, log-mal-rules, telegraf-rules, zabbix-rules, and lal). These
provide the runtime “wiring” data (metric prefixes, rule names, expression lookup
keys) that replacement loader classes use instead of filesystem YAML access.
Not Included (upstream-only)
| File | Reason |
|---|---|
application.yml (upstream) |
Replaced by distro’s own simplified application.yml |
Build Workflow
Build System
- Maven + Makefile orchestrates building on top of the skywalking submodule
- GraalVM JDK 25 in CI (
.github/workflows/ci.yml) - JVM-mode starter with fixed module wiring (
FixedModuleManager+GraalVMOAPServerStartUp) - Simplified config file for selected modules (
application.yml)
Build-Time Pre-Compilation
All four DSL compilers (OAL/MAL/LAL/Hierarchy) use ANTLR4 + Javassist v2 engines. The unified precompiler (build-tools/precompiler) runs them all at build time, capturing generated .class files into the output JAR.
OAL: OAL v2 engine exports .class files (9 defines, ~620 metrics, ~620 builders, ~45 dispatchers). 7 annotation/interface manifests. Same-FQCN OALEngineLoaderService loads from manifests.
MAL: MAL v2 compiler processes 71 YAML files → ~1250 MalExpression classes + ~1188 Javassist meter classes. Deterministic class naming via setClassNameHint(). Same-FQCN v2 DSL.java loads pre-compiled classes by computed name.
LAL: LAL v2 compiler processes 8 YAML files → ~10 LalExpression classes. Deterministic class naming. Same-FQCN v2 DSL.java loads pre-compiled classes.
Hierarchy: Hierarchy v2 compiler processes 4 rules → ~4 BiFunction classes. Same-FQCN CompiledHierarchyRuleProvider loads pre-compiled rules.
Config initialization: ConfigInitializerGenerator generates same-FQCN YamlConfigLoaderUtils using Lombok setters — zero Field.setAccessible at runtime.
Config data serialization: Precompiler serializes parsed config POJOs to META-INF/config-data/*.json (7 JSON files). 3 same-FQCN replacement loaders (MeterConfigs, Rules, LALConfigs) deserialize from JSON instead of filesystem YAML.
Module system: ModuleDefine replacement with direct prepare() overload (bypasses ServiceLoader). GraalVMOAPServerStartUp with configuration.has() guards for 6 optional modules.
Distro resource packaging: 146 runtime files → distro config/, 90 pre-compiled files → JARs. Assembly descriptor (distribution.xml) packages runtime config files from upstream.
Native Image Build
native-maven-plugin(GraalVM buildtools 0.10.4) inoap-graalvm-nativewith-Pnativeprofilereflect-config.jsonauto-generated by precompiler from manifests (OAL, MAL, LAL, meter, HTTP handlers, GraphQL types)log4j2-reflect-config.jsonfor Log4j2 plugin classes; console-onlylog4j2.xmlwithSW_LOG_LEVELenv var- gRPC/Netty/Protobuf/Armeria via GraalVM reachability metadata repository
- Auto-scanned reflection metadata: Armeria HTTP handlers (~19), GraphQL resolvers (~32), GraphQL types (~182), config POJOs (8)
- Native binary: ~203MB, boots to full module init with all HTTP endpoints functional
Native Distro Packaging
- Assembly descriptor (
native-distribution.xml) packages native binary + config files Dockerfile.nativepackages native distro intodebian:bookworm-slimdocker-compose.ymlwith BanyanDB + OAP native services- CI pipeline: multi-arch native build (amd64 + arm64) with Docker manifest push to GHCR
Remaining Verification
- Verify all receiver plugins work (gRPC + HTTP endpoints)
- Verify all query APIs work (GraphQL, PromQL, LogQL, Zipkin)
- Verify cluster mode (K8s)
- Verify alarm module
- Performance benchmarking vs JVM
Upstream Changes Tracker
Upstream SkyWalking PR #13723 replaced Groovy with ANTLR4 + Javassist v2 engines for MAL/LAL/Hierarchy. Groovy is completely removed from all production dependencies. All four DSL compilers now share the same pipeline and expose build-time compilation APIs.
No further upstream changes needed. All GraalVM incompatibilities are resolved in this distro via same-FQCN replacement and build-time pre-compilation:
- OAL: build-time class export works via existing debug API (
setGeneratedFilePath()) - MAL: build-time compilation via
MALClassGenerator.setClassOutputDir() - LAL: build-time compilation via
LALClassGenerator.setClassOutputDir() - Hierarchy: build-time compilation via
HierarchyRuleClassGenerator.setClassOutputDir()