You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/04/16 03:57:27 UTC
(skywalking) 01/01: Update SWIP-11 to reflect actual SpanListener design
This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch docs/swip11-update
in repository https://gitbox.apache.org/repos/asf/skywalking.git
commit ab866242f0fcc8b402319f03ca33e1b87a3b6abe
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu Apr 16 11:56:49 2026 +0800
Update SWIP-11 to reflect actual SpanListener design
- Section 2: describe sourceAttribute as current state, not proposal
- Section 3: add abort vs drop semantics, GENERAL fallback
- Section 5: rewrite to match actual SpanListener SPI — two-phase
design, OTLPSpanReader, requiredModules(), SpanListenerManager,
core.trace package, lazy init
- Section 9: update interface references
- Architecture graph: reflect SpanListener SPI structure
Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
docs/en/swip/SWIP-11.md | 307 ++++++++++++------------------------------------
1 file changed, 74 insertions(+), 233 deletions(-)
diff --git a/docs/en/swip/SWIP-11.md b/docs/en/swip/SWIP-11.md
index 98d1629ace..5cd2e91547 100644
--- a/docs/en/swip/SWIP-11.md
+++ b/docs/en/swip/SWIP-11.md
@@ -26,20 +26,19 @@ This SWIP also establishes a **Mobile** menu group in the UI, preparing for futu
│ │ (port 4318) │ ┌───────────────────────────────────────┐ │
│ Instrumentation: │ │ │ otel-receiver │ │
│ • URLSession (auto) │ │ │ │ │
-│ • MetricKit (auto) │ │ │ Trace Handler │ │
-│ │ │ │ ├─ detect os.name=iOS → Layer.IOS │ │
-│ Signals: │ │ │ ├─ HTTP spans → SpanForward → OAL │ │
-│ 1. HTTP trace spans │ │ │ └─ MetricKit spans │ │
-│ 2. MetricKit spans │ │ │ → iOSMetricKitAnalyzer (new) │ │
-│ 3. Diagnostic logs │ │ │ → extract as metrics │ │
+│ • MetricKit (auto) │ │ │ Trace Handler + SpanListener SPI │ │
+│ │ │ │ ├─ IOSLayerSpanListener (onOTLPSpan) │ │
+│ Signals: │ │ │ │ → detect os.name=iOS → Layer.IOS │ │
+│ 1. HTTP trace spans │ │ │ ├─ IOSMetricKitSpanListener │ │
+│ 2. MetricKit spans │ │ │ │ → extract stats → MAL, skip trace│ │
+│ 3. Diagnostic logs │ │ │ └─ HTTP spans → SpanForward → OAL │ │
│ │ │ │ │ │
-│ Resource attrs: │ │ │ Log Handler (modified) │ │
-│ os.name=iOS │ │ │ ├─ pass resource attrs as │ │
-│ device.model.id=... │ │ │ │ sourceAttributes on LogMetadata │ │
-│ service.name=MyApp │ │ │ └─ LAL script determines layer │ │
-│ (NO service.layer) │ │ │ from sourceAttribute("os.name") │ │
-└──────────────────────┘ │ └───────────────────────────────────────┘ │
- └─────────────────────────────────────────────┘
+│ Resource attrs: │ │ │ Log Handler │ │
+│ os.name=iOS │ │ │ ├─ sourceAttributes on LogMetadata │ │
+│ device.model.id=... │ │ │ └─ LAL (layer: auto) determines layer │ │
+│ service.name=MyApp │ │ │ from sourceAttribute("os.name") │ │
+│ (NO service.layer) │ │ └───────────────────────────────────────┘ │
+└──────────────────────┘ └─────────────────────────────────────────────┘
```
No OTel Collector is required, though one can be used for buffering.
@@ -65,125 +64,23 @@ Normal layer (`isNormal=true`) because the iOS app is directly instrumented.
### 2. Source Attributes on LogMetadata (General Enhancement)
-OTLP resource attributes (e.g., `os.name`, `device.model.identifier`) are currently extracted by
-`OpenTelemetryLogHandler` to read `service.name`, `service.layer`, `service.instance.id`, then
-**discarded**. They are not passed into LogData tags and not available to LAL scripts.
+`LogMetadata` carries a non-persistent `Map<String, String> sourceAttributes` field for
+receiver-level context. `OpenTelemetryLogHandler` passes all OTLP resource attributes into
+this field. LAL scripts access them via `sourceAttribute("key")` — values are available during
+LAL processing but NOT stored in `tagsRawData`. Use `tag 'key': sourceAttribute("attr")` in
+the extractor to selectively persist specific attributes.
-This is a problem not only for iOS but for any OTLP source where `service.layer` is absent — the
-LAL script has no information to determine the layer.
+### 3. LAL `layer: auto` Mode
-#### Solution: `sourceAttributes` on `LogMetadata`
+Rules with `layer: auto` match logs where `service.layer` is absent. The script determines the
+layer in the extractor. In auto mode, `abort` and `drop` have distinct meanings:
+- **`abort {}`** — "not mine, let others try." If all auto rules abort, the log falls back to
+ `GENERAL` layer processing automatically.
+- **`drop {}`** — "mine but sampled out." The rule claimed the log (no GENERAL fallback) but
+ chose not to persist it.
-Add a non-persistent `sourceAttributes` field to `LogMetadata` (Java bean, not proto):
-
-```java
-@Data
-@Builder
-public class LogMetadata {
- private String service;
- private String serviceInstance;
- private String endpoint;
- private String layer;
- private long timestamp;
- @Builder.Default
- private TraceContext traceContext = TraceContext.EMPTY;
-
- /**
- * Non-persistent attributes from the log source (e.g., OTLP resource attributes,
- * ALS node context). Available to LAL scripts via sourceAttribute() but NOT stored
- * in tagsRawData.
- */
- @Builder.Default
- private Map<String, String> sourceAttributes = Collections.emptyMap();
-}
-```
-
-**Why `sourceAttributes` not `resourceAttributes`:** Different receivers have different source
-contexts — OTLP has resource attributes, Envoy ALS has node info, etc. `sourceAttributes` is
-generic.
-
-**Why on `LogMetadata` not `LogData`:** `LogData` is a proto object (from `Logging.proto`). Its
-`tags` field gets serialized into `tagsRawData` and persisted to storage. `LogMetadata` is a Java
-bean used only as a transient carrier during LAL processing — adding fields here has no storage
-impact.
-
-#### Handler Change: `OpenTelemetryLogHandler`
-
-Pass all resource attributes into `LogMetadata.sourceAttributes`:
-
-```java
-// Existing: extract specific fields from resource attributes
-final var service = attributes.get("service.name");
-final var layer = attributes.getOrDefault("service.layer", "");
-final var serviceInstance = attributes.getOrDefault("service.instance.id", "");
-
-// New: pass ALL resource attributes as sourceAttributes
-final var metadata = LogMetadata.builder()
- .service(service)
- .serviceInstance(serviceInstance)
- .layer(layer)
- .timestamp(logRecord.getTimeUnixNano() / 1_000_000)
- .sourceAttributes(attributes) // <-- all resource attrs, non-persistent
- .build();
-logAnalyzerService().doAnalysis(metadata, logDataBuilder);
-```
-
-#### LAL DSL: `sourceAttribute()` Function
-
-Add a new function to the LAL DSL that reads from `LogMetadata.sourceAttributes`:
-
-```
-sourceAttribute("os.name") → "iOS"
-sourceAttribute("os.version") → "17.4.1"
-sourceAttribute("device.model.identifier") → "iPhone15,2"
-```
-
-This is similar to `tag()` but reads from the non-persistent source context instead of LogData tags.
-
-### 3. LAL Script-Level Layer Assignment (`layer: auto`)
-
-Currently, `layer` in a LAL rule YAML serves as both a routing key (only rules matching the log's
-layer are evaluated) and output metadata. This creates a chicken-and-egg problem: a rule that wants
-to SET the layer cannot be reached if the layer is absent.
-
-#### Solution: `layer: auto` mode
-
-A new `layer: auto` declaration indicates the layer is **determined by the script**. Rules with
-`layer: auto` match logs where `service.layer` is absent (empty/unset). The script is expected to
-set the layer in the extractor:
-
-```yaml
-rules:
- - name: ios-metrickit-diagnostics
- layer: auto # layer determined by script; dropped if not set
- dsl: |
- filter {
- // Determine if this is an iOS log
- if (sourceAttribute("os.name") != "iOS" && sourceAttribute("os.name") != "iPadOS") {
- abort {}
- }
-
- extractor {
- layer IOS # LAL script sets the layer
- // ...
- }
- sink { }
- }
-```
-
-**Drop policy:** In `auto` mode, if the script does not set the layer (either because the script
-aborted or because the extractor omitted `layer`), the log is **warned and dropped** at persistence.
-`layer: auto` means "I take responsibility for setting the layer" — if no layer is set, it's either
-a non-matching log (abort) or a script bug (warn).
-
-This enforces that every OTLP log source either:
-1. Sets `service.layer` explicitly (like Envoy AI Gateway), OR
-2. Has a matching `layer: auto` LAL rule that determines the layer from source attributes
-
-**Backward compatibility:** Existing OTLP log sources that set `service.layer` are unaffected —
-their logs have a concrete layer and are routed to layer-specific rules as before. `layer: auto`
-rules only see logs with absent layer. The existing `default.yaml` rule (`layer: GENERAL`) continues
-to catch logs that have `layer = GENERAL`.
+If an auto rule claims the log (doesn't abort) but doesn't set a layer, the log is warned and
+dropped at persistence time.
### 4. Resource Attributes Available to LAL (via `sourceAttribute()`)
@@ -199,126 +96,69 @@ The OTel Swift SDK sets the following resource attributes, all available via `so
| `service.version` | `2.1.0 (45)` | `CFBundleShortVersionString` + build |
| `telemetry.sdk.language` | `swift` | Hardcoded |
-### 5. OTLP Span Listener Mechanism (General Enhancement)
-
-Currently, `SpanForward` hardcodes GenAI-specific logic (`processGenAILogic()`) inline. Adding iOS
-MetricKit handling as another hardcoded case would be unmaintainable. This SWIP introduces a general
-**span listener** mechanism to support extensible span-based metric extraction and trace persistence
-control.
+### 5. Two-Phase SpanListener SPI (General Enhancement)
-#### Current Problem
+A `SpanListener` SPI in the `core.trace` package provides extensible trace span processing with
+two phases:
-```java
-// OpenTelemetryTraceHandler.java — converts ALL spans to Zipkin first
-Span zipkinSpan = convertSpan(span, serviceName, resourceTags);
-result.add(zipkinSpan);
-// ...
-// SpanForward.java — hardcoded GenAI logic after Zipkin conversion
-processGenAILogic(zipkinSpan); // GenAI metric extraction (hardcoded)
-getReceiver().receive(zipkinSpan); // always persists trace
-```
+- **Phase 1 (`onOTLPSpan`)**: called by `OpenTelemetryTraceHandler` before Zipkin conversion.
+ Only OTLP spans reach this phase. Listeners can veto persistence (skip Zipkin conversion),
+ inject tags, or extract metrics. The span is accessed via `OTLPSpanReader` (an interface in
+ `server-core` that abstracts the OTLP proto, avoiding a proto dependency in core).
-Problems:
-1. GenAI logic is hardcoded — adding iOS/Android/etc. would keep growing
-2. Listeners can't see original OTLP structure (InstrumentationScope name, resource attributes
- as separate fields) — everything is already flattened into Zipkin tags
-3. Spans that should NOT be persisted (e.g., 24-hour MetricKit) still get converted to Zipkin format
+- **Phase 2 (`onZipkinSpan`)**: called by `SpanForward` after Zipkin conversion. All trace
+ sources (OTLP, Zipkin HTTP, Kafka) reach this phase.
-#### Solution: `OTLPSpanListener` Interface — Before Zipkin Conversion
-
-Listeners operate on the **raw OTLP span + resource attributes**, before Zipkin conversion.
-This gives listeners access to:
-- `InstrumentationScope` name and version (lost in Zipkin conversion)
-- Resource attributes as a separate map (not flattened with span attributes)
-- Original OTLP span structure
+Both methods are `default` no-ops. Listeners implement one or both.
```java
-/**
- * Listener for OTLP spans. Called BEFORE Zipkin conversion.
- * Implementations can:
- * 1. Extract metrics or other data from spans
- * 2. Modify resource/span attributes before Zipkin conversion
- * 3. Control whether the span should be converted and persisted as a trace
- */
-public interface OTLPSpanListener {
- /**
- * Process an OTLP span.
- *
- * @param span the raw OTLP span
- * @param resourceAttributes resource-level attributes (service.name, os.name, etc.)
- * @param scopeName InstrumentationScope name (e.g., "NSURLSession", "MetricKit")
- * @param scopeVersion InstrumentationScope version
- * @return result controlling persistence and tag modifications
- */
- OTLPSpanListenerResult onSpan(
- io.opentelemetry.proto.trace.v1.Span span,
- Map<String, String> resourceAttributes,
- String scopeName,
- String scopeVersion
- );
-}
+public interface SpanListener {
+ /** Module names this listener requires. Checked before init — skipped if not met. */
+ default String[] requiredModules() { return new String[0]; }
-public class OTLPSpanListenerResult {
- /** Whether this span should be converted to Zipkin and persisted. Default: true */
- private boolean persistTrace = true;
+ void init(ModuleManager moduleManager);
- /** Additional tags to inject before Zipkin conversion (e.g., estimated_cost) */
- private Map<String, String> additionalTags = Collections.emptyMap();
+ /** Phase 1: OTLP-only, before Zipkin conversion. */
+ default SpanListenerResult onOTLPSpan(OTLPSpanReader span,
+ Map<String, String> resourceAttributes,
+ String scopeName, String scopeVersion) {
+ return SpanListenerResult.CONTINUE;
+ }
- /** Layer override — if set, the service is assigned this layer */
- private Layer layer = null;
+ /** Phase 2: all trace sources, after Zipkin conversion. */
+ default SpanListenerResult onZipkinSpan(ZipkinSpan span) {
+ return SpanListenerResult.CONTINUE;
+ }
}
```
-#### Revised Flow in `OpenTelemetryTraceHandler`
+`SpanListenerResult` controls persistence and tag injection. Listeners emit sources (OAL, MAL,
+logs) directly via their cached `ModuleManager` references.
-```java
-// OpenTelemetryTraceHandler.java — listeners BEFORE Zipkin conversion
-for (io.opentelemetry.proto.trace.v1.Span span : scopeSpans.getSpansList()) {
- boolean shouldPersist = true;
- Map<String, String> extraTags = new HashMap<>();
- Layer layerOverride = null;
-
- for (OTLPSpanListener listener : spanListeners) {
- OTLPSpanListenerResult result = listener.onSpan(
- span, resourceTags, scopeName, scopeVersion);
- if (!result.isPersistTrace()) {
- shouldPersist = false;
- }
- extraTags.putAll(result.getAdditionalTags());
- if (result.getLayer() != null) {
- layerOverride = result.getLayer();
- }
- }
+`SpanListenerManager` is a core service that discovers listeners via `ServiceLoader`, checks
+`requiredModules()` against loaded modules, and dispatches both phases. Lazy-initialized on first
+span arrival to ensure all modules are fully started.
- if (shouldPersist) {
- // Merge extraTags into resourceTags before Zipkin conversion
- resourceTags.putAll(extraTags);
- Span zipkinSpan = convertSpan(span, serviceName, resourceTags);
- result.add(zipkinSpan);
- }
-}
+```
+OTLP: TraceHandler → manager.notifyOTLPPhase() → [veto?] → convert → SpanForward → manager.notifyZipkinPhase()
+Zipkin: SpanForward → manager.notifyZipkinPhase()
+Kafka: SpanForward → manager.notifyZipkinPhase()
```
#### Registered Listeners
-| Listener | Detects | Extracts | Persists? | Modifies? |
-|---|---|---|---|---|
-| `GenAISpanListener` | `gen_ai.system` or `gen_ai.provider.name` attribute | Token metrics, cost → Sources | Yes | Yes (adds `estimated_cost` tag) |
-| `IOSMetricKitSpanListener` | `scopeName == "MetricKit"` + `span.name == "MXMetricPayload"` | Device stats → SampleFamily → MAL | **No** | No |
-| `IOSLayerSpanListener` | resource `os.name == "iOS"` or `"iPadOS"` | None (layer assignment only) | Yes | No (sets `layer = Layer.IOS`) |
-
-Listeners are registered via SPI (`META-INF/services/`) and loaded at handler initialization.
-The existing `processGenAILogic()` is refactored into `GenAISpanListener` — no behavior change,
-just better structure.
+| Listener | Phase | Detects | Persists? |
+|---|---|---|---|
+| `GenAISpanListener` | onZipkinSpan | `gen_ai.response.model` tag | Yes (adds `estimated_cost` tag) |
+| `IOSMetricKitSpanListener` | onOTLPSpan | `scopeName == "MetricKit"` + `spanName == "MXMetricPayload"` | **No** (24h stats span) |
+| `IOSLayerSpanListener` | onOTLPSpan | resource `os.name == "iOS"` or `"iPadOS"` | Yes |
**Key design points:**
-- Listeners see raw OTLP data — InstrumentationScope name, resource attributes as separate map
-- Any listener can veto trace persistence — prevents Zipkin conversion entirely (no wasted work)
-- Any listener can inject tags — merged before Zipkin conversion
-- Any listener can set layer — applies to service registration
-- Multiple listeners can process the same span (e.g., a GenAI span on iOS triggers both)
-- If ANY listener vetoes persistence, the span is not converted or stored
+- `OTLPSpanReader` in `server-core` abstracts the OTLP span proto — no proto dependency in core
+- Listeners registered via SPI (`META-INF/services/org.apache.skywalking.oap.server.core.trace.SpanListener`)
+- `requiredModules()` prevents startup failures when optional modules are not loaded
+- Additional tags use last-writer-wins if multiple listeners set the same key
+- If ANY listener vetoes persistence in phase 1, the span is not converted to Zipkin or stored
### 6. Entity Model
@@ -431,11 +271,12 @@ Apple's MetricKit delivers pre-aggregated app statistics once per day. The OTel
this as a single span with `startTime = 24h ago`, `endTime = now`, with all statistics as span
attributes. These are **not trace spans** — they must be intercepted and converted to metrics.
-`IOSMetricKitSpanListener` implements the `OTLPSpanListener` interface (Section 5):
-- **Detection:** `scopeName == "MetricKit"` AND `span.getName() == "MXMetricPayload"` — uses the
- raw OTLP InstrumentationScope name, available because listeners run before Zipkin conversion
+`IOSMetricKitSpanListener` implements `SpanListener.onOTLPSpan()` (Section 5):
+- **Detection:** `scopeName == "MetricKit"` AND `span.spanName() == "MXMetricPayload"` — uses the
+ raw OTLP InstrumentationScope name via `OTLPSpanReader`, available because phase 1 runs before
+ Zipkin conversion
- **Action:** Extract span attributes as `SampleFamily` samples with labels, feed into MAL pipeline
-- **Persistence:** Returns `persistTrace = false` — a 24-hour span must not be stored as a trace
+- **Persistence:** Returns `shouldPersist = false` — a 24-hour span must not be stored as a trace
#### MetricKit Source Attributes