An OpenTelemetry Java agent
extension that adds flagd-controlled INTERNAL span suppression to any Java
service — the Java counterpart to the Python
otelfeature-instrument.
The OTel Java agent auto-instruments libraries and produces spans of various
kinds: SERVER (incoming HTTP), CLIENT (outgoing HTTP/DB), and INTERNAL
(e.g. Spring WebMVC controller spans). This extension wraps the agent's
SpanProcessor and filters out INTERNAL spans when the telemetryLevel
flag served by flagd is set to "IO".
telemetryLevel |
INTERNAL spans |
SERVER / CLIENT spans |
|---|---|---|
"FULL" (default) |
✅ exported | ✅ exported |
"IO" |
❌ suppressed | ✅ exported |
Flip the flag in flagd and the change takes effect within seconds — no restart, no redeploy.
┌──────────────────────────────────────────────────────────────┐
│ JVM + OTel Java Agent │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Instrumented │──▶│ Filtering │──▶│ BatchSpan │ │
│ │ libraries │ │ SpanProcessor │ │ Processor │ │
│ └──────────────┘ │ (this extension) │ └──────┬───────┘ │
│ └────────┬─────────┘ │ │
│ │ ▼ │
│ ┌──────▼───────┐ ┌──────────────┐ │
│ │ FlagdClient │ │ SpanExporter │ │
│ │ (polls every │ │ → OTLP to │ │
│ │ 5 seconds) │ │ collector │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ └──HTTP──▶ flagd :8016 (OFREP) │
└──────────────────────────────────────────────────────────────┘
The extension is discovered via the OTel Java agent's
AutoConfigurationCustomizerProvider SPI.
At agent startup:
- The agent scans
META-INF/services/on the classpath/extension path. - It finds
OtelfeatureCustomizerand callscustomize(). - The customizer wraps the auto-configured
SpanProcessorwithFilteringSpanProcessorviaaddSpanProcessorCustomizer. FlagdClientstarts polling flagd's OFREP REST endpoint (port 8016) every 5 seconds for thetelemetryLevelflag. The first poll runs asynchronously on a background thread — agent startup is not blocked.- On each span's
onEnd(),FilteringSpanProcessorchecks the cached flag value and dropsINTERNALspans before they enter the batch queue. All other span kinds are passed through unchanged.
Filtering at the SpanProcessor level (before batching) means dropped spans
never enter the BatchSpanProcessor queue, saving memory and CPU compared
to exporter-level filtering. Children of suppressed INTERNAL spans are
still exported — they retain their original parent span ID, which trace
backends handle gracefully (same as any sampling scenario).
No code changes to the service — just attach the extension JAR alongside the agent.
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.javaagent.extensions=otelfeature-java-extension.jar \
-jar your-app.jarOr via environment variable:
OTEL_JAVAAGENT_EXTENSIONS=/path/to/otelfeature-java-extension.jar
java -javaagent:opentelemetry-javaagent.jar -jar your-app.jar| Environment variable | Default | Description |
|---|---|---|
FLAGD_HOST |
flagd |
flagd host |
FLAGD_PORT |
8016 |
flagd OFREP REST port |
FLAGD_POLL_INTERVAL_SECONDS |
5 |
How often to poll flagd for flag changes |
OTELFEATURE_FLAG_NAME |
telemetryLevel |
flagd flag key to evaluate |
If flagd is unreachable, the extension defaults to no suppression (all spans exported) and keeps the last known value on transient errors.
gradle build --no-daemon
# Output: build/libs/otelfeature-java-extension.jarRequires Java 21+ and Gradle 8+.
The extension has zero runtime dependencies beyond the JDK and the OTel Java agent (which provides the SDK at runtime). It uses only:
java.net.HttpURLConnection(JDK 11+) for flagd communicationjava.util.regexfor lightweight JSON parsingjava.util.concurrentfor the polling schedulerjava.util.logging.Logger(part of the JDK)
The OTel SDK and autoconfigure SPI are compileOnly dependencies — they're
provided by the agent when the extension is loaded.
| Aspect | Python (otelfeature-instrument) |
Java (otelfeature-java-extension) |
|---|---|---|
| Delivery mechanism | pip package + CLI launcher | Agent extension JAR via SPI |
| Instrumentation approach | SDK configurator | -javaagent bytecode instrumentation |
| flagd connection | gRPC (in-process resolver, port 8015) | HTTP (OFREP REST, port 8016) |
| Span suppression | Configures tracer to not emit INTERNAL spans | Filters INTERNAL spans at SpanProcessor level (before batching) |
| Flag updates | Push (gRPC sync stream) | Poll (every 5 seconds) |
| Service code changes | None | None |
Both achieve the same result: flagd-controlled INTERNAL span suppression
with zero service code changes. The delivery mechanism is idiomatic for each
language.