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:26 UTC
(skywalking) branch docs/swip11-update created (now ab866242f0)
This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a change to branch docs/swip11-update
in repository https://gitbox.apache.org/repos/asf/skywalking.git
at ab866242f0 Update SWIP-11 to reflect actual SpanListener design
This branch includes the following new commits:
new ab866242f0 Update SWIP-11 to reflect actual SpanListener design
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(skywalking) 01/01: Update SWIP-11 to reflect actual SpanListener design
Posted by wu...@apache.org.
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