docs/en/academy/dsl-compiler-design.md
SkyWalking OAP server uses four domain-specific languages (DSLs) for telemetry analysis. All four share the same compilation tech stack: ANTLR4 for grammar parsing and Javassist for runtime bytecode generation.
| DSL | Purpose | Input | Generated Output |
|---|---|---|---|
| OAL (Observability Analysis Language) | Trace/mesh metrics aggregation | .oal script files | Metrics classes, builders, dispatchers |
| MAL (Meter Analysis Language) | Meter/metrics expression evaluation | YAML config exp fields | MalExpression implementations |
| LAL (Log Analysis Language) | Log processing pipelines | YAML config filter blocks | LalExpression implementations |
| Hierarchy Matching Rules | Service hierarchy relationship matching | YAML config expressions | BiFunction<Service, Service, Boolean> implementations |
All four DSLs follow the same three-phase compilation pipeline at OAP startup:
DSL string (from .oal script or YAML config)
|
v
Phase 1: ANTLR4 Parsing
Lexer + Parser (generated from .g4 grammars at build time)
→ Immutable AST model
|
v
Phase 2: Java Source Generation
Walk AST model, emit Java source code as strings
|
v
Phase 3: Javassist Bytecode Generation
ClassPool.makeClass() → CtClass → addMethod(source) → toClass()
→ Ready-to-use class instance loaded into JVM
| DSL | Interface / Base Class | Key Method |
|---|---|---|
| OAL | Extends metrics function class (e.g., LongAvgMetrics) | id(), serialize(), deserialize(), plus dispatcher dispatch(source) |
| MAL metric | MalExpression | SampleFamily run(Map<String, SampleFamily> samples) |
| MAL filter | Predicate<Map<String, String>> | boolean test(Map<String, String> tags) |
| LAL | LalExpression | void execute(FilterSpec filterSpec, ExecutionContext ctx) |
| Hierarchy | BiFunction<Service, Service, Boolean> | Boolean apply(Service upper, Service lower) |
OAL is the most complex -- it generates three classes per metric (metrics class with storage annotations, metrics builder for serialization, and source dispatcher for routing), whereas MAL/LAL/Hierarchy each generate a single functional class per expression.
Each DSL has its own ANTLR4 lexer and parser grammar. The Maven ANTLR4 plugin generates Java lexer/parser classes at build time; these are then used at runtime to parse DSL strings.
| DSL | Grammar Location |
|---|---|
| OAL | oap-server/oal-grammar/src/main/antlr4/.../OALLexer.g4, OALParser.g4 |
| MAL | oap-server/analyzer/meter-analyzer/src/main/antlr4/.../MALLexer.g4, MALParser.g4 |
| LAL | oap-server/analyzer/log-analyzer/src/main/antlr4/.../LALLexer.g4, LALParser.g4 |
| Hierarchy | oap-server/analyzer/hierarchy/src/main/antlr4/.../HierarchyRuleLexer.g4, HierarchyRuleParser.g4 |
Javassist compiles Java source strings into bytecode but has limitations that shape the code generation:
execute() instead of Consumer callbacks.
OAL pre-compiles callbacks as separate CtClass instances where needed.PackageHolder marker class so that
ctClass.toClass(PackageHolder.class) loads the generated class into the correct module/package
(required for JDK 9+ module system).OAL additionally uses FreeMarker templates to generate method bodies for metrics classes, builders, and dispatchers, since these classes are more complex and benefit from template-driven generation.
oap-server/
oal-grammar/ # OAL: ANTLR4 grammar
oal-rt/ # OAL: compiler + runtime (Javassist + FreeMarker)
analyzer/
meter-analyzer/ # MAL: grammar + compiler + runtime
log-analyzer/ # LAL: grammar + compiler + runtime
hierarchy/ # Hierarchy: grammar + compiler + runtime
agent-analyzer/ # Calls MAL compiler for meter data
OAL keeps grammar and runtime in separate modules (oal-grammar and oal-rt) because server-core
depends on the grammar while the runtime implementation depends on server-core (avoiding circular
dependency). MAL, LAL, and Hierarchy are each self-contained in a single module.
Reference: Discussion #13716
MAL, LAL, and Hierarchy previously used Groovy as the runtime scripting engine. OAL has always used ANTLR4 + Javassist. The Groovy-based DSLs were replaced for the following reasons:
Startup cost -- 1,250+ GroovyShell.parse() calls at OAP boot, each spinning up the full Groovy
compiler pipeline.
Runtime execution overhead -- MAL expressions execute on every metrics ingestion cycle. Per-expression
overhead from dynamic Groovy compounds at scale: property resolution through 4+ layers of indirection,
ExpandoMetaClass closure allocation for simple arithmetic, and megamorphic call sites that defeat JIT
optimization.
Late error detection -- MAL uses dynamic Groovy; typos in metric names or invalid method chains are only discovered when that specific expression runs with real data.
Debugging complexity -- Stack traces include Groovy MOP internals (CallSite, MetaClassImpl,
ExpandoMetaClass), obscuring the actual expression logic.
GraalVM incompatibility -- invokedynamic bootstrapping and ExpandoMetaClass are fundamentally
incompatible with ahead-of-time (AOT) compilation, blocking the
GraalVM native-image distribution.
The DSL grammar for users remains 100% unchanged -- the same expressions written in YAML config files work exactly as before. Only the internal compilation engine was replaced.
To ensure the new Java compilers produce identical results to the original Groovy implementation,
a dual-path comparison test suite is maintained under test/script-cases/:
test/script-cases/
scripts/
mal/ # Copies of shipped MAL configs (test-otel-rules, test-meter-analyzer-config, etc.)
lal/ # Copies of shipped LAL scripts (test-lal/)
hierarchy-rule/ # Copy of shipped hierarchy-definition.yml
script-runtime-with-groovy/
mal-v1-with-groovy/ # MAL v1: original Groovy-based implementation
lal-v1-with-groovy/ # LAL v1: original Groovy-based implementation
hierarchy-v1-with-groovy/ # Hierarchy v1: original Groovy-based implementation
mal-lal-v1-v2-checker/ # Runs every MAL/LAL expression through BOTH v1 and v2, compares results
hierarchy-v1-v2-checker/ # Runs every hierarchy rule through BOTH v1 and v2, compares results
The checker mechanism:
test/script-cases/scripts/SampleFamily input data from ExpressionMetadata,
executes with both v1 and v2, compares output samples (count, labels, values with epsilon).
For increase()/rate() expressions, the CounterWindow is primed with an initial run before
comparing the second run's output..input.data files
and the LALSourceTypeProvider SPI resolves the proto type per layer.
Test scripts include both copies of production configs (oap-cases/) and
dedicated feature-coverage rules (feature-cases/).BiFunction evaluation with test Service pairsThis ensures 100% behavioral parity. The Groovy v1 modules are test-only dependencies -- they are not included in the OAP distribution.