ambiguous-pointer commented on PR #1487: URL: https://github.com/apache/dubbo-admin/pull/1487#issuecomment-5082867850
## Summary This PR introduces a **unified event timeline** for Dubbo applications, instances, and services, ingesting both **Kubernetes native events** and **registry-side lifecycle events** (ZooKeeper / Nacos) into a single `K8sEventResource` store, and exposing them through Console API endpoints with a frontend timeline visualization. Closes [#1472](https://github.com/apache/dubbo-admin/issues/1472). Design discussed in [#1474](https://github.com/apache/dubbo-admin/discussions/1474). --- ## Architecture ### Unified Event Model All events — whether from K8s or from a registry — are stored as `K8sEventResource` with an `EventSource` discriminator: | EventSource | Source | Description | |-------------|--------|-------------| | `"KUBERNETES"` | K8s `api/v1/events` | Pod scheduling, image pull, readiness probe, restarts, etc. | | `"REGISTRY"` | ZK / Nacos subscribers | Instance registered/deregistered/updated, config/metadata added/updated/deleted | The `K8sEvent` proto spec captures both origins in a single schema: ```protobuf message K8sEvent { string namespace = 1; // K8s namespace (empty for registry events) string reason = 2; // K8s reason or registry action string message = 3; // human-readable description string type = 4; // "Normal" | "Warning" string involvedObjKind = 5; // K8s: "Pod" | Registry: "registry" | "config" | "metadata" string involvedObjName = 6; // K8s: pod name | Registry: hierarchical encoding (see below) string sourceComponent = 7; // K8s: source component | Registry: "zookeeper" | "nacos" string sourceHost = 8; // K8s: source host | Registry: instance IP string firstTimestamp = 9; string lastTimestamp = 10; int32 count = 11; string eventSource = 12; // "KUBERNETES" | "REGISTRY" } ``` ### Data Flow ``` ┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ │ K8s API Server │ │ ZK /services/* │ │ Nacos naming │ │ /api/v1/events │ │ /dubbo/metadata/ │ │ service list │ │ (Pod only) │ │ /dubbo/config/ │ │ / config page │ └────────┬─────────┘ └──────────┬───────────┘ └──────────┬───────────┘ │ │ │ ▼ ▼ ▼ ┌────────────────────────────────────────────────────────────────────┐ │ Informer (List + Watch) │ │ │ │ K8sEventListerWatcher ZKLW(x4) NacosLW(x6) │ │ │ │ │ │ │ │ TransformFunc: │ │ │ │ │ k8s Event → │ │ │ │ │ K8sEventResource │ │ │ │ │ (mesh=engineID, │ │ │ │ │ EventSource= │ │ │ │ │ "KUBERNETES") │ │ │ │ │ │ │ │ └────┼─────────────────────────┼────────────┼────────────────────────┘ │ │ │ └──────────┬──────────────┴────────────┘ ▼ ┌─────────────────────┐ │ Informer writes to │ │ Store + EventBus │ └─────────┬───────────┘ │ ┌─────────▼──────────┐ │ EventBus │ │ (async dispatch) │ └─┬──────────────┬────┘ │ │ ▼ ▼ ┌────────────┐ ┌──────────────────────────────┐ │ Other │ │ K8sEventSubscriber │ │ subscribers│ │ │ │ (instance,│ │ For KUBERNETES events: │ │ service, │ │ 1. Filter by DubboAppId │ │ ...) │ │ 2. Filter Pod kind only │ │ │ │ 3. alignMeshFromRtInstance()│ │ │ │ 4. prefixTimestampKey() │ │ │ │ 5. eventStore.Add() │ │ │ │ │ │ │ │ For REGISTRY events: │ │ │ │ → writeEvent() directly │ │ │ │ (already enriched) │ └────────────┘ └──────────────┬───────────────┘ │ ┌────────────────┘ ▼ ┌─────────────────────────────────────────────────────────┐ │ K8sEvent store (PG / Memory) │ │ │ │ indexed by: mesh, involvedObjKind, involvedObjName, │ │ eventType, eventSource │ │ │ │ keys prefixed with descending nano-timestamp: │ │ "{sortablePrefix}-{namespace}/{name}" │ │ → alphabetical key sort = chronological (newest first) │ └────────────────────────┬────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Console API (event.go) │ │ │ │ GET /application/event — K8s events for app pods │ │ GET /instance/event — K8s events for instance pod │ │ GET /service/event — Registry config/metadata │ │ events for service │ └────────────────────────┬────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Frontend EventTimeline.vue │ │ Alternating L/R timeline with infinite scroll │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 2026-06-07 17:39:14 │ │ │ │ ⚠ Error updating Endpoint Slices... │ │ │ │ endpoint-slice-ctrl│ │ │ ├──────────────────────────────────────────────────┤ │ │ │ 2026-06-07 10:25:04 │ │ │ │ ✓ Zookeeper provider metadata added: │ │ │ │ demo-app → com.example.TestService │ │ │ │ Zookeeper │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` --- ## Key Design Decisions ### 1. Unified `K8sEventResource` (not dual-resource) Registry events are NOT stored as a separate `PlatformEventResource` type. Instead, `RecordRegistryEvent()` creates `K8sEventResource` instances with `EventSource = "REGISTRY"`. This has several advantages: - **Single store, single API**: Console queries hit one resource kind for both K8s and registry events. - **Unified pagination**: `PageListByIndexes` works identically for both sources. - **No EventBus bypass**: Registry events use `RecordRegistryEvent()` → `eventStore.Add()`, writing directly to the store (no round-trip through EventBus). Downstream subscribers can still observe K8sEvent resources if needed. ### 2. Hierarchical `InvolvedObjName` encoding Registry events encode entity identity into `InvolvedObjName` for single-index prefix matching: | Event category | InvolvedObjName | Example | |---|---|---| | Instance (registry) | `{appName}/{ip}` | `test-event/10.244.1.89` | | Config (ZK) | `{appName}/{serviceName}` | `test-event/com.example.TestService` | | Metadata (ZK) | `{appName}/{serviceName}` | `test-event/com.example.TestService` | K8s events use the Pod name as `InvolvedObjName` (e.g., `test-event-app-7cd9897566-xlxpf`). The Console API's `HasPrefix` query can match both encoding styles with a single index condition. ### 3. Timestamp-prefixed keys for chronological ordering `PageListByIndexes` sorts by resource key alphabetically. To make this chronological, `prefixTimestampKey()` prepends a descending nano-timestamp: ``` key = "{MAX_INT64 - timestampNano}_{namespace}/{podName}" ``` More recent events have smaller prefixes → sort first. Registry events include a nano-timestamp in their name from `RecordRegistryEvent()`. ### 4. Mesh alignment for K8s events K8s `Event` objects have no concept of a "Dubbo mesh". The `K8sEventListerWatcher` assigns the engine ID as the initial mesh, then `K8sEventSubscriber.alignMeshFromRuntimeInstance()` resolves the correct discovery mesh by looking up the corresponding `RuntimeInstance` via the Pod name, using the `ByRuntimeInstanceNameIndex` to search across all meshes. ### 5. K8s Event filtering (Pod only, Dubbo only) - **Field selector**: `involvedObject.kind=Pod` — filters at the K8s API level, avoiding cluster-wide noise (Node events, Namespace events, etc.). - **DubboAppIdentifier**: In `K8sEventSubscriber`, non-Dubbo Pods are discarded by checking the configured `DubboAppIdentifier` (e.g., by label `app`). Events from Pods without the identifier are skipped before reaching the store. --- ## File-by-File Breakdown ### API Layer | File | Description | |------|-------------| | `api/mesh/v1alpha1/k8s_event.proto` | Proto definition for unified `K8sEvent` message | | `api/mesh/v1alpha1/k8s_event.go` | Hand-written Go type with `Clone()` (no protobuf generation needed for simple struct) | ### Core Resource Types & Indexes | File | Description | |------|-------------| | `pkg/core/resource/apis/mesh/v1alpha1/k8sevent_types.go` | `K8sEventResource` + `K8sEventResourceList` types with DeepCopy, SetItems, etc. | | `pkg/core/store/index/k8s_event.go` | Indexes: `ByK8sEventInvolvedObjKind`, `ByK8sEventInvolvedObjName`, `ByK8sEventType`, `ByK8sEventSource` | | `pkg/core/store/index/runtime_instance.go` | Added `ByRuntimeInstanceNameIndex` for cross-mesh RuntimeInstance lookup by pod name | ### K8s Engine | File | Description | |------|-------------| | `pkg/engine/kubernetes/factory.go` | Registers `K8sEventListerWatcher` alongside existing `PodListerWatcher` | | `pkg/engine/kubernetes/listerwatcher/k8s_event.go` | `K8sEventListerWatcher` — List+Watch `api/v1/events` (Pod only), transforms to `K8sEventResource` with zero-timestamp guard | ### Discovery Subscribers | File | Description | |------|-------------| | `pkg/core/discovery/subscriber/k8s_event.go` | `K8sEventSubscriber` — filters K8s events by DubboAppIdentifier + Pod kind, aligns mesh from RuntimeInstance, prefixes timestamp keys for chronological sort. Passes REGISTRY events through directly. | | `pkg/core/discovery/subscriber/registry_event_adapter.go` | `RecordRegistryEvent()` — shared utility that creates `K8sEventResource` with `EventSource="REGISTRY"` and hierarchical `InvolvedObjName`. Warns on empty Mesh/Message instead of silently dropping. | | `pkg/core/discovery/subscriber/zk_config.go` | `ZKConfigEventSubscriber` — records tag-route / condition-route / dynamic-config added/updated/deleted events via `recordConfigPlatformEvent()` | | `pkg/core/discovery/subscriber/zk_metadata.go` | `ZKMetadataEventSubscriber` — records provider/consumer metadata added/updated events via `recordMetadataPlatformEvent()` | | `pkg/core/discovery/subscriber/nacos_service.go` | `NacosServiceEventSubscriber` — records instance registered/deregistered/updated and consumer metadata added/removed/updated events via `recordNacosInstanceEvents()` / `recordNacosConsumerEvents()` | | `pkg/core/discovery/component.go` | Wires `K8sEventSubscriber` into discovery lifecycle (only when engine type is Kubernetes) | ### Console API | File | Description | |------|-------------| | `pkg/console/service/event.go` | Core query logic: `ListApplicationEvents`, `ListInstanceEvents`, `ListServiceEvents`. Each resolves pod names from RuntimeInstances, queries K8sEvent store, filters by involved object name, and returns paginated results. Negative offset and zero pageSize are safely clamped. | | `pkg/console/handler/event.go` | HTTP handlers: `GetApplicationEvents`, `GetInstanceEvents`, `GetServiceEvents` | | `pkg/console/model/event.go` | Request/response types: `EventQueryReq`, `EventItem`, `EventListResp` | | `pkg/console/router/router.go` | Routes: `GET /instance/event`, `GET /application/event`, `GET /service/event` | ### Frontend | File | Description | |------|-------------| | `ui-vue3/src/components/EventTimeline.vue` | Reusable timeline component with Ant Design `a-timeline`, alternating layout, normal/warning icons, IntersectionObserver-based infinite scroll (no watch-driven preloading) | | `ui-vue3/src/views/resources/applications/tabs/event.vue` | Application event tab — wires EventTimeline with `appName` + `mesh` params | | `ui-vue3/src/views/resources/instances/tabs/event.vue` | Instance event tab — wires EventTimeline with `instanceName` + `ip` + `appName` + `mesh` params | | `ui-vue3/src/views/resources/services/tabs/event.vue` | Service event tab — wires EventTimeline with `serviceName` + `appName` + `mesh` params | | `ui-vue3/src/api/service/app.ts` | `listApplicationEvent()` API function | | `ui-vue3/src/api/service/instance.ts` | `listInstanceEvent()` API function | | `ui-vue3/src/api/service/service.ts` | `listServiceEvent()` API function | | `ui-vue3/src/types/api.ts` | `EventItem` TypeScript type | | `ui-vue3/src/router/defaultRoutes.ts` | Restored event tab routes (previously commented out) | ### Config & Minor Changes | File | Description | |------|-------------| | `pkg/core/events/component.go` | "no subscriber for resource" log level: `INFO` → `DEBUG` to reduce noise for `K8sEvent` and `Service` kinds that intentionally have no subscriber | --- ## API Reference ### `GET /api/v1/application/event` Query parameters: | Param | Required | Description | |-------|----------|-------------| | `mesh` | Yes | Dubbo mesh name | | `appName` | Yes | Application name | | `pageOffset` | No | Pagination offset (default 0, clamped to ≥0) | | `pageSize` | No | Page size (default 20) | Returns: `{ code, message, data: { list: EventItem[], total: number } }` ### `GET /api/v1/instance/event` | Param | Required | Description | |-------|----------|-------------| | `mesh` | Yes | Dubbo mesh name | | `ip` | Yes | Instance IP address | | `instanceName` | No | Instance name (used as additional match candidate) | | `appName` | No | Application name (used for registry event matching) | | `pageOffset` | No | Pagination offset | | `pageSize` | No | Page size | ### `GET /api/v1/service/event` | Param | Required | Description | |-------|----------|-------------| | `mesh` | Yes | Dubbo mesh name | | `serviceName` | Yes | Service name | | `appName` | Yes | Application name | | `pageOffset` | No | Pagination offset | | `pageSize` | No | Page size | ### EventItem schema ```json { "time": "2026-07-26 15:40:54", "type": "normal", "message": "Started container agnhost", "source": "kubelet" } ``` --- ## E2E Verification Tested against a 3-node cluster: | Scenario | Result | |----------|--------| | Application events (`mesh=dubbo-samples-shop`, `appName=test-event`) | 96 events returned | | Instance events (`mesh=dubbo-samples-shop`, `ip=10.244.1.93`) | 4 events per pod returned | | Timeline ordering | Events sorted by time descending ✓ | | Source tags (kubelet, default-scheduler) | Rendered correctly ✓ | | Warning events | Yellow icon + orange tag ✓ | | Empty state | "暂无事件" placeholder ✓ | | Infinite scroll | IntersectionObserver-based loadMore ✓ | --- ## Known Limitations 1. **No event TTL / cleanup** — Events accumulate indefinitely in the store. No retention policy, TTL, or automatic cleanup for any store backend. 2. **No subscriber for K8sEvent on EventBus** — K8sEvent and Service resources are stored but no downstream subscriber processes them (by design — they only need storage). The EventBus emits a DEBUG-level "no subscriber" log. 3. **Registry events bypass EventBus** — `RecordRegistryEvent()` writes directly to the store rather than emitting through EventBus. This is intentional (avoids a round-trip), but means registry events are not observable by downstream subscribers of K8sEvent resources. 4. **Service events are registry-only** — K8s has no native "service" concept, so `/service/event` returns only registry-side config and metadata events. K8s-sourced events naturally do not appear here. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
