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/20 11:03:21 UTC
(skywalking) branch master updated: SWIP-12 design + UITemplateInitializer auto-discovery & dev hot-reload (#13834)
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new 681a5be8cb SWIP-12 design + UITemplateInitializer auto-discovery & dev hot-reload (#13834)
681a5be8cb is described below
commit 681a5be8cb1d4bb77eb40a0d079391ade439a64c
Author: 吴晟 Wu Sheng <wu...@foxmail.com>
AuthorDate: Mon Apr 20 19:03:16 2026 +0800
SWIP-12 design + UITemplateInitializer auto-discovery & dev hot-reload (#13834)
**1. SWIP-12 design doc** (`docs/en/swip/SWIP-12.md`)
Proposes WeChat & Alipay Mini-Program monitoring as a new pair of `Layer` values. Covers SDK alignment (histogram bucket unit), native-trace SegmentListener SPI, entity model, layer partitioning, dashboards layout, and MAL/OAL scope split. Still a design-only proposal — implementation lands in follow-up PRs.
**2. UITemplateInitializer extensibility + dev hot-reload**
- `UI_TEMPLATE_FOLDER` is now computed from `Layer.values() + "custom"` at class-init time. Adding a new `Layer` enum value is enough — drop a `ui-initialized-templates/<layer-name-lowercased>/` folder on disk and it's scanned on the next boot. Removes the prior hardcoded allowlist that was easy to miss.
- `SW_UI_TEMPLATE_FORCE_RELOAD` env var switches the initializer from `addIfNotExist` to a new `addOrReplace` helper on `UITemplateManagementService`. When true, shipped templates overwrite any seeded copy every boot — so dev/extension edits show up after a simple OAP restart without wiping storage. Unset / false preserves the production behavior where operator UI edits persist.
- `UITemplateCheckerTest` updated to tolerate missing folders (several `Layer` values have no template folders today).
**3. `new-monitoring-feature` skill** (`.claude/skills/new-monitoring-feature/SKILL.md`)
A wiring map for contributors adding a new layer: which extension point handles which signal (OAL / MAL / LAL / SpanListener / SegmentListener), where contracts live, UI template + submodule touchpoints, and cross-cutting traps.
---
.claude/skills/new-monitoring-feature/SKILL.md | 333 ++++++++
docs/en/swip/SWIP-12.md | 858 +++++++++++++++++++++
docs/en/swip/readme.md | 3 +-
.../ui/template/UITemplateInitializer.java | 83 +-
.../ui/template/UITemplateManagementService.java | 15 +
.../oap/server/starter/UITemplateCheckerTest.java | 12 +-
6 files changed, 1256 insertions(+), 48 deletions(-)
diff --git a/.claude/skills/new-monitoring-feature/SKILL.md b/.claude/skills/new-monitoring-feature/SKILL.md
new file mode 100644
index 0000000000..ef87ca7ef8
--- /dev/null
+++ b/.claude/skills/new-monitoring-feature/SKILL.md
@@ -0,0 +1,333 @@
+---
+name: new-monitoring-feature
+description: Add a new monitoring target / layer to SkyWalking OAP. Orients you to the OAL / MAL / LAL / SpanListener / SegmentListener extension points, the UI template + submodule touchpoints, the docs set that must move together, and the cross-cutting traps that don't live in any one skill.
+---
+
+# Adding a New Monitoring Feature / Layer
+
+SkyWalking monitoring for a new target (a new `Layer` — mobile OS, gateway, DB, language runtime, …) is composed from the same set of reusable extension points. This skill is the index: which extension point fits which signal, where its contract lives, which docs must be updated together, and which harness-wide traps the extension-specific docs don't cover.
+
+There is one skill per narrow concern. This one is the wiring map.
+
+---
+
+## 0. Register the `Layer` — the feature's entry point
+
+A `Layer` is how OAP slices services / instances / endpoints by data source. **Every new feature needs a new `Layer` enum value.** The UI, storage partitioning, menu navigation, and OAL aggregation all key off it.
+
+**Only one place to edit** — `oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/Layer.java`. Add a new enum constant with a unique id and `normal` flag. Ids are never reused; pick the next integer. Examples: `IOS(47, true)`, `APISIX(27, true)`, `VIRTUAL_DATABASE(11, false)` for inferred/non-real services.
+
+UI template folders are auto-discovered: `UITemplateInitializer.UI_TEMPLATE_FOLDER` is computed from `Layer.values()` + `"custom"` at class-init time. Drop a `ui-initialized-templates/<layer-name-lowercased>/` folder on disk and the initializer picks it up on the next boot. Missing folders are silently skipped. There is no allowlist to append to.
+
+**Component ID lookup in Java code**: IDs declared in `component-libraries.yml` are loaded at runtime into `ComponentLibraryCatalogService`'s `componentName2Id` map — they are **not** exposed as Java enum constants. To look up by name in listener code, inject the catalog service and resolve once at construction:
+```java
+IComponentLibraryCatalogService catalog = moduleManager.find(CoreModule.NAME)
+ .provider().getService(IComponentLibraryCatalogService.class);
+int myComponentId = catalog.getComponentId("My-Component-Name");
+```
+Cache as an `int` field; runtime comparisons are then plain `componentId == myComponentId`. **Trap:** there is a `ComponentsDefine` class under `skywalking-trace-receiver-plugin/src/test/java/.../mock/ComponentsDefine.java` — it is a test-only mock holding five hand-picked constants (Tomcat, Dubbo, RocketMQ, MongoDB). Do not import or extend it from production code.
+
+Emit the layer from every source object your feature produces:
+```java
+service.setLayer(Layer.<YOUR_LAYER>);
+serviceInstance.setServiceLayer(Layer.<YOUR_LAYER>);
+endpoint.setServiceLayer(Layer.<YOUR_LAYER>);
+```
+
+Downstream (the core OAL, `service ly <LAYER>` swctl query, topology filters, UI root dashboard's layer selector) all work off this single enum value.
+
+---
+
+## 1. Pick the right extension point
+
+| Signal you want to process | Extension point | Register via |
+|---|---|---|
+| **SkyWalking native trace segments** (from language agents) | `AnalysisListener` (TraceSegmentListener / EntryAnalysisListener / ExitAnalysisListener / LocalAnalysisListener / FirstAnalysisListener) + factory in `agent-analyzer` | `META-INF/services/org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.AnalysisListenerFactory` |
+| **OTLP / Zipkin trace spans** (OTel SDKs, Istio, Envoy, iOS/Android, …) | `SpanListener` in `core.trace` | `META-INF/services/org.apache.skywalking.oap.server.core.trace.SpanListener` |
+| **OTel metrics** (Prometheus scrape, Envoy stats, Istio, …) | MAL rules — `otel-rules/<namespace>/*.yaml` | Append the rule path to `enabledOtelMetricsRules` in `application.yml` |
+| **Logs** (OTLP, SkyWalking native, Fluent Bit, Envoy ALS) | LAL rules — `lal/<rule>.yaml` | Append to `SW_LOG_LAL_FILES` (env) or `lalFiles` (YAML) under `log-analyzer` |
+| **Pre-computed meter / custom meter** (custom agent protocol, meter receiver) | MAL `meter-analyzer-config/*.yaml` | Append to `meter-analyzer-config` entries |
+| **Aggregation from Source entities** (service, instance, endpoint, relation) | OAL rules — `oal/*.oal` | Already loaded by `OAL grammar`; just add lines |
+
+All five DSLs (OAL, MAL, LAL, Hierarchy + SpanListener SPI) compile via ANTLR4 + Javassist at startup. Source-of-truth details are in:
+- [`docs/en/concepts-and-designs/oal.md`](../../../docs/en/concepts-and-designs/oal.md)
+- [`docs/en/concepts-and-designs/mal.md`](../../../docs/en/concepts-and-designs/mal.md)
+- [`docs/en/concepts-and-designs/lal.md`](../../../docs/en/concepts-and-designs/lal.md)
+- [`docs/en/concepts-and-designs/scope-definitions.md`](../../../docs/en/concepts-and-designs/scope-definitions.md)
+- [`oap-server/analyzer/meter-analyzer/CLAUDE.md`](../../../oap-server/analyzer/meter-analyzer/CLAUDE.md) — MAL compiler internals
+- [`oap-server/analyzer/log-analyzer/CLAUDE.md`](../../../oap-server/analyzer/log-analyzer/CLAUDE.md) — LAL compiler internals
+
+---
+
+## 2. Trace ingestion — two parallel pipelines
+
+SkyWalking OAP ingests traces through two distinct entry points. Pick based on source.
+
+### 2.1 SkyWalking native segments — `AnalysisListener`
+
+**Source protocol**: `apm-protocol/apm-network/src/main/proto/language-agent/Tracing.proto` (`TraceSegmentObject`).
+
+**Receiver**: `oap-server/server-receiver-plugin/skywalking-trace-receiver-plugin`. After wire decoding, segments are handed to `SegmentParserServiceImpl` which fans out to registered `AnalysisListenerFactory` implementations.
+
+**Extension**: implement one or more of
+- `FirstAnalysisListener` — per-segment, before per-span phase
+- `EntryAnalysisListener` — per-entry-span
+- `ExitAnalysisListener` — per-exit-span
+- `LocalAnalysisListener` — per-local-span
+- `TraceSegmentListener` — whole-segment, after per-span phase
+
+Register the **factory** in `META-INF/services/org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.AnalysisListenerFactory`. Existing examples: `RPCAnalysisListener`, `MultiScopesAnalysisListener`, `SegmentAnalysisListener` in `oap-server/analyzer/agent-analyzer/src/main/java/.../trace/parser/listener/`.
+
+Use when: the data arrives on the SW native wire (language agents — Java, Python, Go, Node.js, .NET, PHP, Lua, etc.).
+
+### 2.2 OTLP / Zipkin spans — `SpanListener`
+
+**Source protocol**: OTLP proto (`opentelemetry/proto/trace/v1/trace.proto`) or Zipkin v2 JSON.
+
+**Receiver**: `oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/.../otlp/OpenTelemetryTraceHandler.java` (OTLP) and `oap-server/server-receiver-plugin/zipkin-receiver-plugin/.../ITZipkinTraceCollector` (Zipkin). After decoding, spans are iterated and passed to every registered `SpanListener` through `SpanListenerManager`.
+
+**Extension**: implement `org.apache.skywalking.oap.server.core.trace.SpanListener`:
+
+```java
+public interface SpanListener {
+ String[] requiredModules();
+ void init(ModuleManager moduleManager);
+ SpanListenerResult onOTLPSpan(OTLPSpanReader span, Map<String,String> resourceAttributes,
+ String scopeName, String scopeVersion);
+}
+```
+
+Register in `META-INF/services/org.apache.skywalking.oap.server.core.trace.SpanListener`. Reference implementations:
+- `oap-server/analyzer/ios-analyzer/src/main/java/.../IOSHTTPSpanListener.java` — OTel Swift URLSession → OAL (Service / ServiceInstance / Endpoint)
+- `oap-server/analyzer/ios-analyzer/src/main/java/.../IOSMetricKitSpanListener.java` — OTel Swift MetricKit → MAL samples via `OpenTelemetryMetricRequestProcessor.toMeter`
+- `oap-server/analyzer/gen-ai-analyzer/src/main/java/.../GenAISpanListener.java` — LLM / MCP instrumentation
+
+Return `SpanListenerResult.builder().shouldPersist(false).build()` to prevent a span being stored as a trace (useful for e.g. 24-hour aggregate spans that are not meaningful as traces).
+
+**When both native and OTLP should feed the same metric**, write your extension against the downstream scope (Service / ServiceInstance / Endpoint + OAL) and emit those entities from **both** a SegmentListener and a SpanListener. The OAL layer is protocol-agnostic.
+
+---
+
+## 3. Metrics — MAL / OAL rules
+
+### 3.1 OAL (for entities already produced by core sources)
+
+`oap-server/server-starter/src/main/resources/oal/core.oal` already defines the standard `service_cpm`, `service_resp_time`, `service_sla`, `endpoint_cpm`, etc. — keyed off the `Service`, `ServiceInstance`, `Endpoint` source scopes. If your new layer just emits those entities with the right `Layer`, you get all the core metrics for free.
+
+Additional OAL rules go in the same file. OAL grammar: `oap-server/oal-grammar/src/main/antlr4/OALParser.g4`. Syntax and examples: [`docs/en/concepts-and-designs/oal.md`](../../../docs/en/concepts-and-designs/oal.md).
+
+### 3.2 MAL (for meter / metric samples)
+
+**File location**: `oap-server/server-starter/src/main/resources/otel-rules/<namespace>/*.yaml` (OTel scrape) or `meter-analyzer-config/*.yaml` (SW native meter).
+
+**Registration** (MANDATORY): append the rule path to the default in `oap-server/server-starter/src/main/resources/application.yml`:
+```yaml
+receiver-otel:
+ default:
+ enabledOtelMetricsRules: ${SW_OTEL_RECEIVER_ENABLED_OTEL_METRICS_RULES:"...,<your-namespace>/*"}
+```
+If you skip this, the rule isn't loaded in the default starter and downstream `converters.forEach(...)` is a silent no-op — samples just disappear.
+
+**Also update** `test/e2e-v2/cases/storage/expected/config-dump.yml`. This e2e diffs `/debugging/config/dump` output against a static file; every default-value change in `application.yml` must mirror here or the storage e2e fails.
+
+**Naming**: keep attribute scope in the metric name. If a metric reads from `foo.bar.foreground.exit_count`, call it `foo_foreground_exit_count`, not `foo_exit_count`. When two attributes (foreground vs background, ingress vs egress) share a theme, keep them as separate distinguishable metrics — don't flatten.
+
+### 3.3 Histogram pitfalls (both OAL and MAL)
+
+- **Per-bucket counts, not cumulative.** `histogram_percentile` accumulates internally. Feeding cumulative counts doubles the accumulator and shifts percentiles up. Each observation contributes `1` to exactly one `le` bucket.
+- **`le` label scale.** `SampleFamily.histogram()` rescales `le` by `defaultHistogramBucketUnit.toMillis(1)` (default `SECONDS` ⇒ ×1000). If your raw labels are already in ms, use `SampleFamilyBuilder.newBuilder(samples).defaultHistogramBucketUnit(TimeUnit.MILLISECONDS).build()`.
+- **Overflow bucket.** MAL parses `le="Infinity"` as `Double.POSITIVE_INFINITY` → stored as `Long.MAX_VALUE`. If a percentile lands there, the UI renders ≈ 9.2 × 10¹⁸. Prefer a finite sentinel (e.g., 30 s for hang/launch) so outliers remain visible without crashing the chart.
+- **`NamingControl`** — every `service` / `serviceInstance` / `endpoint` label that ends up as an entity key must go through `namingControl.formatServiceName()` / `formatInstanceName()` / `formatEndpointName()`. Raw resource attributes (long git-SHA build metadata, path-style endpoints) will otherwise exceed storage limits.
+
+---
+
+## 4. Logs — LAL
+
+**File location**: `oap-server/server-starter/src/main/resources/lal/<feature>.yaml`.
+
+**Registration**: append to `SW_LOG_LAL_FILES` env var, or `log-analyzer.default.lalFiles` in `application.yml`.
+
+**Grammar**: `oap-server/analyzer/log-analyzer/src/main/antlr4/LALParser.g4`. The compiler docs are thorough: [`oap-server/analyzer/log-analyzer/CLAUDE.md`](../../../oap-server/analyzer/log-analyzer/CLAUDE.md) — covers:
+- `filter { … }` block with `parsed.*`, `tag()`, `sourceAttribute()`, `log.*`, `def` locals, `extractor {}`, `metrics {}`, `sink {}`
+- `layer: auto` mode (script decides layer from source attrs)
+- `inputType` / `outputType` SPI (`LALSourceTypeProvider`) to bind typed proto to `parsed.*`
+- Test harness at `oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/oap-cases/` — ship YAML + `.data.yaml` pair; `LALScriptExecutionTest` picks them up automatically.
+
+**`layer: auto` gotcha**: the extractor's `layer "X"` assignment must propagate to `LogMetadata.layer` (not just the output builder) — `FilterSpec.doSink` reads metadata to decide drop/route. The current codegen (`LALBlockCodegen.generateFieldToOutput`) handles this correctly; if you extend codegen, preserve that invariant.
+
+---
+
+## 5. UI — templates, submodule, and menu
+
+### 5.1 Dashboard templates
+
+JSON files under `oap-server/server-starter/src/main/resources/ui-initialized-templates/<feature>/`. Conventional fileset:
+
+- `<feature>-root.json` — `ServiceList` widget, layered service index.
+- `<feature>-service.json` — per-service dashboard, usually a `Tab` with Overview / Instance / Endpoint / Log sub-tabs.
+- `<feature>-instance.json` — per-instance dashboard.
+- `<feature>-endpoint.json` — per-endpoint dashboard.
+- Menu link in `oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml`.
+
+Details that always bite:
+
+- **Tab chrome eats ~4 rows.** Set outer `Tab.h = content_max_y + 4` or you get an inner scrollbar on top of the page scrollbar.
+- **Widget heights consistent.** Use `h=15` per row for `Line` / `Bar` widgets to match mesh / general peer templates. Don't mix h=12 / h=15 / h=24 across a feature's dashboards.
+- **List widgets (ServiceList / InstanceList / EndpointList)** render two pieces per cell:
+ - `expressions` → the aggregated summary number (right side). Required. Wrap raw metrics in `avg(...)` / `sum(...)`. Omitting it leaves the column empty.
+ - `subExpressions` → the sparkline trend (left side). Optional, falls back to `expressions` if absent.
+ - `metricConfig` pairs 1-to-1 with `expressions` (not `subExpressions`). Use for `label` (column header) and `unit`.
+ - Compound columns via MQE addition: `avg(metric_a + metric_b)` in `expressions`, `metric_a + metric_b` in `subExpressions`.
+ - Normal widgets with multiple expressions on one chart — always give a human label per expression in `metricConfig`, otherwise legend renders raw metric names.
+
+### 5.2 Hot-reload (edits to shipped templates take effect on restart)
+
+`UITemplateInitializer` normally uses `addIfNotExist` (keyed on JSON `id`), so on a plain restart your edits to shipped templates are ignored — the seeded copy in storage wins. For dev / extension work, set the environment variable and restart:
+
+```bash
+SW_UI_TEMPLATE_FORCE_RELOAD=true
+# then restart OAP — shipped templates overwrite the seeded copies via addOrReplace.
+# Hard-refresh the browser (Cmd+Shift+R) because the booster UI caches config in memory.
+```
+
+This is a dev / extension knob only — read directly from the OS environment, not wired through `application.yml`. Unset or `false` preserves the default production behavior where operators' UI edits persist across restarts.
+
+### 5.3 New top-level menu group (e.g. "Mobile", "Gateway")
+
+New **child entries** only need template JSON + `menu.yaml`. A new **top-level group** needs submodule work in `skywalking-ui` (git submodule at `skywalking-ui/`):
+
+1. `skywalking-ui/src/assets/icons/<name>.svg` — menu icon.
+2. `skywalking-ui/src/locales/lang/menus/{en,es,zh}.ts` — i18n labels (`<name>`, `<name>_desc`, `<name>_<child>`, `<name>_<child>_desc`). Missing i18n falls back to raw keys on screen.
+3. Commit those upstream in `apache/skywalking-booster-ui`, bump the submodule pointer here.
+4. Rebuild with `-Pui,dist` (not `-Pbackend,dist`) and `make docker.ui`, otherwise the new assets aren't in the image. See [package skill](../package/SKILL.md).
+
+---
+
+## 6. Docs set — everything that moves together
+
+| File | What to add |
+|---|---|
+| `docs/en/setup/backend/<feature>-monitoring.md` | User-facing setup guide. Required SDK / collector, required env vars, sample OTLP payload, metrics / log tags list. |
+| `docs/menu.yml` | Add the guide to the docs navigation tree. |
+| `docs/en/security/README.md` | **Client-side monitoring**: mobile / browser / mini-program endpoints accept data from the public internet without auth by default. Add a rate-limit + abuse-protection note. Agent-side monitoring over private VPC usually doesn't need this. |
+| `docs/en/changes/changes.md` | Per-release changelog. Add under `#### OAP Server` for features/bug-fixes, `#### UI` if the submodule bumps, `#### Documentation` if new guides. |
+| `docs/en/swip/SWIP-<N>.md` | Design doc for non-trivial features; keep reconciled with final code after review. |
+| `docs/en/swip/readme.md` | Move SWIP from "Proposed" to "Accepted" once merged. |
+
+---
+
+## 7. E2E test
+
+Folder: `test/e2e-v2/cases/<feature>/`. See [run-e2e skill](../run-e2e/SKILL.md) for harness basics (setup / trigger / verify phases, template functions, expected-file authoring via `contains`).
+
+Feature-specific extras:
+
+1. **Docker-compose**: extend `../../script/docker-compose/base-compose.yml` oap + storage. Set `SW_OTEL_RECEIVER_ENABLED_HANDLERS`, `SW_LOG_LAL_FILES`, and any feature-specific env. Don't list default rules here — rely on `application.yml` so the config-dump test stays consistent.
+2. **Register in CI**: add an entry under the e2e matrix in `.github/workflows/skywalking.yaml`.
+3. **OTLP data hygiene**:
+ - `spanId` is exactly 16 hex chars; `traceId` is exactly 32. Protobuf-JSON decodes them as base64; other lengths return HTTP 400.
+ - Setup-step curl loops must fail loudly: `curl -sS -f --retry 30 --retry-delay 5 --retry-connrefused --retry-all-errors --max-time 10 ...` + `set -e`. The pattern `for …; do curl && break || sleep 5; done` silently succeeds when every attempt connection-refused (the final `sleep` returns 0) because OAP takes ~50 s to start in CI.
+4. **Verify order matters**. `e2e verify` stops at the first failing case. Fire each verify command manually against a live OAP via `swctl` before committing (stale CLI flags and wrong metric names burn 20 min of CI retries otherwise).
+5. **Config dump**: when you modify any `application.yml` default (rule list, handler list, ...), also update `test/e2e-v2/cases/storage/expected/config-dump.yml`.
+
+---
+
+## 8. Pre-submit checklist
+
+Run in order; each has a dedicated skill:
+
+1. [`compile`](../compile/SKILL.md) — `./mvnw clean flatten:flatten install javadoc:javadoc -B -q -Pall -Dmaven.test.skip -Dcheckstyle.skip -Dgpg.skip`. Javadoc `[ERROR] ... error:` must be fixed; `[ERROR] ... warning:` is noise.
+2. Tests: `./mvnw -pl oap-server/analyzer/log-analyzer -am test -Dtest=LALScriptExecutionTest` for LAL changes, equivalent for MAL, OAL unit tests where the rule lives.
+3. [`license`](../license/SKILL.md) — `license-eye header check` (Apache 2.0 headers). If the UI submodule bumped npm deps, also `license-eye dependency resolve --summary ./dist-material/release-docs/LICENSE.tpl` and commit the `LICENSE` diff.
+4. [`package`](../package/SKILL.md) — full `make docker` (or dist-rebuild + `make docker.oap` / `make docker.ui`). Don't trust `docker.oap` alone to pick up code changes; the dist tarball is a file-level prereq.
+5. [`run-e2e`](../run-e2e/SKILL.md) — run the new case end-to-end locally before pushing.
+6. [`gh-pull-request`](../gh-pull-request/SKILL.md) — commit and open PR with the template, attach UI screenshots.
+
+---
+
+## 9. Cross-cutting traps collected from real extensions
+
+| Trap | Symptom | Fix |
+|---|---|---|
+| Layer folder named with hyphens | `ui-initialized-templates/my-layer/*.json` on disk but dashboard empty | Folder must be `Layer.name().toLowerCase()` (underscores). `wechat_mini_program/`, not `wechat-mini-program/` |
+| Missing layer-root template | Menu item lands on empty "no dashboard" view | `Layer.vue:41-44` requires a template with `isRoot: true`; ship a `<layer>-root.json` (precedent: `ios/ios-root.json`) |
+| New layer not selectable in UI / swctl | `service ly <LAYER>` returns empty | Add the enum in `Layer.java` with a fresh id; see §0 |
+| Stale dist in image | `make docker.oap` succeeds but behavior is old | See [`package` skill](../package/SKILL.md); rebuild dist first, then image |
+| Shipped template edits don't render after restart | Edited JSON on disk, but OAP shows old copy | Set `SW_UI_TEMPLATE_FORCE_RELOAD=true` (environment variable, not `application.yml`) and restart; OAP writes `addOrReplace`. Unset in production so operator UI edits aren't clobbered. |
+| MAL service-scoped rule fragmented by instance | "Overall app" metric has N series per version | Keep `service_instance_id` out of service-scoped `.sum`/`.avg`. Put instance-scoped metrics in a separate `<feature>-instance.yaml` with `expSuffix: instance(...)` — see `otel-rules/ios/` pattern. |
+| Config dump drift | Storage e2e fails after `application.yml` edit | Mirror the change in `test/e2e-v2/cases/storage/expected/config-dump.yml` |
+| LAL `layer:auto` WARN drop | Every log silently dropped | Codegen must set `metadata.layer` alongside output builder's layer |
+| Histogram 1000× off | p50 = 2 000 000 ms | `defaultHistogramBucketUnit(MILLISECONDS)` on the `SampleFamily` |
+| Histogram shifted up | p50 = 5 000 on 500–2500 ms data | Emit per-bucket, not cumulative |
+| Empty list column | Dashboard instance table has no numbers | Add `expressions` (not just `subExpressions`) |
+| Double scroll | Inner tab scroll + page scroll | `Tab.h = content_max + 4` chrome buffer |
+| Raw metric name in legend | Multiple expressions on one widget | Add parallel `metricConfig[].label` entries |
+| `spanKind` mismatch | Span skipped silently | `OTLPSpanReaderImpl.spanKind()` returns enum `.name()` like `SPAN_KIND_CLIENT`, not `CLIENT` |
+| Negative latency | `*_sla` inflated | Clamp `rawLatency < 0 ? 0 : (int) min(Integer.MAX_VALUE, rawLatency)` |
+| SLA too optimistic | Client-side DNS / timeout / connection-refused counted as success | Require a real response code for success: `status = code > 0 && code < 400`. Do not treat missing (0) as success. |
+| Silent drop with no rules | `converters == null` NPE elsewhere | Init to empty list at declaration |
+| `+Inf` bucket surfaces as `9.2e18` | UI shows garbage when a sample lands in overflow | Use finite sentinel bucket (e.g. 30 s) |
+| OTel submodule bump misses UI image | New icons / i18n not visible | Rebuild with `-Pui,dist` + `make docker.ui` |
+| LICENSE drift after submodule bump | CI `Dependency licenses` fails | `license-eye dependency resolve --summary ./dist-material/release-docs/LICENSE.tpl` and commit |
+| Setup-step curl loop swallows failure | e2e passes metrics but fails log verify | `curl -f --retry-connrefused --retry-all-errors` + `set -e` |
+| Verify case flag mismatch | CI fails early on `swctl` flag unknown to pinned commit | Fire every verify command locally first; bump `SW_CTL_COMMIT` if needed |
+
+---
+
+## 10. Reference extensions (read their code)
+
+Browse these when you need a template to copy from. Grouped by the kind of data source, because the extension points differ significantly:
+
+### SW native trace (language agents) — the "General" layer and friends
+
+`Layer.GENERAL` is the default layer for services reporting via the SkyWalking native trace protocol (Java / Python / Go / Node.js / .NET / PHP / Lua / Rust agents). It's the richest reference because the native pipeline is where the **full topology** lives:
+
+- **Service → service dependency** (topology): derived by `MultiScopesAnalysisListener` and `NetworkAddressAliasMappingListener` in `oap-server/analyzer/agent-analyzer/src/main/java/.../trace/parser/listener/`. Entry/exit spans produce `ServiceRelation`, `ServiceInstanceRelation` sources.
+- **Endpoint → endpoint dependency**: `RPCAnalysisListener` emits `EndpointRelation` sources from cross-process refs and exit spans. This is unique to the native protocol — OTLP spans don't preserve upstream entry-span metadata the same way, so OTLP-only layers generally lose endpoint-level topology.
+- OAL aggregation: `oap-server/server-starter/src/main/resources/oal/core.oal` — all `service_relation_*`, `service_instance_relation_*`, `endpoint_relation_*` metrics are already defined there; a new native-tracing layer just needs to produce the right sources with the right `Layer`.
+
+Read order: `AnalysisListenerFactory` SPI registration → `SegmentParserServiceImpl` fan-out → individual listeners → OAL rules.
+
+### Service-mesh (Istio / Envoy ALS) — topology from proxy-side signals
+
+No agent in the service; Envoy emits HTTP/TCP access logs (ALS) and metrics. Service-to-service topology is reconstructed from ALS request/response pairs.
+
+- `oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/` — ALS receiver + `EnvoyAccessLogBuilder` persistence (LAL output type).
+- `otel-rules/istio-*`, `otel-rules/envoy/*` — MAL rules for proxy-emitted OTel metrics.
+- `lal/envoy-als.yaml` — LAL rule that extracts topology-generating fields from ALS.
+- Layer: `Layer.MESH`, plus `Layer.MESH_CP` (control plane) and `Layer.MESH_DP` (data plane) for split-plane topology.
+
+Topology parity with native traces: ALS gives you service-level relations; endpoint-level relations are approximate (proxy sees inbound + outbound, not the full call-chain that a native agent observes).
+
+### OTLP-only layers (OTel SDKs, iOS/Android, AI gateway, …)
+
+OTLP spans feed a `SpanListener`. Topology is **partial by construction** — an OTel Swift SDK on a mobile device only knows about its own outbound requests; there's no upstream caller. So these layers typically produce only `Service` / `ServiceInstance` / `Endpoint` sources (no `*Relation`) and the Topology view shows isolated islands, by design.
+
+- **iOS** (`Layer.IOS`, OTLP traces + MetricKit + LAL diagnostics): `oap-server/analyzer/ios-analyzer/` + `docs/en/swip/SWIP-11.md`.
+- **Envoy AI Gateway** (`Layer.ENVOY_AI_GATEWAY`, LLM / MCP, OTLP metrics + Zipkin sampled traces + LAL access-log sampling): `oap-server/analyzer/gen-ai-analyzer/`, `otel-rules/envoy-ai-gateway/*`, `lal/envoy-ai-gateway.yaml`.
+
+### OTel-metrics-only layers (scraped exporters)
+
+No spans, no logs — just MAL rules converting OTel metric scrapes.
+
+- **MySQL / PostgreSQL**: `otel-rules/mysql/*`, `otel-rules/postgresql/*`.
+- **Kubernetes**: `otel-rules/k8s/*` — paired with `virtual-k8s-service` (`Layer.K8S_SERVICE`) sources for synthetic services.
+- **AWS**: `otel-rules/aws-eks/*`, `otel-rules/aws-s3/*`, `otel-rules/aws-dynamodb/*`.
+
+### SW native meter (custom protocol)
+
+No OTel; the agent / plugin sends pre-built meter samples on the SW `MeterProtocol`.
+
+- **Nginx Lua** (`Layer.NGINX`): `meter-analyzer-config/nginx-service.yaml`, with the Lua agent in `apache/skywalking-nginx-lua`.
+- **APISIX** (`Layer.APISIX`): `meter-analyzer-config/apisix-service.yaml` + LAL trace extraction.
+
+### Which to copy from
+
+| You're adding | Closest reference |
+|---|---|
+| Language agent (new JVM / runtime) | The General / Java layer flow; extend `AnalysisListener` SPI. |
+| Service mesh / proxy | `mesh-*` + `envoy-*` rules; LAL on ALS. |
+| Mobile / browser / mini-program SDK (OTLP) | `ios-analyzer` (OTLP client-side). Expect no topology relations. |
+| Database / middleware (metrics only) | `mysql`, `postgresql` — pure MAL rules. |
+| Cloud service (no agent, just metrics) | `aws-eks` / `aws-s3`. |
+| Custom protocol / home-grown telemetry | `apisix` / `nginx` — SW native meter. |
diff --git a/docs/en/swip/SWIP-12.md b/docs/en/swip/SWIP-12.md
new file mode 100644
index 0000000000..9fe8de7fa8
--- /dev/null
+++ b/docs/en/swip/SWIP-12.md
@@ -0,0 +1,858 @@
+# SWIP-12 Support WeChat & Alipay Mini Program Monitoring
+
+## Motivation
+
+WeChat (微信) and Alipay (支付宝) Mini Programs are among the most widely used client-side
+platforms in China — many businesses ship a mini-program before (or instead of) a native
+mobile app. Observability for mini-programs is as important as iOS/Android monitoring.
+
+The [SkyAPM `mini-program-monitor`](https://github.com/SkyAPM/mini-program-monitor) SDK
+provides a single JavaScript client that covers both WeChat and Alipay runtimes. It emits:
+
+- **OTLP logs** — JS errors, promise rejections, `pageNotFound` (WeChat), HTTP failures
+- **OTLP metrics** — app launch / first-render / route / script / sub-package perf
+ gauges (durations) + a `first_paint.time` epoch-ms timestamp gauge + a
+ request-duration delta histogram (per-flush)
+- **SkyWalking native trace segments** (opt-in) — one `SegmentObject` per sampled
+ outgoing request, posted to `/v3/segments` with an `sw8` header injected on the wire
+ so downstream services join the same trace. As of SDK v0.3.0, every exit span carries
+ per-platform `componentId` (`10002` WeChat, `10003` Alipay) and a
+ `miniprogram.platform` tag
+
+Because the SDK speaks **standard OTLP + SkyWalking native**, no new receiver is required.
+This SWIP focuses on: two new layers, platform-aware MAL/LAL routing, service/instance/
+endpoint entity convention, componentId-driven layer mapping for native trace segments,
+menu/dashboard support, and a data generator for skywalking-showcase.
+
+This SWIP builds on LAL `layer: auto` / `sourceAttribute()` (SWIP-11) and the existing
+`SegmentParserListenerManager` trace-analyzer pipeline — no new general-purpose
+infrastructure is needed.
+
+## Architecture Graph
+
+```
+┌───────────────────────────┐ ┌────────────────────────────────────────────┐
+│ WeChat / Alipay │ OTLP/HTTP │ SkyWalking OAP │
+│ Mini Program │ ─────────────────────> │ │
+│ + mini-program-monitor │ POST /v1/logs │ ┌──────────────────────────────────────┐ │
+│ SDK │ POST /v1/metrics │ │ receiver-otel │ │
+│ │ │ │ │ │
+│ Instrumentation: │ SW native │ │ Log Handler │ │
+│ • wx.onError / │ ─────────────────────> │ │ → LAL rule (layer: auto) │ │
+│ my.onError │ POST /v3/segments │ │ fork by miniprogram.platform │ │
+│ • wx.request / │ │ │ → WECHAT_MINI_PROGRAM layer │ │
+│ my.request │ │ │ → ALIPAY_MINI_PROGRAM layer │ │
+│ • wx.getPerformance() │ │ │ │ │
+│ (WeChat) │ │ │ Metric Handler │ │
+│ • App/Page lifecycle │ │ │ → MAL rules (miniprogram/*.yaml) │ │
+│ (Alipay fallback) │ │ │ fork by miniprogram.platform │ │
+│ │ │ │ │ │
+│ Resource attrs: │ │ │ Trace Handler (native) │ │
+│ • service.name │ │ │ → SegmentObject │ │
+│ • service.version │ │ │ componentId 10002 / 10003 → │ │
+│ • miniprogram.platform │ │ │ WECHAT / ALIPAY_MINI_PROGRAM │ │
+│ = wechat | alipay │ │ │ layer (CommonAnalysisListener) │ │
+│ Span componentId: │ │ └──────────────────────────────────────┘ │
+│ • 10002 (WeChat) / │ │ │
+│ 10003 (AliPay) │ │ │
+└───────────────────────────┘ └────────────────────────────────────────────┘
+```
+
+## Proposed Changes
+
+### 1. Two New Layers
+
+Add in `Layer.java`:
+```java
+/**
+ * WeChat Mini Program monitoring via mini-program-monitor SDK
+ */
+WECHAT_MINI_PROGRAM(48, true),
+
+/**
+ * Alipay Mini Program monitoring via mini-program-monitor SDK
+ */
+ALIPAY_MINI_PROGRAM(49, true),
+```
+
+Both are normal layers (`isNormal=true`) — the mini-program is the observed service itself.
+
+**Why two layers, not one:** the two platforms expose different sets of metrics (see §3),
+and users want to compare across WeChat apps or across Alipay apps separately. Using a
+tag would force every query and dashboard widget to filter by platform — a layer per
+platform is cleaner and mirrors how the SkyWalking UI organizes other platforms.
+
+### 2. Service / Instance / Endpoint Mapping
+
+The SDK's OTLP resource block provides three identifiers, but only two are usable as
+aggregation dimensions:
+
+| SkyWalking Entity | Source | Cardinality | Rationale |
+|-------------------|-----------------------------------|-------------------|--------------------------------------------------------|
+| **Service** | `service.name` | 1 per app | Fleet-wide app health |
+| **ServiceInstance** | `service.instance.id` (recommended pattern: operator sets it to `service.version`) | tens per app | Version regression / rollout monitoring. **Coherence depends on operator following the recommended pattern** — see "Instance coherence" below. |
+| **Endpoint** | `miniprogram.page.path` | dozens per app | Which in-app page is slow / errors — matches browser-agent semantics |
+
+**What we deliberately drop:**
+
+| Dropped | Instead used as | Why |
+|------------------------------------------------------|----------------------------------------------------|---------------------------------------------------------------|
+| Per-device `service.instance.id` | Not aggregated as an entity | Unbounded cardinality — millions for any real user base. SDK ≥ v0.4.0 no longer auto-generates a device id; operators set `serviceInstance` to a version-scoped value (see §8). |
+| `server.address` (remote host for outbound requests) | Metric label on `miniprogram.request.duration` + `peer` on segments | Not a mini-program entity; topology handles it via tracing |
+
+All three entities are needed — each answers a distinct question:
+- **Service** → how is the app doing overall?
+- **Instance (= version)** → did v1.3 regress vs v1.2?
+- **Endpoint (= page)** → which page is slow / error-prone?
+
+Skipping any of them loses a class of question.
+
+#### Instance coherence across signals
+
+The three signal pipelines key off different attributes by default:
+
+| Signal | Source attribute used as instance |
+|---|---|
+| OTLP metrics | OTLP resource `service.instance.id` (omitted by SDK if `serviceInstance` unset) |
+| Native trace segments | `serviceInstance` field on the segment (substituted with literal `-` if unset) |
+| OTLP logs (via LAL) | `sourceAttribute("service.instance.id")` (the recommended LAL extractor — see §5) |
+
+For all three to land on the same OAP instance entity, the operator **must** set
+`init({ serviceInstance: <some-string> })` — recommended value is `service.version` so
+the same string appears as both `service.instance.id` (OTLP) and segment
+`serviceInstance`.
+
+When `serviceInstance` is unset, the three pipelines do **not** uniformly fall back to
+the same placeholder — they each handle absence differently:
+
+| Pipeline | Behavior on absent `serviceInstance` |
+|---|---|
+| **Native trace segment** | SDK substitutes the literal `-` at the wire (mini-program-monitor `request.ts:147`); OAP records the instance entity literally as `-`. |
+| **OTLP log → LAL** | `TrafficSinkListener:83` short-circuits when `metadata.serviceInstance` is empty; **no instance traffic is generated**. |
+| **OTLP metric → MAL** | `SampleFamily.dim()` (`SampleFamily.java:715`) collapses missing labels to the empty string — the instance dimension is empty, no instance entity is built. |
+
+So the unset case is not "all three aligned under `-`" — segments get a `-` entity,
+logs and metrics get no instance entity at all. Operators who care about per-instance
+dashboards must set `serviceInstance`. This is documented as the recommended pattern in
+the SDK (`README.md` / `SIGNALS.md`) and pinned in the SDK's e2e CI.
+
+The earlier draft of this SWIP set the LAL `instance` to `sourceAttribute("service.version")`,
+which would make logs disagree with metrics + traces whenever `serviceInstance !=
+serviceVersion`. §5 below sources from `service.instance.id` directly so when the
+operator follows the recommended pattern, all three signal types share the same
+instance entity.
+
+### 3. Metric Coverage Per Platform
+
+Not every signal is supported on both runtimes. MAL rules emit the same metric names
+for both layers where supported; WeChat-only metrics produce data only under the
+`WECHAT_MINI_PROGRAM` layer.
+
+| Metric (MAL output, `meter_miniprogram_*`) | WeChat | Alipay | Source |
+|---------------------------------------------|--------|--------|--------------------------------------------------------------------------|
+| `app_launch_duration` | ✓ | ✓ | `wx.getPerformance()` (WeChat) / `App.onLaunch→onShow` fallback (Alipay) |
+| `first_render_duration` | ✓ | ✓ | PerformanceObserver `firstRender` (WeChat) / `onLoad→onReady` (Alipay) |
+| `first_paint.time` *(passthrough — see note)* | ✓ | — | PerformanceObserver `firstPaint`. **Wall-clock epoch ms timestamp**, not a duration. Not aggregated by MAL — surfaced only on individual page traces / log queries. |
+| `route_duration` | ✓ | — | PerformanceObserver `navigation/route` |
+| `script_duration` | ✓ | — | PerformanceObserver `script` |
+| `package_load_duration` | ✓ | — | PerformanceObserver `loadPackage` |
+| `request_duration_percentile` (P50–P99) | ✓ | ✓ | `miniprogram.request.duration` histogram (per-flush DELTA) |
+| `error_count` | ✓ | ✓ | `js`, `promise`, `ajax` error logs (via LAL) |
+| `page_not_found_count` | ✓ | — | `pageNotFound` error log (no `my.onPageNotFound` hook on Alipay) |
+
+**Precision caveat for Alipay perf metrics:** on WeChat, perf values come from the native
+`PerformanceObserver` entries. On Alipay, the SDK falls back to lifecycle hooks
+(`App.onLaunch→onShow`, `Page.onLoad→onReady`) because the Alipay base library does not
+expose `PerformanceObserver` entries for the same events. These are approximations of
+"time-to-interactive" rather than authoritative renderer timings. Dashboards should not
+compare WeChat and Alipay perf values directly; this is documented in the per-platform
+doc pages.
+
+### 3a. OAL / Topology Metrics Emerge From the Layer Mapping
+
+The SDK posts Exit spans (`spanType=Exit`, `spanLayer=Http`) to `/v3/segments` via the
+SkyWalking native protocol. After §6's `identifyServiceLayer` mapping assigns
+`Layer.WECHAT_MINI_PROGRAM` / `Layer.ALIPAY_MINI_PROGRAM`, the native trace pipeline's
+`RPCAnalysisListener` emits `Service`, `ServiceInstance`, `Endpoint` sources — plus
+`ServiceRelation` / `ServiceInstanceRelation` / `EndpointRelation` for the
+mini-program → backend call edge — all tagged with the mini-program layer.
+
+`core.oal` then produces, for free, under each mini-program layer:
+
+| Metric family | Scope | Source |
+|---|---|---|
+| `service_cpm`, `service_resp_time`, `service_sla`, `service_percentile`, `service_apdex` | Service | OAL on `Service` source |
+| `service_instance_cpm`, `service_instance_resp_time`, `service_instance_sla`, `service_instance_percentile` | ServiceInstance | OAL on `ServiceInstance` source |
+| `endpoint_cpm`, `endpoint_resp_time`, `endpoint_sla`, `endpoint_percentile` | Endpoint (page path) | OAL on `Endpoint` source |
+| Topology edges to backend services | ServiceRelation / EndpointRelation | `sw8` propagation + backend receives the trace |
+
+These are **in addition to** the MAL-produced `meter_wechat_mp_*` /
+`meter_alipay_mp_*` metrics from §4. The service dashboard panels should mix both —
+outbound request latency from OAL (`service_resp_time`, `service_percentile`) keyed on
+observed response time, plus the SDK's own per-page perf gauges from MAL.
+
+Topology note: mini-programs are leaf sources — they issue outbound requests but never
+receive inbound traffic. So each mini-program service has outbound edges (to backend
+APIs carrying `sw8`) but no upstream. This is correct by construction for client-side
+platforms.
+
+### 3b. Error-Count Metric — Log-MAL Rule
+
+`error_count` in §3's metric table is derived from LAL-processed error logs. The
+extraction itself is a separate file in the log-MAL rules directory (not the MAL `otel-rules/`
+directory — log-MAL rules convert persisted logs into metric samples):
+
+`oap-server/server-starter/src/main/resources/log-mal-rules/miniprogram.yaml`:
+
+```yaml
+expSuffix: service(['service_name'], Layer.WECHAT_MINI_PROGRAM)
+metricPrefix: meter_wechat_mp
+metricsRules:
+ - name: error_count
+ exp: miniprogram_error_count.sum(['service_name', 'exception_type'])
+---
+expSuffix: service(['service_name'], Layer.ALIPAY_MINI_PROGRAM)
+metricPrefix: meter_alipay_mp
+metricsRules:
+ - name: error_count
+ exp: miniprogram_error_count.sum(['service_name', 'exception_type'])
+```
+
+The sample `miniprogram_error_count` is emitted by a `metrics {}` block in the LAL
+rule (§5) — one sample per error log processed, labelled with
+`exception_type`, `miniprogram_platform`, `service_name`. The platform attribute lets
+the per-layer filter route to the correct `expSuffix`.
+
+Register the log-MAL file alongside the MAL rule file in `application.yml`:
+
+```yaml
+log-analyzer:
+ default:
+ malFiles: ${SW_LOG_MAL_FILES:"<existing defaults>,miniprogram"}
+```
+
+### 4. MAL Rules — Per-Platform × Per-Scope, Mirroring the iOS Layout
+
+Following the iOS pattern (`otel-rules/ios/ios-metrickit.yaml` for service-scoped +
+`ios-metrickit-instance.yaml` for instance-scoped — service-scoped meters there have no
+`service_instance_id` dim so the "overall app health" view is genuinely
+fleet-aggregated), this SWIP creates **four files**:
+
+```
+oap-server/server-starter/src/main/resources/otel-rules/miniprogram/
+├── wechat-mini-program.yaml # service-scoped
+├── wechat-mini-program-instance.yaml # instance-scoped (per release/version)
+├── alipay-mini-program.yaml # service-scoped
+└── alipay-mini-program-instance.yaml # instance-scoped
+```
+
+Each file has a single `expSuffix` (one Layer, one entity scope) and a `filter` block
+that gates on `miniprogram.platform` so traffic from the wrong platform is dropped at
+the rule level.
+
+#### `wechat-mini-program.yaml` — service-scoped
+
+```yaml
+expSuffix: service(['service_name'], Layer.WECHAT_MINI_PROGRAM)
+metricPrefix: meter_wechat_mp
+filter: "{ tags -> tags['miniprogram_platform'] == 'wechat' }"
+
+metricsRules:
+ - name: app_launch_duration
+ exp: miniprogram_app_launch_duration.avg(['service_name'])
+ - name: first_render_duration
+ exp: miniprogram_first_render_duration.avg(['service_name'])
+ # first_paint.time is an epoch-ms timestamp, not a duration — not aggregated.
+ - name: route_duration
+ exp: miniprogram_route_duration.avg(['service_name'])
+ - name: script_duration
+ exp: miniprogram_script_duration.avg(['service_name'])
+ - name: package_load_duration
+ exp: miniprogram_package_load_duration.avg(['service_name'])
+ - name: request_duration_percentile
+ exp: miniprogram_request_duration_histogram.sum(['service_name', 'le']).histogram().histogram_percentile([50,75,90,95,99])
+
+ # Endpoint-scoped per-page (chained .endpoint(...) overrides expSuffix)
+ - name: endpoint_app_launch_duration
+ exp: miniprogram_app_launch_duration.avg(['service_name', 'miniprogram_page_path']).endpoint(['service_name'], ['miniprogram_page_path'], Layer.WECHAT_MINI_PROGRAM)
+ - name: endpoint_first_render_duration
+ exp: miniprogram_first_render_duration.avg(['service_name', 'miniprogram_page_path']).endpoint(['service_name'], ['miniprogram_page_path'], Layer.WECHAT_MINI_PROGRAM)
+ - name: endpoint_request_duration_percentile
+ exp: miniprogram_request_duration_histogram.sum(['service_name', 'miniprogram_page_path', 'le']).histogram().histogram_percentile([50,75,90,95,99]).endpoint(['service_name'], ['miniprogram_page_path'], Layer.WECHAT_MINI_PROGRAM)
+```
+
+#### `wechat-mini-program-instance.yaml` — instance-scoped (per version)
+
+```yaml
+expSuffix: instance(['service_name'], ['service_instance_id'], Layer.WECHAT_MINI_PROGRAM)
+metricPrefix: meter_wechat_mp_instance
+filter: "{ tags -> tags['miniprogram_platform'] == 'wechat' }"
+
+metricsRules:
+ - name: app_launch_duration
+ exp: miniprogram_app_launch_duration.avg(['service_name', 'service_instance_id'])
+ - name: first_render_duration
+ exp: miniprogram_first_render_duration.avg(['service_name', 'service_instance_id'])
+ - name: route_duration
+ exp: miniprogram_route_duration.avg(['service_name', 'service_instance_id'])
+ - name: script_duration
+ exp: miniprogram_script_duration.avg(['service_name', 'service_instance_id'])
+ - name: package_load_duration
+ exp: miniprogram_package_load_duration.avg(['service_name', 'service_instance_id'])
+ - name: request_duration_percentile
+ exp: miniprogram_request_duration_histogram.sum(['service_name', 'service_instance_id', 'le']).histogram().histogram_percentile([50,75,90,95,99])
+```
+
+#### `alipay-mini-program.yaml` / `alipay-mini-program-instance.yaml`
+
+Mirror the WeChat files exactly, differing only in:
+- `filter`: `tags['miniprogram_platform'] == 'alipay'`
+- `expSuffix` Layer: `Layer.ALIPAY_MINI_PROGRAM`
+- `metricPrefix`: `meter_alipay_mp` / `meter_alipay_mp_instance`
+- **Drop the WeChat-only metrics** Alipay doesn't emit
+ (`route_duration`, `script_duration`, `package_load_duration`)
+
+#### Notes
+
+- **Service-scoped rules sum/avg by `service_name` only** — no `service_instance_id`
+ fragmentation. This produces the genuine fleet-aggregated view for the "overall app
+ health" dashboard panels. iOS's `ios-metrickit.yaml` is the precedent.
+- **Instance-scoped rules go in their own file** with `expSuffix: instance(...)`. This
+ is what backs per-release / version-regression dashboards.
+- **`service_instance_id` source:** SDK ≥ v0.4.0 emits OTLP `service.instance.id` only
+ when the operator passes `init({ serviceInstance: ... })`. When unset, the attribute
+ is omitted, `SampleFamily.dim()` collapses the `service_instance_id` label to an
+ empty string, and `Analyzer.java:345` (`if (!Strings.isNullOrEmpty(entity.getInstanceName()))`)
+ short-circuits — **no instance traffic is emitted**, and the per-instance MAL rules
+ produce no metrics. The instance dashboard is therefore meaningful only when operators
+ follow the recommended `serviceInstance: serviceVersion` pattern. MAL itself can add
+ a fail-safe (`tag {tags -> tags.service_instance_id = tags.service_instance_id ?: tags.service_name}`)
+ to keep per-instance metrics populated when the operator doesn't set `serviceInstance`,
+ but standard practice is to rely on the SDK side.
+- **The `.endpoint(...)` chain on service-scoped files** — same expression-level
+ override pattern as APISIX (`apisix.yaml:91-102`) and RocketMQ. One rule emits to
+ service scope (default from `expSuffix`), the next emits to endpoint scope by
+ chaining `.endpoint(...)` at the end.
+
+#### Histogram bucket unit — SDK should follow the seconds convention
+
+OAP's MAL `SampleFamily.histogram()` rescales `le` labels into milliseconds using
+`defaultHistogramBucketUnit` (default `SECONDS` on the OTLP ingestion path, because
+`PrometheusMetricConverter` builds `SampleFamily` without overriding the default).
+This matches Prometheus ecosystem convention, where latency histograms ship `le`
+values in seconds (e.g. `[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]`).
+
+The SDK currently emits `miniprogram.request.duration` bucket bounds in **ms**
+(`[10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]`). To land correctly on the
+OAP default ingestion path, the SDK should switch to seconds-convention bounds in its
+next release — same numeric coverage, just divided by 1000. This is an SDK-side change
+only; no OAP-side preprocessor needed. Tracked as a prerequisite alongside the SWIP-12
+implementation.
+
+The alternative — adding a mini-program-specific OAP preprocessor that marks
+`SampleFamily` with `defaultHistogramBucketUnit(MILLISECONDS)` — complicates OAP
+wiring for a single feature and is rejected. Match the ecosystem convention at the SDK
+boundary.
+
+#### `+Inf` overflow bucket
+
+The SDK's histogram has an implicit `+Inf` overflow for observations above the top
+bucket. MAL stores `le="Infinity"` as `Long.MAX_VALUE` (≈9.2 × 10¹⁸); if a percentile
+lands in the overflow bucket the UI renders that sentinel as a visible garbage number.
+For request duration, the top finite bound of 10 s covers essentially all real requests,
+so this is a low-risk dashboard-rendering concern rather than a correctness issue. If
+outlier percentiles turn out to surface the sentinel in practice, the SDK can add a
+finite overflow bound (e.g. a 30 s bucket) — same treatment SWIP-11 applied to the
+MetricKit hang/launch histograms.
+
+### 5. LAL Rules (Error Logs, `layer: auto` Fork by Platform)
+
+Create `oap-server/server-starter/src/main/resources/lal/miniprogram.yaml`.
+
+Uses the `layer: auto` + `sourceAttribute()` mechanism from SWIP-11:
+
+```yaml
+rules:
+ - name: miniprogram-errors
+ layer: auto
+ dsl: |
+ filter {
+ def platform = sourceAttribute("miniprogram.platform");
+ if (platform != "wechat" && platform != "alipay") { abort {} }
+ if (tag("exception.type") == null) { abort {} }
+
+ extractor {
+ layer platform == "wechat" ? "WECHAT_MINI_PROGRAM" : "ALIPAY_MINI_PROGRAM"
+ // Instance source matches what OTLP metrics use, so logs aggregate under
+ // the same OAP instance entity when operator follows the recommended
+ // serviceInstance == serviceVersion pattern. SDK ≥ v0.4.0 emits
+ // service.instance.id only when init({serviceInstance: ...}) is set.
+ // If absent, sourceAttribute() returns null/empty → TrafficSinkListener
+ // skips the instance traffic, matching MAL's empty-dim behavior.
+ instance sourceAttribute("service.instance.id")
+ endpoint tag("miniprogram.page.path")
+
+ tag 'platform': platform
+ tag 'exception.type': tag("exception.type")
+ tag 'exception.stacktrace': tag("exception.stacktrace")
+ // ajax-specific extras, nullable
+ tag 'http.method': tag("http.request.method")
+ tag 'http.status': tag("http.response.status_code")
+ tag 'server.address': tag("server.address")
+
+ // Emit a counter sample for every error log. Picked up by the log-MAL
+ // rule in §3b, which aggregates per (service, exception_type) for the
+ // per-layer error_count metric.
+ metrics {
+ miniprogram_error_count {
+ tag('service_name', log.service)
+ tag('exception_type', tag("exception.type"))
+ tag('miniprogram_platform', platform)
+ value 1
+ }
+ }
+ }
+
+ sink { }
+ }
+```
+
+The rule sets the layer script-side based on `miniprogram.platform`, so **one rule file
+produces two layers**. Error counts per service / instance / endpoint / exception.type
+can be derived via existing OAL log-metric machinery.
+
+### 6. Trace Segment Handling — Component-Driven Layer Mapping
+
+The SDK posts `SegmentObject` directly to `/v3/segments` (SkyWalking native protocol).
+These segments are parsed by the normal trace pipeline — no new SPI is needed.
+
+Service-layer assignment already lives in
+`CommonAnalysisListener.identifyServiceLayer(SpanLayer)` (a `protected` instance method
+on the abstract base shared by `RPCAnalysisListener` and
+`EndpointDepFromCrossThreadAnalysisListener` in the agent-analyzer module —
+`SegmentAnalysisListener` does **not** extend `CommonAnalysisListener`, it has its own
+service-meta path). Today it
+maps `SpanLayer.FAAS → Layer.FAAS` and everything else to `Layer.GENERAL`. Extend it to
+also accept the span's `componentId` (which the SDK already sets on every outbound span)
+and dispatch to the mini-program layers.
+
+The component name → id mapping lives in `component-libraries.yml` and is exposed only
+at runtime via `IComponentLibraryCatalogService`
+(`ComponentLibraryCatalogService.java:84-104`) — there are no auto-generated Java
+constants. So the listener's abstract base resolves the two ids once at construction
+time and caches them as `int` fields:
+
+```java
+abstract class CommonAnalysisListener {
+ private final int wechatMiniProgramComponentId;
+ private final int alipayMiniProgramComponentId;
+
+ protected CommonAnalysisListener(IComponentLibraryCatalogService catalog) {
+ this.wechatMiniProgramComponentId = catalog.getComponentId("WeChat-MiniProgram");
+ this.alipayMiniProgramComponentId = catalog.getComponentId("AliPay-MiniProgram");
+ }
+
+ protected Layer identifyServiceLayer(SpanLayer spanLayer, int componentId) {
+ if (componentId == wechatMiniProgramComponentId) {
+ return Layer.WECHAT_MINI_PROGRAM;
+ }
+ if (componentId == alipayMiniProgramComponentId) {
+ return Layer.ALIPAY_MINI_PROGRAM;
+ }
+ if (SpanLayer.FAAS.equals(spanLayer)) {
+ return Layer.FAAS;
+ }
+ return Layer.GENERAL;
+ }
+}
+```
+
+`IComponentLibraryCatalogService` is already a `CoreModule` service, so wiring it into
+the listener factories is one constructor parameter. Each of the 5 existing call sites
+in `RPCAnalysisListener` (×4) and `EndpointDepFromCrossThreadAnalysisListener` (×1)
+adds `span.getComponentId()` to its current `identifyServiceLayer(span.getSpanLayer())`
+call.
+
+No new SPI, no new listener registration — all the work happens inside the existing
+`SegmentParserListenerManager` pipeline.
+
+**Persistence:** default `true` — unlike iOS MetricKit spans (which represent 24-hour
+windows and must be suppressed), mini-program segments are real outgoing HTTP spans
+that belong in the trace UI.
+
+### 7. Component Library Entries
+
+Add to `oap-server/server-starter/src/main/resources/component-libraries.yml`, in the
+JavaScript block `[10000, 11000)`:
+
+```yaml
+WeChat-MiniProgram:
+ id: 10002
+ languages: JavaScript
+AliPay-MiniProgram:
+ id: 10003
+ languages: JavaScript
+```
+
+**Status:** as of mini-program-monitor v0.3.0, the SDK already emits these component
+ids on every exit span. Without the OAP-side registration, current OAP releases render
+them as "N/A" in topology even though the tag data is captured. Adding the two entries
+to `component-libraries.yml` is the unblock for proper topology rendering and is what
+makes the layer mapping in §6 effective.
+
+### 8. SDK-Side Convention
+
+All three originally-proposed SDK conventions are resolved as of mini-program-monitor
+**v0.4.0** (released):
+
+| Convention | Status |
+|---------------------------------------------------------|------------------------------------------------------------------------------------------|
+| Per-platform `componentId` on exit spans (`10002` / `10003`) | ✅ shipped in v0.3.0 |
+| `miniprogram.platform` span tag on every exit span | ✅ shipped in v0.3.0 |
+| Drop auto-generated per-device `serviceInstance` | ✅ shipped in v0.4.0 — `serviceInstance` defaults to unset; OTLP omits `service.instance.id`, native segments substitute `-` (protocol-mandatory field) |
+| Recommend version-scoped `serviceInstance` | ✅ documented in SDK README / SIGNALS / SAMPLES + e2e CI pins it to `service.version` |
+
+The originally-imagined "default to `service.version`" was rejected upstream in favor
+of a cleaner shape: the SDK has no opinion on what `serviceInstance` should be, but its
+docs explicitly recommend a version-scoped value (mirroring `service.version` or a
+release tag). When operators leave it unset, OTLP simply omits `service.instance.id` —
+spec-allowed (it's RECOMMENDED, not REQUIRED) — and OAP aggregates at the service level.
+
+This also means **no need for a `miniprogram.device` span tag fallback** — the device-id
+problem is gone at the source. Operators that genuinely need per-session granularity
+can still pass `init({ serviceInstance: '<their-id>' })` themselves.
+
+### 9. UI Menu and Dashboards
+
+#### Menu
+
+Extend the existing `Mobile` menu group (added in SWIP-11) in
+`oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml`:
+
+```yaml
+- title: Mobile
+ icon: mobile
+ menus:
+ - title: iOS
+ layer: IOS
+ ...
+ - title: WeChat Mini Program
+ layer: WECHAT_MINI_PROGRAM
+ description: WeChat Mini Program monitoring via mini-program-monitor SDK.
+ documentLink: https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-wechat-mini-program-monitoring/
+ i18nKey: wechat_mini_program
+ - title: Alipay Mini Program
+ layer: ALIPAY_MINI_PROGRAM
+ description: Alipay Mini Program monitoring via mini-program-monitor SDK.
+ documentLink: https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-alipay-mini-program-monitoring/
+ i18nKey: alipay_mini_program
+```
+
+#### Dashboards
+
+`UITemplateInitializer` auto-discovers template folders from `Layer.values()` — no
+allowlist to append to. Two platform requirements remain:
+
+1. **Folder name must be `Layer.X.name().toLowerCase()`** — underscores, not hyphens.
+ Hyphenated folders don't match any `Layer` enum value and are silently skipped.
+2. **Include a layer-root template** — `Layer.vue:41-44` requires a dashboard with
+ `isRoot: true` to render the menu landing page (precedent: `ios/ios-root.json`).
+ Without the root template, clicking the menu item lands on an empty "no dashboard"
+ view.
+ ```
+ oap-server/server-starter/src/main/resources/ui-initialized-templates/
+ ├── wechat_mini_program/
+ │ ├── wechat_mini_program-root.json # isRoot: true — service-list landing page
+ │ ├── wechat_mini_program-service.json
+ │ ├── wechat_mini_program-instance.json
+ │ └── wechat_mini_program-endpoint.json
+ └── alipay_mini_program/
+ ├── alipay_mini_program-root.json # isRoot: true — service-list landing page
+ ├── alipay_mini_program-service.json
+ ├── alipay_mini_program-instance.json
+ └── alipay_mini_program-endpoint.json
+ ```
+ Hyphenated folder names (e.g. `wechat-mini-program/`) are silently skipped because
+ they don't match `Layer.WECHAT_MINI_PROGRAM.name().toLowerCase()`.
+
+Structure mirrors the iOS dashboards but **adds a trace widget** — because native
+segments are queryable in the normal trace UI (unlike iOS's OTLP→Zipkin path).
+
+Metric names below use per-platform prefixes from §4 (`meter_wechat_mp` /
+`meter_wechat_mp_instance` / `meter_alipay_mp` / `meter_alipay_mp_instance`). The
+WeChat dashboard pulls from `meter_wechat_mp_*`; the Alipay dashboard pulls from
+`meter_alipay_mp_*`.
+
+**Per-service dashboard panels (WeChat):**
+
+| Panel Group | Widgets | Notes |
+|----------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
+| App Launch | `meter_wechat_mp_app_launch_duration` | |
+| Page Render | `meter_wechat_mp_first_render_duration` | `first_paint.time` is an epoch-ms timestamp, not aggregated by MAL — see §3 |
+| Navigation | `meter_wechat_mp_route_duration`, `meter_wechat_mp_script_duration`, `meter_wechat_mp_package_load_duration` | WeChat-only metrics — these panels are absent from the Alipay dashboard |
+| Request Perf | `meter_wechat_mp_request_duration_percentile` (P50/P75/P90/P95/P99) | |
+| Errors | Error count by `exception.type`; top error endpoints | Derived from LAL-processed logs |
+| **Traces** | Native trace list for the in-scope service (service list is layer-filtered upstream); endpoint trace drill-down | **Mini-program only** — iOS dashboards lack this because iOS traces go to Zipkin |
+
+**Per-service dashboard panels (Alipay):** same shape as WeChat, but only includes the
+metrics Alipay actually emits (`app_launch_duration`, `first_render_duration`,
+`request_duration_percentile`, errors, traces). The Navigation row and the Page Render
+row's WeChat-only `first_paint` mention are absent.
+
+**Per-instance (version) dashboard:** same metric set scoped to the service instance —
+backed by `meter_wechat_mp_instance_*` / `meter_alipay_mp_instance_*` (§4).
+
+**Per-endpoint (page) dashboard:** uses the chained-`.endpoint(...)` per-page metrics
+from §4 (`endpoint_app_launch_duration`, `endpoint_first_render_duration`,
+`endpoint_request_duration_percentile`), plus per-page error list.
+
+#### UI Side
+
+A separate PR in [skywalking-booster-ui](https://github.com/apache/skywalking-booster-ui)
+is needed for i18n entries for the two new sub-menus.
+
+### 10. Data Generator for skywalking-showcase
+
+mini-program-monitor v0.3.0 ships a **simulator ecosystem** as a first-class deliverable,
+not a separate harness — and it already publishes multi-arch (`linux/amd64`, `linux/arm64`)
+images per commit:
+
+- `ghcr.io/skyapm/mini-program-monitor/sim-wechat:<sha-or-version>`
+- `ghcr.io/skyapm/mini-program-monitor/sim-alipay:<sha-or-version>`
+
+[skywalking-showcase](https://github.com/apache/skywalking-showcase) consumes these
+images directly — no new image to build, no driver scripts to maintain in the showcase
+repo. Just two service entries pointing at OAP, e.g.:
+
+```yaml
+sim-wechat:
+ image: ghcr.io/skyapm/mini-program-monitor/sim-wechat:v0.4.0
+ environment:
+ MODE: loop
+ SCENARIO: demo
+ COLLECTOR_URL: http://oap:12800
+ TRACE_COLLECTOR_URL: http://oap:12800
+ SERVICE: showcase-wechat-mp
+ SERVICE_VERSION: v1.0.0
+ SERVICE_INSTANCE: v1.0.0 # version-scoped, mirrors SDK recommendation
+sim-alipay:
+ image: ghcr.io/skyapm/mini-program-monitor/sim-alipay:v0.4.0
+ environment:
+ MODE: loop
+ SCENARIO: demo
+ COLLECTOR_URL: http://oap:12800
+ TRACE_COLLECTOR_URL: http://oap:12800
+ SERVICE: showcase-alipay-mp
+ SERVICE_VERSION: v1.0.0
+ SERVICE_INSTANCE: v1.0.0
+```
+
+**Run modes** (env `MODE`): `loop` (forever, for demo), `timed` (for `DURATION_MS` then
+exit), `once` (one of each signal then exit, for CI parity).
+
+**Scenarios** (env `SCENARIO`): `demo` (healthy + all four error surfaces), `baseline`
+(steady happy stream), `error-storm` (high error rate + 5xx), `slow-api` (heavy tail
+latency).
+
+**Payload cost in the showcase:** at default cadences, well below existing
+Java/Python/Go showcase services. Pinning to a specific SHA / version (no `:latest`) is
+mandated by the SDK side — the showcase manifest tracks an explicit version.
+
+### 11. OAP-Side E2E Test Case
+
+Separately from the showcase demo generator (§10), an OAP-side e2e test is required
+for CI coverage. Add under `test/e2e-v2/cases/`:
+
+```
+test/e2e-v2/cases/miniprogram/wechat/e2e.yaml # wechat sim image as the workload
+test/e2e-v2/cases/miniprogram/wechat/expected/ # swctl-format expected outputs
+test/e2e-v2/cases/miniprogram/alipay/e2e.yaml # alipay sim image as the workload
+test/e2e-v2/cases/miniprogram/alipay/expected/ # swctl-format expected outputs
+```
+
+Each case drives the `ghcr.io/skyapm/mini-program-monitor/sim-{wechat,alipay}:v0.4.0`
+image in `MODE=once` (one-shot signal emission, then exit) against an OAP container
+wired with the new MAL / LAL / log-MAL rules. Verify steps cover:
+- service listed under the correct layer (`swctl service list --layer WECHAT_MINI_PROGRAM`)
+- per-platform MAL metrics non-empty (`meter_wechat_mp_app_launch_duration`, …)
+- `meter_wechat_mp_error_count` non-zero on the `error-storm` scenario
+- endpoint list populated (page paths)
+- native trace segments queryable
+
+Register the two cases in `.github/workflows/skywalking.yaml` e2e matrix.
+
+Additionally, every change to `application.yml` defaults made by this SWIP (new
+`miniprogram/*` entry in `enabledOtelMetricsRules`, new `miniprogram` entry in
+`lalFiles` and `malFiles`) **must** be mirrored in
+`test/e2e-v2/cases/storage/expected/config-dump.yml`. The storage e2e diffs
+`/debugging/config/dump` output against this file and fails on any default drift.
+
+### 12. Security Notice
+
+Mini-program SDKs run on end-user devices and post telemetry to OAP's OTLP + native-segment
+endpoints **from the public internet**, without agent-side authentication. Same
+exposure profile as iOS (SWIP-11) and browser-agent. Add a client-side-monitoring
+paragraph to `docs/en/security/README.md` covering:
+
+- The recommendation to front OAP with a rate-limiter or WAF for public-facing
+ endpoints (`/v1/logs`, `/v1/metrics`, `/v3/segments`).
+- The abuse surface (malformed payloads, high-volume senders, fake `sw8` headers)
+ and mitigation pointers.
+- Explicit mention that per-service authentication for client-side SDKs is out of
+ scope for v1; operators who need it should terminate at a gateway.
+
+### 13. Implementation Deliverables Checklist
+
+The design sections above stop at the rule-file / code-file level. The actual PR(s)
+implementing this SWIP must also ship:
+
+| Deliverable | Location |
+|---|---|
+| User-facing doc — WeChat | `docs/en/setup/backend/backend-wechat-mini-program-monitoring.md` |
+| User-facing doc — Alipay | `docs/en/setup/backend/backend-alipay-mini-program-monitoring.md` |
+| Docs navigation | Two new entries in `docs/menu.yml` under the existing "Mobile" section alongside iOS |
+| Changelog | Entry in `docs/en/changes/changes.md` under `#### OAP Server` (feature) and `#### Documentation` (the two guides) |
+| SWIP readme | Ensure this SWIP is listed under "Accepted SWIPs" in `docs/en/swip/readme.md` |
+| UI i18n | Separate PR in `apache/skywalking-booster-ui` for i18n keys `wechat_mini_program` / `alipay_mini_program` (§9) |
+
+## Imported Dependencies libs and their licenses
+
+No new OAP-side dependencies. All processing uses existing OTLP receiver, native trace
+receiver, OAL, LAL, and MAL infrastructure.
+
+The mini-program-monitor SDK itself (Apache-2.0) is an **external** dependency of the
+user's mini-program project, not of OAP. The showcase data generator images bundle the
+SDK's compiled JS, same license.
+
+## Compatibility
+
+- **Configuration:** two new layers + a new menu section + new MAL/LAL rule files —
+ additive, opt-in.
+- **Storage:** no new storage structures. Uses existing trace / metric / log storage.
+- **Protocols:** no protocol changes. Uses existing OTLP and SkyWalking native receivers.
+- **Layer mapping:** the change to `CommonAnalysisListener.getLayer()` is additive —
+ it only redirects traffic carrying the two new component ids; all other segments
+ continue to resolve to `Layer.GENERAL` / `Layer.FAAS` as today.
+- **Component library:** `10002` and `10003` are newly reserved ids in the JavaScript
+ range `[10000, 11000)`; no collision with existing entries.
+- **SDK version recommendation:** mini-program-monitor **≥ v0.4.0** is the recommended
+ baseline. v0.3.0 also works but with the legacy instance-id behavior below.
+ - SDK ≤ v0.2.x emits `componentId = 10001` (ajax-inherited) — its segments resolve
+ to `Layer.GENERAL` and do not benefit from this SWIP's layer / topology integration.
+ OTLP metrics + logs still flow through MAL / LAL because they key on the
+ `miniprogram.platform` resource attribute, which v0.2 already emits.
+- **Instance entity behavior across SDK versions:**
+ - SDK ≤ v0.3.x auto-generated `service.instance.id = mp-{random}` per session,
+ creating one OAP instance entity per device — usually undesirable. Operators on
+ v0.3.x can avoid this by passing `init({ serviceInstance: serviceVersion })`
+ explicitly.
+ - SDK ≥ v0.4.0 leaves `service.instance.id` unset by default. The three signal
+ pipelines then handle absence differently (see §2 "Instance coherence" table):
+ native segments produce a literal `-` instance entity; OTLP logs and metrics
+ create no instance entity at all. Per-instance dashboards are meaningful only
+ when the operator sets `serviceInstance`.
+ - Recommended operator pattern (SDK docs + e2e CI): set `serviceInstance` to a
+ version-scoped value (mirroring `service.version` or a release tag). Then all
+ three signal pipelines aggregate under the same OAP instance entity.
+ - Dashboards built against pre-v0.4 traffic see a long tail of `mp-*` instance ids;
+ after upgrade with no `serviceInstance` set, only the segment-side `-` entity
+ remains. Set `serviceInstance` to keep populated per-version dashboards.
+- **`server.address` sentinel change in SDK v0.4.0:** when the request URL has no
+ parseable `https?://host` prefix, OTLP now omits `server.address` (was `"unknown"`)
+ and segments substitute `-` for `peer`. MAL queries that group / filter on
+ `server.address == "unknown"` need to union the old sentinel with the new behavior
+ for data spanning the v0.3 → v0.4 upgrade boundary.
+
+## General usage docs
+
+### Prerequisites
+
+- Mini program instrumented with [mini-program-monitor](https://github.com/SkyAPM/mini-program-monitor) **≥ v0.4.0** recommended (clean `serviceInstance` defaults). v0.3.0 still works with manual `serviceInstance: serviceVersion` workaround.
+- SkyWalking OAP with the changes from this SWIP — OTLP HTTP receiver enabled (default on core REST port 12800), and the two new component ids registered
+
+### Mini Program Setup
+
+```js
+// WeChat (app.js)
+const { init } = require('mini-program-monitor');
+App({
+ onLaunch() {
+ init({
+ service: 'my-mini-program',
+ serviceVersion: 'v1.2.0',
+ // SDK ≥ v0.4.0 recommendation: set serviceInstance to a version-scoped value
+ // (mirroring service.version or a release tag). Leaving it unset means OTLP
+ // metrics + logs do not produce an instance entity at all (segments produce a
+ // literal `-`); per-version / per-release dashboards need this.
+ serviceInstance: 'v1.2.0',
+ collector: 'https://<oap-host>',
+ platform: 'wechat', // optional — auto-detected
+ enable: { tracing: true }, // opt-in: SkyWalking native segments
+ });
+ },
+});
+```
+
+```js
+// Alipay (app.js) — same API, different platform attribute
+init({
+ service: 'my-mini-program',
+ serviceVersion: 'v1.2.0',
+ serviceInstance: 'v1.2.0',
+ collector: 'https://<oap-host>',
+ platform: 'alipay',
+ enable: { tracing: true },
+});
+```
+
+### SkyWalking OAP Configuration
+
+Append the mini-program glob to `enabledOtelMetricsRules` and the LAL file to
+`lalFiles` in `application.yml` (preserve the existing defaults — don't replace them):
+
+```yaml
+receiver-otel:
+ selector: ${SW_OTEL_RECEIVER:default}
+ default:
+ enabledHandlers: ${SW_OTEL_RECEIVER_ENABLED_HANDLERS:"otlp-traces,otlp-metrics,otlp-logs"}
+ enabledOtelMetricsRules: ${SW_OTEL_RECEIVER_ENABLED_OTEL_METRICS_RULES:"<existing defaults>,miniprogram/*"}
+
+log-analyzer:
+ selector: ${SW_LOG_ANALYZER:default}
+ default:
+ lalFiles: ${SW_LOG_LAL_FILES:"<existing defaults>,miniprogram"}
+```
+
+`miniprogram/*` picks up all four MAL files under `otel-rules/miniprogram/`. The
+existing defaults (distributed with OAP) are a long list including `apisix`,
+`ios/*`, `kafka/*`, and the `default` LAL rule — these must be kept; otherwise
+non–mini-program workloads lose their MAL / LAL wiring.
+
+Native trace segments (`/v3/segments`) need no additional config — handled by the
+existing trace receiver. Layer is assigned automatically from the span's componentId.
+
+### What You'll See
+
+1. **Mobile > WeChat Mini Program** and **Mobile > Alipay Mini Program** menu items
+2. **Service list** per platform layer — one row per mini-program
+3. **Service dashboard** — launch time, render/paint timings, request percentiles,
+ error counts, **trace list**
+4. **Instance (version) dashboard** — same metrics scoped to a version, for rollout
+ and regression monitoring
+5. **Endpoint (page) dashboard** — per-page perf + error list
+6. **Trace view** — individual outgoing requests when `enable.tracing = true`, with
+ `sw8` propagation joining the mini-program's trace with downstream backend services
+
+### Limitations
+
+- **Alipay perf metrics are lifecycle-based approximations**, not native renderer
+ timings. Do not compare WeChat and Alipay perf numbers head-to-head.
+- WeChat-only metrics (`first_paint_time`, `route_duration`, `script_duration`,
+ `package_load_duration`, `pageNotFound` error) are absent from Alipay dashboards.
+- Device-level per-user aggregation is not supported by design — `serviceInstance`
+ is intended to be a version-scoped identifier, not per-device. SDK v0.4.0 dropped
+ the per-device auto-generator entirely; operators who genuinely need per-session
+ granularity can pass any string they want via `init({ serviceInstance: '…' })`,
+ but be aware OAP aggregates one instance entity per distinct value.
+- WebSocket, memory-warning, and network-status-change signals are not instrumented by
+ the current SDK.
\ No newline at end of file
diff --git a/docs/en/swip/readme.md b/docs/en/swip/readme.md
index f5979e4647..c36edba506 100644
--- a/docs/en/swip/readme.md
+++ b/docs/en/swip/readme.md
@@ -68,12 +68,13 @@ All accepted and proposed SWIPs can be found in [here](https://github.com/apache
## Known SWIPs
-Next SWIP Number: 12
+Next SWIP Number: 13
### Proposed SWIPs
### Accepted SWIPs
+- [SWIP-12 Support WeChat & Alipay Mini Program Monitoring](SWIP-12.md)
- [SWIP-11 Support iOS App Monitoring via OpenTelemetry](SWIP-11.md)
- [SWIP-10 Support Envoy AI Gateway Observability](SWIP-10/SWIP.md)
- [SWIP-9 Support Flink Monitoring](SWIP-9.md)
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java
index e5e24daa82..42c5f2f1fb 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java
@@ -25,8 +25,10 @@ import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Stream;
import org.apache.skywalking.oap.server.core.CoreModule;
import org.apache.skywalking.oap.server.core.analysis.Layer;
import org.apache.skywalking.oap.server.core.query.input.DashboardSetting;
@@ -39,53 +41,37 @@ import org.apache.skywalking.oap.server.library.util.StringUtil;
* UITemplateInitializer load the template from the config file in json format. It depends on the UI implementation only.
* Each config file should be only one dashboard setting json object.
* The dashboard names should be different in the same Layer and entity.
+ * <p>
+ * Folder discovery is automatic: every {@link Layer} enum value is probed as a folder
+ * (via {@code Layer.name().toLowerCase()}) plus the {@code "custom"} slot. Folders that
+ * don't exist are silently skipped. Extensions adding a new {@code Layer} do not need to
+ * touch this class.
+ * <p>
+ * Dev/extension reload: when environment variable {@code SW_UI_TEMPLATE_FORCE_RELOAD}
+ * is {@code true}, each template on disk is written via {@code addOrReplace} rather
+ * than {@code addIfNotExist}, so edits to shipped JSON take effect on the next OAP
+ * restart without needing to wipe the storage container.
*/
@Slf4j
public class UITemplateInitializer {
- public static String[] UI_TEMPLATE_FOLDER = new String[] {
- Layer.MESH.name(),
- Layer.GENERAL.name(),
- Layer.OS_LINUX.name(),
- Layer.MESH_CP.name(),
- Layer.MESH_DP.name(),
- Layer.MYSQL.name(),
- Layer.POSTGRESQL.name(),
- Layer.K8S.name(),
- Layer.BROWSER.name(),
- Layer.SO11Y_OAP.name(),
- Layer.VIRTUAL_DATABASE.name(),
- Layer.VIRTUAL_CACHE.name(),
- Layer.K8S_SERVICE.name(),
- Layer.SO11Y_SATELLITE.name(),
- Layer.APISIX.name(),
- Layer.VIRTUAL_MQ.name(),
- Layer.AWS_EKS.name(),
- Layer.OS_WINDOWS.name(),
- Layer.AWS_S3.name(),
- Layer.AWS_DYNAMODB.name(),
- Layer.AWS_GATEWAY.name(),
- Layer.REDIS.name(),
- Layer.ELASTICSEARCH.name(),
- Layer.RABBITMQ.name(),
- Layer.MONGODB.name(),
- Layer.KAFKA.name(),
- Layer.PULSAR.name(),
- Layer.BOOKKEEPER.name(),
- Layer.NGINX.name(),
- Layer.ROCKETMQ.name(),
- Layer.CLICKHOUSE.name(),
- Layer.ACTIVEMQ.name(),
- Layer.CILIUM_SERVICE.name(),
- Layer.SO11Y_JAVA_AGENT.name(),
- Layer.KONG.name(),
- Layer.SO11Y_GO_AGENT.name(),
- Layer.FLINK.name(),
- Layer.BANYANDB.name(),
- Layer.VIRTUAL_GENAI.name(),
- Layer.ENVOY_AI_GATEWAY.name(),
- Layer.IOS.name(),
- "custom"
- };
+ /**
+ * Every {@link Layer} enum value, lower-cased, plus the {@code "custom"} folder.
+ * Computed once from {@link Layer#values()} so adding a new {@code Layer} automatically
+ * causes its {@code ui-initialized-templates/<layer>/} folder to be scanned.
+ */
+ public static final String[] UI_TEMPLATE_FOLDER = Stream.concat(
+ Arrays.stream(Layer.values()).map(Layer::name),
+ Stream.of("custom")
+ ).toArray(String[]::new);
+
+ /**
+ * Environment variable: when {@code true}, templates on disk overwrite any previously
+ * seeded copy in storage on every boot. Read from the OS environment directly so
+ * operators / extenders can flip it without touching {@code application.yml}.
+ */
+ private static final boolean FORCE_RELOAD =
+ Boolean.parseBoolean(System.getenv("SW_UI_TEMPLATE_FORCE_RELOAD"));
+
private final UITemplateManagementService uiTemplateManagementService;
private final ObjectMapper mapper;
@@ -98,6 +84,9 @@ public class UITemplateInitializer {
}
public void initAll() throws IOException {
+ if (FORCE_RELOAD) {
+ log.info("SW_UI_TEMPLATE_FORCE_RELOAD=true — shipped UI templates will overwrite any previously seeded copy on this boot.");
+ }
for (String folder : UITemplateInitializer.UI_TEMPLATE_FOLDER) {
try {
File[] templateFiles = ResourceUtils.getPathFiles("ui-initialized-templates/" + folder.toLowerCase());
@@ -128,7 +117,11 @@ public class UITemplateInitializer {
setting.setId(inId);
setting.setConfiguration(configNode.toString());
- uiTemplateManagementService.addIfNotExist(setting);
+ if (FORCE_RELOAD) {
+ uiTemplateManagementService.addOrReplace(setting);
+ } else {
+ uiTemplateManagementService.addIfNotExist(setting);
+ }
}
private void verifyNameConflict(File template, String inId, String inNameKey) throws IOException {
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateManagementService.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateManagementService.java
index 0a31705b1b..a2a3e3b461 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateManagementService.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateManagementService.java
@@ -75,4 +75,19 @@ public class UITemplateManagementService implements Service {
getUITemplateManagementDAO().addTemplate(setting);
}
}
+
+ /**
+ * Add or replace: if a template with the same id already exists, overwrite it;
+ * otherwise create a new one. Used by the initializer when
+ * {@code SW_UI_TEMPLATE_FORCE_RELOAD=true} so that edits to shipped templates
+ * take effect without wiping the storage container.
+ */
+ public void addOrReplace(DashboardSetting setting) throws IOException {
+ DashboardConfiguration configuration = getUITemplateManagementDAO().getTemplate(setting.getId());
+ if (configuration == null) {
+ getUITemplateManagementDAO().addTemplate(setting);
+ } else {
+ getUITemplateManagementDAO().changeTemplate(setting);
+ }
+ }
}
diff --git a/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/UITemplateCheckerTest.java b/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/UITemplateCheckerTest.java
index e02b4b1c3d..56965ec879 100644
--- a/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/UITemplateCheckerTest.java
+++ b/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/UITemplateCheckerTest.java
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Locale;
@@ -45,8 +46,15 @@ public class UITemplateCheckerTest {
Set<String> dashboardIds = new HashSet<>();
Set<String> dashboardNames = new HashSet<>();
for (String folder : UITemplateInitializer.UI_TEMPLATE_FOLDER) {
- File[] templateFiles = ResourceUtils.getPathFiles("ui-initialized-templates/" + folder.toLowerCase(
- Locale.ROOT));
+ File[] templateFiles;
+ try {
+ templateFiles = ResourceUtils.getPathFiles("ui-initialized-templates/" + folder.toLowerCase(
+ Locale.ROOT));
+ } catch (FileNotFoundException e) {
+ // Layer enum values without an on-disk template folder are skipped —
+ // mirrors UITemplateInitializer.initAll() behavior post-auto-discovery.
+ continue;
+ }
for (File template : templateFiles) {
JsonNode jsonNode;
try {