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