This is an automated email from the ASF dual-hosted git repository.

robocanic pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-admin.git


The following commit(s) were added to refs/heads/develop by this push:
     new 6da958c4 feature: Implement Event stream for 
Application/Instance/Service  #1472 (#1487)
6da958c4 is described below

commit 6da958c4dd77a5021475dee114e04250b0352fee
Author: Comrade Yi <[email protected]>
AuthorDate: Sun Aug 9 19:38:37 2026 +0800

    feature: Implement Event stream for Application/Instance/Service  #1472 
(#1487)
    
    * Feat: Add event timeline for application/instance/service with K8s event 
ingestion ([#1472](https://github.com/apache/dubbo-admin/issues/1472))
    
    - Add K8sEvent resource type (proto + Go spec) and store indexes
    - Add K8sEventListerWatcher to watch K8s /api/v1/events via client-go
    - Register K8sEventListerWatcher in Kubernetes EngineFactory
    - Add /application/event, /instance/event, /service/event console API 
endpoints
    - Add EventTimeline shared Vue component with normal/warning node styles
    - Wire up event tabs for application, instance, and service detail pages
    - Un-hide event tab routes in frontend router
    - Add mock event handlers for development
    - Add PlatformEvent resource type (platformevent_types.go) with store 
indexes
    - Add shared platform_event_recorder utility for event recording
    - Record ZK config change events (tag-route, condition-route, 
dynamic-config)
    - Record ZK metadata events (provider/consumer metadata added/updated)
    - Record Nacos instance registration/deregistration events
    - Record Nacos consumer metadata change events
    - Merge K8s events and Platform events into unified timeline in event query 
service
    - Downgrade "no subscriber" log level from INFO to DEBUG to reduce log noise
    
    * Fix: Address review feedback for K8s event timeline — zero timestamps, 
watch spam, silent drops, delete-before-add
    
    - K8sEventListerWatcher: check IsZero() before formatting 
FirstTimestamp/LastTimestamp
      to avoid emitting synthetic "0001-01-01 00:00:00" values that break 
timeline ordering.
    - EventTimeline.vue: remove unconditional watch() that triggered cascading 
loadMore calls;
      rely solely on IntersectionObserver for scroll-based pagination.
    - RecordRegistryEvent: log a warning when dropping events due to empty Mesh 
or Message
      instead of silently discarding them.
    - K8sEventSubscriber.writeEvent: delete the informer-written (original-key) 
entry only
      after a successful Add of the timestamp-prefixed entry, preventing 
permanent event
      loss when Add fails.
    
    * Refactor: Rename K8sEvent to LifecycleEvent for unified event model
    
    K8sEvent was originally named for K8s-sourced events only, but now also
    carries registry-side events (ZK/Nacos) via the EventSource discriminator.
    Rename to LifecycleEvent to accurately reflect its role as a unified
    lifecycle event type covering both K8s and registry origins.
    
    - Proto: K8sEvent → LifecycleEvent
    - Resource: K8sEventResource → LifecycleEventResource
    - Kind: K8sEventKind → LifecycleEventKind
    - Subscriber: K8sEventSubscriber → LifecycleEventSubscriber
    - Index constants: ByK8sEvent* → ByLifecycleEvent*
    - Files: k8s_event.* → lifecycle_event.*
    - Retained: K8sEventListerWatcher (K8s-specific component)
---
 api/mesh/v1alpha1/lifecycle_event.go               |  55 +++
 api/mesh/v1alpha1/lifecycle_event.proto            |  38 ++
 pkg/console/handler/event.go                       |  77 ++++
 pkg/console/handler/mesh.go                        |  14 +-
 pkg/console/{handler/mesh.go => model/event.go}    |  41 ++-
 pkg/console/router/router.go                       |   3 +
 pkg/console/service/event.go                       | 312 ++++++++++++++++
 pkg/core/discovery/component.go                    |   8 +
 pkg/core/discovery/subscriber/lifecycle_event.go   | 217 ++++++++++++
 pkg/core/discovery/subscriber/nacos_service.go     | 197 ++++++++++-
 .../discovery/subscriber/registry_event_adapter.go | 126 +++++++
 pkg/core/discovery/subscriber/zk_config.go         |  44 +++
 pkg/core/discovery/subscriber/zk_metadata.go       |  35 ++
 pkg/core/events/component.go                       |   2 +-
 .../apis/mesh/v1alpha1/lifecycle_event_types.go    | 166 +++++++++
 pkg/core/store/index/lifecycle_event.go            |  87 +++++
 pkg/core/store/index/runtime_instance.go           |  19 +-
 pkg/engine/kubernetes/factory.go                   |   7 +
 pkg/engine/kubernetes/listerwatcher/k8s_event.go   | 107 ++++++
 ui-vue3/src/api/service/app.ts                     |   7 +-
 ui-vue3/src/api/service/instance.ts                |  27 +-
 ui-vue3/src/api/service/service.ts                 |  13 +
 ui-vue3/src/base/http/request.ts                   |  22 +-
 ui-vue3/src/base/i18n/en.ts                        |   1 +
 ui-vue3/src/base/i18n/zh.ts                        |   1 +
 ui-vue3/src/components/EventTimeline.vue           | 208 +++++++++++
 ui-vue3/src/mocks/handlers/app.ts                  |   8 +-
 ui-vue3/src/mocks/handlers/instance.ts             |  22 +-
 ui-vue3/src/mocks/handlers/service.ts              |  20 +-
 ui-vue3/src/router/defaultRoutes.ts                |   3 -
 ui-vue3/src/types/api.ts                           |   7 +
 .../views/resources/applications/tabs/event.vue    | 187 +++-------
 .../instances/slots/InstanceTabHeaderSlot.vue      |  13 +-
 .../src/views/resources/instances/tabs/detail.vue  | 394 +++++++++++----------
 .../src/views/resources/instances/tabs/event.vue   |  65 +++-
 .../src/views/resources/services/tabs/event.vue    | 114 +++---
 36 files changed, 2223 insertions(+), 444 deletions(-)

diff --git a/api/mesh/v1alpha1/lifecycle_event.go 
b/api/mesh/v1alpha1/lifecycle_event.go
new file mode 100644
index 00000000..d39bee8a
--- /dev/null
+++ b/api/mesh/v1alpha1/lifecycle_event.go
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package v1alpha1
+
+// LifecycleEvent is the spec for a K8s event resource, capturing both K8s 
native events
+// and registry-side lifecycle events in a unified format.
+type LifecycleEvent struct {
+       Namespace       string `json:"namespace,omitempty"`
+       Reason          string `json:"reason,omitempty"`
+       Message         string `json:"message,omitempty"`
+       Type            string `json:"type,omitempty"`
+       InvolvedObjKind string `json:"involvedObjKind,omitempty"`
+       InvolvedObjName string `json:"involvedObjName,omitempty"`
+       SourceComponent string `json:"sourceComponent,omitempty"`
+       SourceHost      string `json:"sourceHost,omitempty"`
+       FirstTimestamp  string `json:"firstTimestamp,omitempty"`
+       LastTimestamp   string `json:"lastTimestamp,omitempty"`
+       Count           int32  `json:"count,omitempty"`
+       EventSource     string `json:"eventSource,omitempty"`
+}
+
+func (e *LifecycleEvent) Clone() *LifecycleEvent {
+       if e == nil {
+               return nil
+       }
+       return &LifecycleEvent{
+               Namespace:       e.Namespace,
+               Reason:          e.Reason,
+               Message:         e.Message,
+               Type:            e.Type,
+               InvolvedObjKind: e.InvolvedObjKind,
+               InvolvedObjName: e.InvolvedObjName,
+               SourceComponent: e.SourceComponent,
+               SourceHost:      e.SourceHost,
+               FirstTimestamp:  e.FirstTimestamp,
+               LastTimestamp:   e.LastTimestamp,
+               Count:           e.Count,
+               EventSource:     e.EventSource,
+       }
+}
diff --git a/api/mesh/v1alpha1/lifecycle_event.proto 
b/api/mesh/v1alpha1/lifecycle_event.proto
new file mode 100644
index 00000000..189389c6
--- /dev/null
+++ b/api/mesh/v1alpha1/lifecycle_event.proto
@@ -0,0 +1,38 @@
+syntax = "proto3";
+
+package dubbo.mesh.v1alpha1;
+
+option go_package = "github.com/apache/dubbo-admin/api/mesh/v1alpha1";
+
+import "api/mesh/options.proto";
+
+message LifecycleEvent {
+  option (dubbo.mesh.resource).name = "LifecycleEvent";
+  option (dubbo.mesh.resource).plural_name = "LifecycleEvents";
+  option (dubbo.mesh.resource).package = "mesh";
+  option (dubbo.mesh.resource).is_experimental = true;
+
+  string namespace = 1;
+
+  string reason = 2;
+
+  string message = 3;
+
+  string type = 4;
+
+  string involvedObjKind = 5;
+
+  string involvedObjName = 6;
+
+  string sourceComponent = 7;
+
+  string sourceHost = 8;
+
+  string firstTimestamp = 9;
+
+  string lastTimestamp = 10;
+
+  int32 count = 11;
+
+  string eventSource = 12;
+}
diff --git a/pkg/console/handler/event.go b/pkg/console/handler/event.go
new file mode 100644
index 00000000..acb546f8
--- /dev/null
+++ b/pkg/console/handler/event.go
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package handler
+
+import (
+       "net/http"
+
+       "github.com/gin-gonic/gin"
+
+       consolectx "github.com/apache/dubbo-admin/pkg/console/context"
+       "github.com/apache/dubbo-admin/pkg/console/model"
+       "github.com/apache/dubbo-admin/pkg/console/service"
+       "github.com/apache/dubbo-admin/pkg/console/util"
+)
+
+func GetApplicationEvents(ctx consolectx.Context) gin.HandlerFunc {
+       return func(c *gin.Context) {
+               req := &model.EventQueryReq{}
+               if err := c.ShouldBindQuery(req); err != nil {
+                       util.HandleArgumentError(c, err)
+                       return
+               }
+               resp, err := service.ListApplicationEvents(ctx, req)
+               if err != nil {
+                       util.HandleServiceError(c, err)
+                       return
+               }
+               c.JSON(http.StatusOK, model.NewSuccessResp(resp))
+       }
+}
+
+func GetInstanceEvents(ctx consolectx.Context) gin.HandlerFunc {
+       return func(c *gin.Context) {
+               req := &model.EventQueryReq{}
+               if err := c.ShouldBindQuery(req); err != nil {
+                       util.HandleArgumentError(c, err)
+                       return
+               }
+               resp, err := service.ListInstanceEvents(ctx, req)
+               if err != nil {
+                       util.HandleServiceError(c, err)
+                       return
+               }
+               c.JSON(http.StatusOK, model.NewSuccessResp(resp))
+       }
+}
+
+func GetServiceEvents(ctx consolectx.Context) gin.HandlerFunc {
+       return func(c *gin.Context) {
+               req := &model.EventQueryReq{}
+               if err := c.ShouldBindQuery(req); err != nil {
+                       util.HandleArgumentError(c, err)
+                       return
+               }
+               resp, err := service.ListServiceEvents(ctx, req)
+               if err != nil {
+                       util.HandleServiceError(c, err)
+                       return
+               }
+               c.JSON(http.StatusOK, model.NewSuccessResp(resp))
+       }
+}
diff --git a/pkg/console/handler/mesh.go b/pkg/console/handler/mesh.go
index 227c3427..36091ccf 100644
--- a/pkg/console/handler/mesh.go
+++ b/pkg/console/handler/mesh.go
@@ -24,11 +24,12 @@ import (
        "github.com/gin-gonic/gin"
 
        discoverycfg "github.com/apache/dubbo-admin/pkg/config/discovery"
+       enginecfg "github.com/apache/dubbo-admin/pkg/config/engine"
        consolectx "github.com/apache/dubbo-admin/pkg/console/context"
        "github.com/apache/dubbo-admin/pkg/console/model"
 )
 
-// ListMeshes list all meshes(discoveries) defined in config
+// ListMeshes list all meshes(discoveries + engine) defined in config
 func ListMeshes(ctx consolectx.Context) gin.HandlerFunc {
        return func(c *gin.Context) {
                discoveries := ctx.Config().Discovery
@@ -39,6 +40,17 @@ func ListMeshes(ctx consolectx.Context) gin.HandlerFunc {
                                Type: string(item.Type),
                        }
                })
+
+               // Include the engine as a mesh so K8s resources are 
discoverable in the UI
+               engineCfg := ctx.Config().Engine
+               if engineCfg != nil && engineCfg.Type != enginecfg.Mock {
+                       meshes = append(meshes, model.MeshResp{
+                               ID:   engineCfg.ID,
+                               Name: engineCfg.Name,
+                               Type: string(engineCfg.Type),
+                       })
+               }
+
                c.JSON(http.StatusOK, model.NewSuccessResp(meshes))
        }
 }
diff --git a/pkg/console/handler/mesh.go b/pkg/console/model/event.go
similarity index 53%
copy from pkg/console/handler/mesh.go
copy to pkg/console/model/event.go
index 227c3427..57b76aed 100644
--- a/pkg/console/handler/mesh.go
+++ b/pkg/console/model/event.go
@@ -15,30 +15,29 @@
  * limitations under the License.
  */
 
-package handler
+package model
 
 import (
-       "net/http"
+       coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
+)
 
-       "github.com/duke-git/lancet/v2/slice"
-       "github.com/gin-gonic/gin"
+type EventQueryReq struct {
+       AppName      string `form:"appName"`
+       InstanceName string `form:"instanceName"`
+       InstanceIP   string `form:"ip"`
+       ServiceName  string `form:"serviceName"`
+       Mesh         string `form:"mesh"`
+       coremodel.PageReq
+}
 
-       discoverycfg "github.com/apache/dubbo-admin/pkg/config/discovery"
-       consolectx "github.com/apache/dubbo-admin/pkg/console/context"
-       "github.com/apache/dubbo-admin/pkg/console/model"
-)
+type EventItem struct {
+       Time    string `json:"time"`
+       Type    string `json:"type"`
+       Message string `json:"message"`
+       Source  string `json:"source"`
+}
 
-// ListMeshes list all meshes(discoveries) defined in config
-func ListMeshes(ctx consolectx.Context) gin.HandlerFunc {
-       return func(c *gin.Context) {
-               discoveries := ctx.Config().Discovery
-               meshes := slice.Map(discoveries, func(index int, item 
*discoverycfg.Config) model.MeshResp {
-                       return model.MeshResp{
-                               ID:   item.ID,
-                               Name: item.Name,
-                               Type: string(item.Type),
-                       }
-               })
-               c.JSON(http.StatusOK, model.NewSuccessResp(meshes))
-       }
+type EventListResp struct {
+       List  []*EventItem `json:"list"`
+       Total int          `json:"total"`
 }
diff --git a/pkg/console/router/router.go b/pkg/console/router/router.go
index 85704c47..846306b6 100644
--- a/pkg/console/router/router.go
+++ b/pkg/console/router/router.go
@@ -50,6 +50,7 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) {
                        instanceConfig.GET("/operatorLog", 
handler.InstanceConfigOperatorLogGET(ctx))
                        instanceConfig.PUT("/operatorLog", 
handler.InstanceConfigOperatorLogPUT(ctx))
                }
+               instance.GET("/event", handler.GetInstanceEvents(ctx))
                instance.GET("/metric-dashboard", 
handler.GetGrafanaDashboard(ctx, handler.InstanceDimension, 
handler.MetricDashboard))
                instance.GET("/trace-dashboard", 
handler.GetGrafanaDashboard(ctx, handler.InstanceDimension, 
handler.TraceDashboard))
                instance.GET("/metrics-list", handler.GetMetricsList(ctx))
@@ -73,6 +74,7 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) {
                        applicationConfig.GET("/gray", 
handler.ApplicationConfigGrayGET(ctx))
                        applicationConfig.PUT("/gray", 
handler.ApplicationConfigGrayPUT(ctx))
                }
+               application.GET("/event", handler.GetApplicationEvents(ctx))
                application.GET("/metric-dashboard", 
handler.GetGrafanaDashboard(ctx, handler.AppDimension, handler.MetricDashboard))
                application.GET("/trace-dashboard", 
handler.GetGrafanaDashboard(ctx, handler.AppDimension, handler.TraceDashboard))
        }
@@ -107,6 +109,7 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) {
                service.GET("/search", handler.SearchServices(ctx))
                service.GET("/graph", handler.GetServiceGraph(ctx))
                service.GET("/detail", handler.GetServiceDetail(ctx))
+               service.GET("/event", handler.GetServiceEvents(ctx))
                service.GET("/interfaces", handler.GetServiceInterfaces(ctx))
        }
 
diff --git a/pkg/console/service/event.go b/pkg/console/service/event.go
new file mode 100644
index 00000000..1bae199e
--- /dev/null
+++ b/pkg/console/service/event.go
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package service
+
+import (
+       "strings"
+
+       consolectx "github.com/apache/dubbo-admin/pkg/console/context"
+       "github.com/apache/dubbo-admin/pkg/console/model"
+       "github.com/apache/dubbo-admin/pkg/core/logger"
+       "github.com/apache/dubbo-admin/pkg/core/manager"
+       meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
+       coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
+       "github.com/apache/dubbo-admin/pkg/core/store/index"
+)
+
+const defaultPageSize = 20
+
+// ListApplicationEvents returns a paginated event timeline for a Dubbo 
application.
+// Resolves pod names from all instances belonging to this app, then matches
+// events by InvolvedObjName against both the pod names and the registry event
+// encoding (appName/...).
+func ListApplicationEvents(ctx consolectx.Context, req *model.EventQueryReq) 
(*model.EventListResp, error) {
+       matchNames := resolveAppEventNames(ctx, req)
+
+       allConditions := []index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: 
index.Equals},
+       }
+       resources, err := 
manager.ListByIndexes[*meshresource.LifecycleEventResource](
+               ctx.ResourceManager(),
+               meshresource.LifecycleEventKind,
+               allConditions,
+       )
+       if err != nil {
+               return nil, err
+       }
+
+       filtered := filterEventsByNames(resources, matchNames)
+
+       offset := req.PageOffset
+       if offset < 0 {
+               offset = 0
+       }
+       if offset > len(filtered) {
+               offset = len(filtered)
+       }
+       pageSize := req.PageSize
+       if pageSize <= 0 {
+               pageSize = defaultPageSize
+       }
+       end := offset + pageSize
+       if end > len(filtered) {
+               end = len(filtered)
+       }
+       paged := filtered[offset:end]
+
+       return 
toEventListResp(&coremodel.PageData[*meshresource.LifecycleEventResource]{
+               Pagination: coremodel.Pagination{
+                       Total:      len(filtered),
+                       PageOffset: req.PageOffset,
+                       PageSize:   req.PageSize,
+               },
+               Data: paged,
+       }), nil
+}
+
+// resolveAppEventNames returns all InvolvedObjName values that could match
+// events for the given application. It collects pod names from 
RuntimeInstances
+// belonging to this app, plus the registry event prefix and the K8s pod name 
prefix.
+func resolveAppEventNames(ctx consolectx.Context, req *model.EventQueryReq) 
[]string {
+       names := make([]string, 0)
+
+       if req.AppName == "" {
+               return names
+       }
+
+       // Registry event format: appName/... (matched by HasPrefix in filter)
+       names = append(names, req.AppName+"/")
+
+       // K8s Deployment pod names follow {appName}-{hash}-{hash}.
+       // This HasPrefix fallback catches events even after RuntimeInstances 
are gone.
+       names = append(names, req.AppName+"-")
+
+       // Find all InstanceResources for this app, then map to pod names via 
RuntimeInstance IP.
+       instances, err := manager.ListByIndexes[*meshresource.InstanceResource](
+               ctx.ResourceManager(),
+               meshresource.InstanceKind,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                       {IndexName: index.ByInstanceAppNameIndex, Value: 
req.AppName, Operator: index.Equals},
+               },
+       )
+       if err != nil {
+               logger.Warnf("resolveAppEventNames: failed to list instances 
for app %s: %v", req.AppName, err)
+       }
+       for _, inst := range instances {
+               if inst.Spec == nil || inst.Spec.Ip == "" {
+                       continue
+               }
+               rtResources, err := 
manager.ListByIndexes[*meshresource.RuntimeInstanceResource](
+                       ctx.ResourceManager(),
+                       meshresource.RuntimeInstanceKind,
+                       []index.IndexCondition{
+                               {IndexName: index.ByRuntimeInstanceIPIndex, 
Value: inst.Spec.Ip, Operator: index.Equals},
+                       },
+               )
+               if err != nil {
+                       continue
+               }
+               for _, rt := range rtResources {
+                       if rt.Spec != nil && rt.Spec.Name != "" {
+                               names = append(names, rt.Spec.Name)
+                       }
+               }
+       }
+
+       return names
+}
+
+// filterEventsByNames returns events whose InvolvedObjName matches any of the
+// given names, supporting both exact match and prefix match.
+// Prefix candidates end with "/" (registry: appName/...) or "-" (K8s pod: 
appName-hash-hash).
+func filterEventsByNames(resources []*meshresource.LifecycleEventResource, 
matchNames []string) []*meshresource.LifecycleEventResource {
+       filtered := make([]*meshresource.LifecycleEventResource, 0)
+       for _, r := range resources {
+               if r.Spec == nil {
+                       continue
+               }
+               n := r.Spec.InvolvedObjName
+               for _, candidate := range matchNames {
+                       if strings.HasSuffix(candidate, "/") || 
strings.HasSuffix(candidate, "-") {
+                               if strings.HasPrefix(n, candidate) {
+                                       filtered = append(filtered, r)
+                                       break
+                               }
+                       } else {
+                               if n == candidate {
+                                       filtered = append(filtered, r)
+                                       break
+                               }
+                       }
+               }
+       }
+       return filtered
+}
+
+// ListInstanceEvents returns a paginated event timeline for a specific 
instance.
+// K8s events use InvolvedObjName = podName; registry events use appName/ip.
+// The frontend sends instanceName = {appName}{IP}:{port}, which doesn't match
+// either format. So we look up the RuntimeInstance by IP to get the pod name,
+// then match events against both the pod name and the IP-based encoding.
+func ListInstanceEvents(ctx consolectx.Context, req *model.EventQueryReq) 
(*model.EventListResp, error) {
+       matchNames := resolveInstanceEventNames(ctx, req)
+
+       allConditions := []index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: 
index.Equals},
+       }
+       resources, err := 
manager.ListByIndexes[*meshresource.LifecycleEventResource](
+               ctx.ResourceManager(),
+               meshresource.LifecycleEventKind,
+               allConditions,
+       )
+       if err != nil {
+               return nil, err
+       }
+
+       filtered := filterEventsByNames(resources, matchNames)
+
+       offset := req.PageOffset
+       if offset < 0 {
+               offset = 0
+       }
+       if offset > len(filtered) {
+               offset = len(filtered)
+       }
+       pageSize := req.PageSize
+       if pageSize <= 0 {
+               pageSize = defaultPageSize
+       }
+       end := offset + pageSize
+       if end > len(filtered) {
+               end = len(filtered)
+       }
+       paged := filtered[offset:end]
+
+       return 
toEventListResp(&coremodel.PageData[*meshresource.LifecycleEventResource]{
+               Pagination: coremodel.Pagination{
+                       Total:      len(filtered),
+                       PageOffset: req.PageOffset,
+                       PageSize:   req.PageSize,
+               },
+               Data: paged,
+       }), nil
+}
+
+// resolveInstanceEventNames returns the set of InvolvedObjName values that
+// could match this instance's events. For K8s events it's the pod name; for
+// registry events it's appName/ip.
+func resolveInstanceEventNames(ctx consolectx.Context, req 
*model.EventQueryReq) []string {
+       names := make([]string, 0, 3)
+
+       // Always include the raw instanceName and IP as candidates (covers 
registry
+       // events and the case where the instance name IS the pod name).
+       if req.InstanceName != "" {
+               names = append(names, req.InstanceName)
+       }
+       if req.InstanceIP != "" {
+               names = append(names, req.InstanceIP)
+               // Registry event format: appName/ip
+               if req.AppName != "" {
+                       names = append(names, req.AppName+"/"+req.InstanceIP)
+               }
+       }
+
+       // Resolve pod name from RuntimeInstance by IP.
+       if req.InstanceIP != "" {
+               rtResources, err := 
manager.ListByIndexes[*meshresource.RuntimeInstanceResource](
+                       ctx.ResourceManager(),
+                       meshresource.RuntimeInstanceKind,
+                       []index.IndexCondition{
+                               {IndexName: index.ByRuntimeInstanceIPIndex, 
Value: req.InstanceIP, Operator: index.Equals},
+                       },
+               )
+               if err != nil {
+                       logger.Warnf("resolveInstanceEventNames: failed to list 
RuntimeInstance by IP %s: %v", req.InstanceIP, err)
+               } else {
+                       for _, rt := range rtResources {
+                               if rt.Spec != nil && rt.Spec.Name != "" {
+                                       names = append(names, rt.Spec.Name)
+                               }
+                       }
+               }
+       }
+
+       return names
+}
+
+// ListServiceEvents returns a paginated event timeline for a Dubbo service.
+// Covers registry-side config and metadata events. K8s has no native
+// "service" concept, so K8s-sourced events naturally will not appear here.
+func ListServiceEvents(ctx consolectx.Context, req *model.EventQueryReq) 
(*model.EventListResp, error) {
+       conditions := []index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: 
index.Equals},
+       }
+
+       if req.ServiceName != "" && req.AppName != "" {
+               conditions = append(conditions, index.IndexCondition{
+                       IndexName: index.ByLifecycleEventInvolvedObjName,
+                       Value:     req.AppName + "/" + req.ServiceName,
+                       Operator:  index.HasPrefix,
+               })
+       }
+
+       pageData, err := 
manager.PageListByIndexes[*meshresource.LifecycleEventResource](
+               ctx.ResourceManager(),
+               meshresource.LifecycleEventKind,
+               conditions,
+               req.PageReq,
+       )
+       if err != nil {
+               return nil, err
+       }
+
+       return toEventListResp(pageData), nil
+}
+
+func toEventListResp(pageData 
*coremodel.PageData[*meshresource.LifecycleEventResource]) *model.EventListResp 
{
+       items := pageData.Data
+       list := make([]*model.EventItem, 0, len(items))
+       for _, eventRes := range items {
+               if eventRes.Spec == nil {
+                       continue
+               }
+
+               eventType := "normal"
+               if strings.EqualFold(eventRes.Spec.Type, "Warning") {
+                       eventType = "warning"
+               }
+
+               source := eventRes.Spec.SourceComponent
+               if source == "" {
+                       source = eventRes.Spec.EventSource
+               }
+
+               list = append(list, &model.EventItem{
+                       Time:    eventRes.Spec.LastTimestamp,
+                       Type:    eventType,
+                       Message: eventRes.Spec.Message,
+                       Source:  source,
+               })
+       }
+
+       return &model.EventListResp{
+               List:  list,
+               Total: pageData.Total,
+       }
+}
diff --git a/pkg/core/discovery/component.go b/pkg/core/discovery/component.go
index 0911eab5..09b2004d 100644
--- a/pkg/core/discovery/component.go
+++ b/pkg/core/discovery/component.go
@@ -285,5 +285,13 @@ func (d *discoveryComponent) initSubscribes(storeRouter 
store.Router, emitter ev
                zkConfigSub := subscriber.NewZKConfigEventSubscriber(emitter, 
storeRouter)
                d.subscribers = append(d.subscribers, zkMetadataSub, 
zkConfigSub)
        }
+
+       // LifecycleEventSubscriber processes all LifecycleEvent resources on 
the EventBus,
+       // enriching K8s-sourced events and writing them to the store.
+       if engineConfig != nil && engineConfig.Type == engine.Kubernetes {
+               k8sEventSub := 
subscriber.NewLifecycleEventSubscriber(storeRouter, engineConfig)
+               d.subscribers = append(d.subscribers, k8sEventSub)
+       }
+
        return nil
 }
diff --git a/pkg/core/discovery/subscriber/lifecycle_event.go 
b/pkg/core/discovery/subscriber/lifecycle_event.go
new file mode 100644
index 00000000..d923dfb0
--- /dev/null
+++ b/pkg/core/discovery/subscriber/lifecycle_event.go
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package subscriber
+
+import (
+       "fmt"
+       "time"
+
+       "k8s.io/client-go/tools/cache"
+
+       "github.com/apache/dubbo-admin/pkg/common/constants"
+       enginecfg "github.com/apache/dubbo-admin/pkg/config/engine"
+       "github.com/apache/dubbo-admin/pkg/core/events"
+       "github.com/apache/dubbo-admin/pkg/core/logger"
+       meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
+       coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
+       "github.com/apache/dubbo-admin/pkg/core/store"
+       "github.com/apache/dubbo-admin/pkg/core/store/index"
+)
+
+// LifecycleEventSubscriber processes LifecycleEvent resources on the EventBus.
+// It enriches K8s-sourced events with Dubbo application identification
+// and discards events from non-Dubbo Pods before they reach the store.
+type LifecycleEventSubscriber struct {
+       storeRouter store.Router
+       engineCfg   *enginecfg.Config
+}
+
+func NewLifecycleEventSubscriber(storeRouter store.Router, engineCfg 
*enginecfg.Config) *LifecycleEventSubscriber {
+       return &LifecycleEventSubscriber{
+               storeRouter: storeRouter,
+               engineCfg:   engineCfg,
+       }
+}
+
+func (k *LifecycleEventSubscriber) Name() string {
+       return "Discovery-" + k.ResourceKind().ToString()
+}
+
+func (k *LifecycleEventSubscriber) ResourceKind() coremodel.ResourceKind {
+       return meshresource.LifecycleEventKind
+}
+
+func (k *LifecycleEventSubscriber) AsyncEnabled() bool {
+       return true
+}
+
+func (k *LifecycleEventSubscriber) ProcessEvent(event events.Event) error {
+       switch event.Type() {
+       case cache.Deleted:
+               return k.processDelete(event)
+       default:
+               return k.processUpsert(event)
+       }
+}
+
+func (k *LifecycleEventSubscriber) processUpsert(event events.Event) error {
+       newObj, ok := event.NewObj().(*meshresource.LifecycleEventResource)
+       if !ok || newObj == nil || newObj.Spec == nil {
+               logger.Debugf("LifecycleEventSubscriber: event has no valid 
LifecycleEventResource, skipping")
+               return nil
+       }
+
+       // REGISTRY events: already enriched by registry subscriber, write 
directly.
+       if newObj.Spec.EventSource == "REGISTRY" {
+               return k.writeEvent(newObj)
+       }
+
+       // KUBERNETES events: filter by Dubbo app identification.
+       identifier := k.engineCfg.Properties.DubboAppIdentifier
+       if identifier == nil {
+               logger.Debugf("LifecycleEventSubscriber: DubboAppIdentifier not 
configured, skipping K8s event %s",
+                       newObj.Spec.InvolvedObjName)
+               return nil
+       }
+
+       if newObj.Spec.InvolvedObjKind != "Pod" {
+               logger.Debugf("LifecycleEventSubscriber: skipping non-Pod K8s 
event (kind=%s)", newObj.Spec.InvolvedObjKind)
+               return nil
+       }
+
+       // Align LifecycleEvent mesh with the RuntimeInstance mesh.
+       // K8sEventListerWatcher cannot know the Pod's Dubbo mesh at transform 
time,
+       // so we resolve it here by looking up the corresponding 
RuntimeInstance.
+       k.alignMeshFromRuntimeInstance(newObj)
+
+       return k.writeEvent(newObj)
+}
+
+// alignMeshFromRuntimeInstance resolves the correct Dubbo mesh for a 
LifecycleEvent
+// by looking up the corresponding RuntimeInstance via the Pod name.
+// RuntimeInstances are stored under their discovery mesh (e.g. "nacos2.5"),
+// not the engine mesh, so we use the name index to search across all meshes.
+func (k *LifecycleEventSubscriber) alignMeshFromRuntimeInstance(eventRes 
*meshresource.LifecycleEventResource) {
+       rtStore, err := 
k.storeRouter.ResourceKindRoute(meshresource.RuntimeInstanceKind)
+       if err != nil {
+               logger.Debugf("LifecycleEventSubscriber: cannot route to 
RuntimeInstance store: %v", err)
+               return
+       }
+
+       podName := eventRes.Spec.InvolvedObjName
+
+       rtResources, listErr := rtStore.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByRuntimeInstanceNameIndex, Value: podName, 
Operator: index.Equals},
+       })
+       if listErr != nil {
+               logger.Debugf("LifecycleEventSubscriber: failed to list 
RuntimeInstances by name %s: %v", podName, listErr)
+               return
+       }
+
+       for _, rtRes := range rtResources {
+               rtInstance, ok := rtRes.(*meshresource.RuntimeInstanceResource)
+               if !ok || rtInstance == nil {
+                       continue
+               }
+               resolvedMesh := rtInstance.ResourceMesh()
+               if resolvedMesh != "" && resolvedMesh != eventRes.Mesh {
+                       // Delete the old entry (keyed by old mesh) before 
changing mesh.
+                       eventStore, _ := 
k.storeRouter.ResourceKindRoute(meshresource.LifecycleEventKind)
+                       if eventStore != nil {
+                               _ = eventStore.Delete(eventRes)
+                       }
+                       eventRes.Mesh = resolvedMesh
+                       logger.Debugf("LifecycleEventSubscriber: aligned mesh 
from %q to %q for pod %s",
+                               k.engineCfg.ID, resolvedMesh, podName)
+                       return
+               }
+       }
+}
+
+func (k *LifecycleEventSubscriber) processDelete(event events.Event) error {
+       oldObj, ok := event.OldObj().(*meshresource.LifecycleEventResource)
+       if !ok || oldObj == nil {
+               return nil
+       }
+
+       eventStore, err := 
k.storeRouter.ResourceKindRoute(meshresource.LifecycleEventKind)
+       if err != nil {
+               logger.Errorf("LifecycleEventSubscriber: cannot route to 
LifecycleEvent store: %v", err)
+               return err
+       }
+
+       if err := eventStore.Delete(oldObj); err != nil {
+               logger.Errorf("LifecycleEventSubscriber: failed to delete 
LifecycleEvent %s: %v", oldObj.ResourceKey(), err)
+       }
+       return nil
+}
+
+func (k *LifecycleEventSubscriber) writeEvent(eventRes 
*meshresource.LifecycleEventResource) error {
+       eventStore, err := 
k.storeRouter.ResourceKindRoute(meshresource.LifecycleEventKind)
+       if err != nil {
+               logger.Errorf("LifecycleEventSubscriber: cannot route to 
LifecycleEvent store: %v", err)
+               return err
+       }
+
+       // Ensure K8s-sourced events have a sortable timestamp prefix in their 
key
+       // so that PageListByIndexes (which sorts keys alphabetically) returns
+       // events in chronological order.
+       var oldName string
+       if eventRes.Spec.EventSource == "KUBERNETES" {
+               oldName = eventRes.Name
+               k.prefixTimestampKey(eventRes)
+       }
+
+       if err := eventStore.Add(eventRes); err != nil {
+               logger.Errorf("LifecycleEventSubscriber: failed to add 
LifecycleEvent %s: %v", eventRes.ResourceKey(), err)
+               return err
+       }
+
+       // Delete the informer-written entry with the original key only after 
the
+       // timestamp-prefixed entry has been successfully added, so that a 
failed
+       // Add does not cause permanent event loss.
+       if oldName != "" {
+               oldRes := 
meshresource.NewLifecycleEventResourceWithAttributes(oldName, eventRes.Mesh)
+               _ = eventStore.Delete(oldRes)
+       }
+
+       logger.Infof("LifecycleEventSubscriber: processed event, source=%s, 
kind=%s, involved=%s",
+               eventRes.Spec.EventSource, eventRes.Spec.InvolvedObjKind, 
eventRes.Spec.InvolvedObjName)
+       return nil
+}
+
+// prefixTimestampKey prepends a descending nano-timestamp to the resource name
+// so that the store's alphabetical key sort produces chronological 
(most-recent-first)
+// ordering. Registry events already have this prefix from RecordRegistryEvent.
+// The caller (writeEvent) is responsible for deleting the original key after a
+// successful Add to avoid duplicate entries.
+func (k *LifecycleEventSubscriber) prefixTimestampKey(eventRes 
*meshresource.LifecycleEventResource) {
+       timestampNano := int64(0)
+       if eventRes.Spec.LastTimestamp != "" {
+               if t, err := time.Parse(constants.TimeFormatStr, 
eventRes.Spec.LastTimestamp); err == nil {
+                       timestampNano = t.UnixNano()
+               }
+       }
+       if timestampNano == 0 {
+               timestampNano = time.Now().UnixNano()
+       }
+       // Use descending order: more recent → smaller prefix → sorts first 
alphabetically.
+       // maxInt64 - timestampNano gives descending sort.
+       sortablePrefix := fmt.Sprintf("%019d", 
int64(^uint64(0)>>1)-timestampNano)
+       eventRes.Name = sortablePrefix + "-" + eventRes.Name
+}
diff --git a/pkg/core/discovery/subscriber/nacos_service.go 
b/pkg/core/discovery/subscriber/nacos_service.go
index 4d8495ce..000351b5 100644
--- a/pkg/core/discovery/subscriber/nacos_service.go
+++ b/pkg/core/discovery/subscriber/nacos_service.go
@@ -78,14 +78,14 @@ func (n *NacosServiceEventSubscriber) ProcessEvent(event 
events.Event) error {
                        logger.Errorf(errStr)
                        return bizerror.New(bizerror.EventError, errStr)
                }
-               processErr = n.processUpsert(newObj)
+               processErr = n.processUpsert(oldObj, newObj, event.Type())
        case cache.Deleted:
                if oldObj == nil {
                        errStr := "process nacos service delete event, but old 
obj is nil, skipped processing"
                        logger.Errorf(errStr)
                        return bizerror.New(bizerror.EventError, errStr)
                }
-               processErr = n.processDelete(oldObj)
+               processErr = n.processDelete(oldObj, event.Type())
        }
        if processErr != nil {
                logger.Errorf("process nacos service event failed, cause: %s, 
event: %s", processErr.Error(), event.String())
@@ -95,7 +95,11 @@ func (n *NacosServiceEventSubscriber) ProcessEvent(event 
events.Event) error {
        return nil
 }
 
-func (n *NacosServiceEventSubscriber) processUpsert(serviceRes 
*meshresource.NacosServiceResource) error {
+func (n *NacosServiceEventSubscriber) processUpsert(
+       oldServiceRes *meshresource.NacosServiceResource,
+       serviceRes *meshresource.NacosServiceResource,
+       eventType cache.DeltaType,
+) error {
        providerRe := 
regexp.MustCompile(`^providers:[\w.]+(?::[\w.]*:|::[\w.]*)?$`)
        consumerRe := 
regexp.MustCompile(`^consumers:[\w.]+(?::[\w.]*:|::[\w.]*)?$`)
        if providerRe.MatchString(serviceRes.Name) {
@@ -103,13 +107,17 @@ func (n *NacosServiceEventSubscriber) 
processUpsert(serviceRes *meshresource.Nac
                return nil
        }
        if consumerRe.MatchString(serviceRes.Name) {
-               return n.processConsumerMetadataUpsert(serviceRes)
+               return n.processConsumerMetadataUpsert(oldServiceRes, 
serviceRes, eventType)
        }
-       return n.processRPCInstanceUpsert(serviceRes)
+       return n.processRPCInstanceUpsert(oldServiceRes, serviceRes, eventType)
 
 }
 
-func (n *NacosServiceEventSubscriber) processConsumerMetadataUpsert(serviceRes 
*meshresource.NacosServiceResource) error {
+func (n *NacosServiceEventSubscriber) processConsumerMetadataUpsert(
+       oldServiceRes *meshresource.NacosServiceResource,
+       serviceRes *meshresource.NacosServiceResource,
+       eventType cache.DeltaType,
+) error {
        serviceName, err := parseServiceName(serviceRes.Name)
        if err != nil {
                return bizerror.Wrap(err, bizerror.UnknownError, "parse service 
name error, raw nacos service is"+serviceRes.String())
@@ -183,6 +191,7 @@ func (n *NacosServiceEventSubscriber) 
processConsumerMetadataUpsert(serviceRes *
                n.emitter.Send(events.NewResourceChangedEvent(cache.Updated, 
item, item))
        })
 
+       n.recordNacosConsumerEvents(oldServiceRes, serviceRes, eventType, 
serviceName)
        return nil
 }
 
@@ -201,7 +210,11 @@ func parseServiceName(s string) (string, error) {
        return parts[0], nil
 }
 
-func (n *NacosServiceEventSubscriber) processRPCInstanceUpsert(serviceRes 
*meshresource.NacosServiceResource) error {
+func (n *NacosServiceEventSubscriber) processRPCInstanceUpsert(
+       oldServiceRes *meshresource.NacosServiceResource,
+       serviceRes *meshresource.NacosServiceResource,
+       eventType cache.DeltaType,
+) error {
        convertFunc := func(i int, instance *meshproto.NacosInstance) 
maputil.Entry[string, *meshresource.RPCInstanceResource] {
 
                res := meshresource.ToRPCInstance(serviceRes.Mesh, 
serviceRes.Name, instance.Ip, instance.Port, instance.Metadata)
@@ -269,10 +282,11 @@ func (n *NacosServiceEventSubscriber) 
processRPCInstanceUpsert(serviceRes *meshr
        })
        logger.Debugf("process rpc instance upsert event, oldInstances: %s, 
newInstances: %s, offlineInstances: %s, addInstances: %s, updateInstances: %s",
                maputil.Keys(oldInstances), maputil.Keys(newInstances), 
maputil.Keys(offlineInstances), maputil.Keys(addInstances), updateInstances)
+       n.recordNacosInstanceEvents(oldServiceRes, serviceRes, eventType)
        return nil
 }
 
-func (n *NacosServiceEventSubscriber) processDelete(serviceRes 
*meshresource.NacosServiceResource) error {
+func (n *NacosServiceEventSubscriber) processDelete(serviceRes 
*meshresource.NacosServiceResource, eventType cache.DeltaType) error {
        providerRe := 
regexp.MustCompile(`^providers:[\w.]+(?::[\w.]*:|::[\w.]*)?$`)
        consumerRe := 
regexp.MustCompile(`^consumers:[\w.]+(?::[\w.]*:|::[\w.]*)?$`)
        if providerRe.MatchString(serviceRes.Name) {
@@ -280,20 +294,24 @@ func (n *NacosServiceEventSubscriber) 
processDelete(serviceRes *meshresource.Nac
                return nil
        }
        if consumerRe.MatchString(serviceRes.Name) {
-               return n.processServiceConsumerDelete(serviceRes)
+               return n.processServiceConsumerDelete(serviceRes, eventType)
        }
-       return n.processRPCInstanceDelete(serviceRes)
+       return n.processRPCInstanceDelete(serviceRes, eventType)
 }
 
-func (n *NacosServiceEventSubscriber) processServiceConsumerDelete(serviceRes 
*meshresource.NacosServiceResource) error {
+func (n *NacosServiceEventSubscriber) processServiceConsumerDelete(serviceRes 
*meshresource.NacosServiceResource, eventType cache.DeltaType) error {
        st, err := 
n.storeRouter.ResourceKindRoute(meshresource.ServiceConsumerMetadataKind)
        if err != nil {
                logger.Errorf("process service consumer delete event, but 
cannot route to service consumer metadata resource, cause: %v", err)
                return err
        }
+       serviceName, parseErr := parseServiceName(serviceRes.Name)
+       if parseErr != nil {
+               return bizerror.Wrap(parseErr, bizerror.UnknownError, "parse 
service name error, raw nacos service is"+serviceRes.String())
+       }
        resources, err := st.ListByIndexes([]index.IndexCondition{
                {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, 
Operator: index.Equals},
-               {IndexName: index.ByServiceConsumerServiceName, Value: 
serviceRes.Name, Operator: index.Equals},
+               {IndexName: index.ByServiceConsumerServiceName, Value: 
serviceName, Operator: index.Equals},
        })
        if err != nil {
                logger.Errorf("process service consumer delete event, but 
cannot list service consumer metadata resource of %s, cause: %v", 
serviceRes.Name, err)
@@ -306,10 +324,11 @@ func (n *NacosServiceEventSubscriber) 
processServiceConsumerDelete(serviceRes *m
                }
                n.emitter.Send(events.NewResourceChangedEvent(cache.Deleted, 
item, nil))
        })
+       n.recordNacosConsumerEvents(serviceRes, nil, eventType, serviceName)
        return nil
 }
 
-func (n *NacosServiceEventSubscriber) processRPCInstanceDelete(serviceRes 
*meshresource.NacosServiceResource) error {
+func (n *NacosServiceEventSubscriber) processRPCInstanceDelete(serviceRes 
*meshresource.NacosServiceResource, eventType cache.DeltaType) error {
        st, err := n.storeRouter.ResourceKindRoute(meshresource.RPCInstanceKind)
        if err != nil {
                logger.Errorf("process rpc instance delete event, but cannot 
route to rpc instance resource, cause: %v", err)
@@ -330,5 +349,157 @@ func (n *NacosServiceEventSubscriber) 
processRPCInstanceDelete(serviceRes *meshr
                }
                n.emitter.Send(events.NewResourceChangedEvent(cache.Deleted, 
item, nil))
        })
+       n.recordNacosInstanceEvents(serviceRes, nil, eventType)
        return nil
 }
+
+func (n *NacosServiceEventSubscriber) recordNacosInstanceEvents(
+       oldServiceRes *meshresource.NacosServiceResource,
+       newServiceRes *meshresource.NacosServiceResource,
+       eventType cache.DeltaType,
+) {
+       if eventType != cache.Added && eventType != cache.Updated && eventType 
!= cache.Deleted {
+               return
+       }
+
+       oldInstances := buildNacosRPCInstanceMap(oldServiceRes)
+       newInstances := buildNacosRPCInstanceMap(newServiceRes)
+
+       for key, item := range maputil.Minus(oldInstances, newInstances) {
+               RecordRegistryEvent(n.storeRouter, RegistryEventInput{
+                       Mesh:         item.Mesh,
+                       Source:       "Nacos",
+                       SourceType:   "nacos",
+                       Category:     "registry",
+                       Action:       "deregistered",
+                       Message:      fmt.Sprintf("Nacos instance deregistered: 
%s (%s:%d)", item.Spec.Name, item.Spec.Ip, item.Spec.Port),
+                       AppName:      item.Spec.AppName,
+                       InstanceName: item.Spec.Name,
+                       InstanceIP:   item.Spec.Ip,
+               })
+               delete(oldInstances, key)
+       }
+
+       for _, item := range maputil.Values(maputil.Minus(newInstances, 
oldInstances)) {
+               RecordRegistryEvent(n.storeRouter, RegistryEventInput{
+                       Mesh:         item.Mesh,
+                       Source:       "Nacos",
+                       SourceType:   "nacos",
+                       Category:     "registry",
+                       Action:       "registered",
+                       Message:      fmt.Sprintf("Nacos instance registered: 
%s (%s:%d)", item.Spec.Name, item.Spec.Ip, item.Spec.Port),
+                       AppName:      item.Spec.AppName,
+                       InstanceName: item.Spec.Name,
+                       InstanceIP:   item.Spec.Ip,
+               })
+       }
+
+       for key, newItem := range newInstances {
+               oldItem, exists := oldInstances[key]
+               if !exists || oldItem == nil || oldItem.Spec == nil || 
newItem.Spec == nil {
+                       continue
+               }
+               if reflect.DeepEqual(oldItem.Spec, newItem.Spec) {
+                       continue
+               }
+               RecordRegistryEvent(n.storeRouter, RegistryEventInput{
+                       Mesh:         newItem.Mesh,
+                       Source:       "Nacos",
+                       SourceType:   "nacos",
+                       Category:     "registry",
+                       Action:       "updated",
+                       Message:      fmt.Sprintf("Nacos instance metadata 
updated: %s (%s:%d)", newItem.Spec.Name, newItem.Spec.Ip, newItem.Spec.Port),
+                       AppName:      newItem.Spec.AppName,
+                       InstanceName: newItem.Spec.Name,
+                       InstanceIP:   newItem.Spec.Ip,
+               })
+       }
+}
+
+func (n *NacosServiceEventSubscriber) recordNacosConsumerEvents(
+       oldServiceRes *meshresource.NacosServiceResource,
+       newServiceRes *meshresource.NacosServiceResource,
+       eventType cache.DeltaType,
+       serviceName string,
+) {
+       if eventType != cache.Added && eventType != cache.Updated && eventType 
!= cache.Deleted {
+               return
+       }
+
+       oldConsumers := buildNacosConsumerMap(oldServiceRes)
+       newConsumers := buildNacosConsumerMap(newServiceRes)
+
+       for key, item := range maputil.Minus(oldConsumers, newConsumers) {
+               RecordRegistryEvent(n.storeRouter, RegistryEventInput{
+                       Mesh:        item.Mesh,
+                       Source:      "Nacos",
+                       SourceType:  "nacos",
+                       Category:    "metadata",
+                       Action:      "consumer-removed",
+                       Message:     fmt.Sprintf("Nacos consumer metadata 
removed: %s -> %s", item.Spec.ConsumerAppName, serviceName),
+                       AppName:     item.Spec.ConsumerAppName,
+                       ServiceName: serviceName,
+               })
+               delete(oldConsumers, key)
+       }
+
+       for _, item := range maputil.Values(maputil.Minus(newConsumers, 
oldConsumers)) {
+               RecordRegistryEvent(n.storeRouter, RegistryEventInput{
+                       Mesh:        item.Mesh,
+                       Source:      "Nacos",
+                       SourceType:  "nacos",
+                       Category:    "metadata",
+                       Action:      "consumer-added",
+                       Message:     fmt.Sprintf("Nacos consumer metadata 
added: %s -> %s", item.Spec.ConsumerAppName, serviceName),
+                       AppName:     item.Spec.ConsumerAppName,
+                       ServiceName: serviceName,
+               })
+       }
+
+       for key, newItem := range newConsumers {
+               oldItem, exists := oldConsumers[key]
+               if !exists || oldItem == nil || oldItem.Spec == nil || 
newItem.Spec == nil {
+                       continue
+               }
+               if reflect.DeepEqual(oldItem.Spec, newItem.Spec) {
+                       continue
+               }
+               RecordRegistryEvent(n.storeRouter, RegistryEventInput{
+                       Mesh:        newItem.Mesh,
+                       Source:      "Nacos",
+                       SourceType:  "nacos",
+                       Category:    "metadata",
+                       Action:      "consumer-updated",
+                       Message:     fmt.Sprintf("Nacos consumer metadata 
updated: %s -> %s", newItem.Spec.ConsumerAppName, serviceName),
+                       AppName:     newItem.Spec.ConsumerAppName,
+                       ServiceName: serviceName,
+               })
+       }
+}
+
+func buildNacosRPCInstanceMap(serviceRes *meshresource.NacosServiceResource) 
map[string]*meshresource.RPCInstanceResource {
+       result := make(map[string]*meshresource.RPCInstanceResource)
+       if serviceRes == nil || serviceRes.Spec == nil {
+               return result
+       }
+       for _, item := range serviceRes.Spec.Instances {
+               res := meshresource.ToRPCInstance(serviceRes.Mesh, 
serviceRes.Name, item.Ip, item.Port, item.Metadata)
+               result[res.ResourceKey()] = res
+       }
+       return result
+}
+
+func buildNacosConsumerMap(serviceRes *meshresource.NacosServiceResource) 
map[string]*meshresource.ServiceConsumerMetadataResource {
+       result := make(map[string]*meshresource.ServiceConsumerMetadataResource)
+       if serviceRes == nil || serviceRes.Spec == nil {
+               return result
+       }
+       for _, item := range serviceRes.Spec.Instances {
+               res := 
meshresource.ToServiceConsumerMetadataByMap(item.Metadata, serviceRes.Mesh)
+               if res == nil {
+                       continue
+               }
+               result[res.ResourceKey()] = res
+       }
+       return result
+}
diff --git a/pkg/core/discovery/subscriber/registry_event_adapter.go 
b/pkg/core/discovery/subscriber/registry_event_adapter.go
new file mode 100644
index 00000000..ddec9db5
--- /dev/null
+++ b/pkg/core/discovery/subscriber/registry_event_adapter.go
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package subscriber
+
+import (
+       "fmt"
+       "regexp"
+       "strings"
+       "time"
+
+       meshproto "github.com/apache/dubbo-admin/api/mesh/v1alpha1"
+       "github.com/apache/dubbo-admin/pkg/common/constants"
+       "github.com/apache/dubbo-admin/pkg/core/logger"
+       meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
+       "github.com/apache/dubbo-admin/pkg/core/store"
+)
+
+var registryEventNameSanitizer = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
+
+// RegistryEventInput carries the data needed to record a registry-side event
+// as a unified LifecycleEventResource.
+type RegistryEventInput struct {
+       Mesh         string
+       Source       string // human-readable source name (e.g. "Nacos", 
"Zookeeper")
+       SourceType   string // machine identifier (e.g. "nacos", "zookeeper")
+       Category     string // "registry" | "config" | "metadata"
+       Action       string // "registered" | "deregistered" | "updated" | 
"added" | "deleted"
+       Message      string
+       Type         string // "normal" | "warning"
+       AppName      string
+       InstanceName string
+       InstanceIP   string
+       ServiceName  string
+}
+
+// RecordRegistryEvent writes a registry-side lifecycle event as a unified
+// LifecycleEventResource. The InvolvedObjName encodes a hierarchical 
identifier
+// (appName/ip:port for instances, appName/serviceName for config/metadata)
+// so that Console API queries can use a single HasPrefix lookup.
+func RecordRegistryEvent(storeRouter store.Router, input RegistryEventInput) {
+       if input.Mesh == "" || input.Message == "" {
+               logger.Warnf("RecordRegistryEvent: dropping event due to empty 
Mesh or Message, source=%s, category=%s, action=%s, appName=%s",
+                       input.SourceType, input.Category, input.Action, 
input.AppName)
+               return
+       }
+       eventStore, err := 
storeRouter.ResourceKindRoute(meshresource.LifecycleEventKind)
+       if err != nil {
+               logger.Errorf("route LifecycleEvent store failed, cause: %v", 
err)
+               return
+       }
+
+       now := time.Now()
+
+       // Build InvolvedObjName with hierarchical encoding:
+       // - instance events:  {appName}/{ip}:{port}
+       // - config events:    {appName}/{serviceName}:{ruleType}
+       // - metadata events:  {appName}/{serviceName}
+       var involvedObjName string
+       switch {
+       case input.InstanceIP != "":
+               involvedObjName = input.AppName + "/" + input.InstanceIP
+       case input.ServiceName != "":
+               involvedObjName = input.AppName + "/" + input.ServiceName
+       default:
+               involvedObjName = input.AppName
+       }
+
+       nameSeed := strings.Join([]string{
+               input.SourceType,
+               input.Category,
+               input.Action,
+               input.AppName,
+               input.ServiceName,
+               input.InstanceName,
+               input.InstanceIP,
+       }, "-")
+       sanitized := registryEventNameSanitizer.ReplaceAllString(nameSeed, "-")
+       sanitized = strings.Trim(sanitized, "-")
+       if sanitized == "" {
+               sanitized = "event"
+       }
+       eventName := fmt.Sprintf("%d-%s", now.UnixNano(), sanitized)
+
+       eventType := "normal"
+       if strings.EqualFold(input.Type, "warning") {
+               eventType = "warning"
+       }
+
+       source := input.Source
+       if source == "" {
+               source = input.SourceType
+       }
+
+       res := meshresource.NewLifecycleEventResourceWithAttributes(eventName, 
input.Mesh)
+       res.Spec = &meshproto.LifecycleEvent{
+               InvolvedObjKind: input.Category,
+               InvolvedObjName: involvedObjName,
+               Reason:          input.Action,
+               Message:         input.Message,
+               Type:            eventType,
+               SourceComponent: input.SourceType,
+               SourceHost:      input.InstanceIP,
+               EventSource:     "REGISTRY",
+               LastTimestamp:   now.Format(constants.TimeFormatStr),
+               FirstTimestamp:  now.Format(constants.TimeFormatStr),
+       }
+
+       if err := eventStore.Add(res); err != nil {
+               logger.Errorf("record registry event failed, key: %s, cause: 
%v", res.ResourceKey(), err)
+       }
+}
diff --git a/pkg/core/discovery/subscriber/zk_config.go 
b/pkg/core/discovery/subscriber/zk_config.go
index f9e0eec1..f4173db4 100644
--- a/pkg/core/discovery/subscriber/zk_config.go
+++ b/pkg/core/discovery/subscriber/zk_config.go
@@ -174,6 +174,8 @@ func processConfigUpsert[T coremodel.Resource](
                        logger.Errorf("add rule %s to store failed, cause: %s", 
newRuleRes.ResourceKey(), err.Error())
                        return err
                }
+               recordConfigPlatformEvent(router, newRuleRes, "added")
+               emitter.Send(events.NewResourceChangedEvent(cache.Added, nil, 
newRuleRes))
                
emitter.Send(events.NewResourceChangedEventWithContext(cache.Added, nil, 
newRuleRes, map[string]string{
                        events.SourceRegistryContextKey: 
sourceRegistryZookeeper,
                }))
@@ -196,6 +198,8 @@ func processConfigUpsert[T coremodel.Resource](
        emitter.Send(events.NewResourceChangedEventWithContext(cache.Updated, 
oldMetadataRes, newRuleRes, map[string]string{
                events.SourceRegistryContextKey: sourceRegistryZookeeper,
        }))
+       recordConfigPlatformEvent(router, newRuleRes, "updated")
+       emitter.Send(events.NewResourceChangedEvent(cache.Updated, 
oldMetadataRes, newRuleRes))
        return nil
 }
 
@@ -228,8 +232,48 @@ func processConfigDelete[T coremodel.Resource](
                logger.Errorf("delete rule %s from store failed, cause: %s", 
resourceKey, err.Error())
                return err
        }
+       recordConfigPlatformEvent(router, oldRuleRes, "deleted")
+       emitter.Send(events.NewResourceChangedEvent(cache.Deleted, oldRuleRes, 
nil))
        emitter.Send(events.NewResourceChangedEventWithContext(cache.Deleted, 
oldRuleRes, nil, map[string]string{
                events.SourceRegistryContextKey: sourceRegistryZookeeper,
        }))
        return nil
 }
+
+func recordConfigPlatformEvent(router store.Router, res coremodel.Resource, 
action string) {
+       serviceName, category := extractRuleEventContext(res)
+       if serviceName == "" {
+               return
+       }
+       RecordRegistryEvent(router, RegistryEventInput{
+               Mesh:        res.ResourceMesh(),
+               Source:      "Zookeeper",
+               SourceType:  "zookeeper",
+               Category:    category,
+               Action:      action,
+               Message:     fmt.Sprintf("Zookeeper %s %s: %s", category, 
action, serviceName),
+               ServiceName: serviceName,
+       })
+}
+
+func extractRuleEventContext(res coremodel.Resource) (string, string) {
+       switch item := res.(type) {
+       case *meshresource.TagRouteResource:
+               if item.Spec == nil {
+                       return "", ""
+               }
+               return item.Spec.Key, "tag-route"
+       case *meshresource.ConditionRouteResource:
+               if item.Spec == nil {
+                       return "", ""
+               }
+               return item.Spec.Key, "condition-route"
+       case *meshresource.DynamicConfigResource:
+               if item.Spec == nil {
+                       return "", ""
+               }
+               return item.Spec.Key, "dynamic-config"
+       default:
+               return "", ""
+       }
+}
diff --git a/pkg/core/discovery/subscriber/zk_metadata.go 
b/pkg/core/discovery/subscriber/zk_metadata.go
index 4a744119..93c91d87 100644
--- a/pkg/core/discovery/subscriber/zk_metadata.go
+++ b/pkg/core/discovery/subscriber/zk_metadata.go
@@ -136,6 +136,7 @@ func processMetadataUpsert[T coremodel.Resource](
                        logger.Errorf("add metadata %s to store failed, cause: 
%s", newMetadataRes.ResourceKey(), err.Error())
                        return err
                }
+               recordMetadataPlatformEvent(router, newMetadataRes, "added")
                emitter.Send(events.NewResourceChangedEvent(cache.Added, nil, 
newMetadataRes))
                return nil
        }
@@ -153,6 +154,40 @@ func processMetadataUpsert[T coremodel.Resource](
                return 
bizerror.NewAssertionError(reflect.TypeOf(oldMetadataRes), oldRes)
        }
 
+       recordMetadataPlatformEvent(router, newMetadataRes, "updated")
        emitter.Send(events.NewResourceChangedEvent(cache.Updated, 
oldMetadataRes, newMetadataRes))
        return nil
 }
+
+func recordMetadataPlatformEvent(router store.Router, res coremodel.Resource, 
action string) {
+       switch item := res.(type) {
+       case *meshresource.ServiceProviderMetadataResource:
+               if item.Spec == nil {
+                       return
+               }
+               RecordRegistryEvent(router, RegistryEventInput{
+                       Mesh:        item.Mesh,
+                       Source:      "Zookeeper",
+                       SourceType:  "zookeeper",
+                       Category:    "metadata",
+                       Action:      action,
+                       Message:     fmt.Sprintf("Zookeeper provider metadata 
%s: %s -> %s", action, item.Spec.ProviderAppName, item.Spec.ServiceName),
+                       AppName:     item.Spec.ProviderAppName,
+                       ServiceName: item.Spec.ServiceName,
+               })
+       case *meshresource.ServiceConsumerMetadataResource:
+               if item.Spec == nil {
+                       return
+               }
+               RecordRegistryEvent(router, RegistryEventInput{
+                       Mesh:        item.Mesh,
+                       Source:      "Zookeeper",
+                       SourceType:  "zookeeper",
+                       Category:    "metadata",
+                       Action:      action,
+                       Message:     fmt.Sprintf("Zookeeper consumer metadata 
%s: %s -> %s", action, item.Spec.ConsumerAppName, item.Spec.ServiceName),
+                       AppName:     item.Spec.ConsumerAppName,
+                       ServiceName: item.Spec.ServiceName,
+               })
+       }
+}
diff --git a/pkg/core/events/component.go b/pkg/core/events/component.go
index 7592dbaa..f5fbf6fe 100644
--- a/pkg/core/events/component.go
+++ b/pkg/core/events/component.go
@@ -178,7 +178,7 @@ func (b *eventBus) Send(event Event) {
        }
        states, exists := b.subscriberDir[rk]
        if !exists {
-               logger.Infof("no subscriber for resource %s, skipped sending 
event%v", rk, event)
+               logger.Debugf("no subscriber for resource %s, skipped sending 
event%v", rk, event)
                return
        }
        for _, st := range states {
diff --git a/pkg/core/resource/apis/mesh/v1alpha1/lifecycle_event_types.go 
b/pkg/core/resource/apis/mesh/v1alpha1/lifecycle_event_types.go
new file mode 100644
index 00000000..6a8eba6a
--- /dev/null
+++ b/pkg/core/resource/apis/mesh/v1alpha1/lifecycle_event_types.go
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package v1alpha1
+
+import (
+       "encoding/json"
+
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       k8sruntime "k8s.io/apimachinery/pkg/runtime"
+
+       meshproto "github.com/apache/dubbo-admin/api/mesh/v1alpha1"
+       "github.com/apache/dubbo-admin/pkg/core/logger"
+       coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
+)
+
+const LifecycleEventKind coremodel.ResourceKind = "LifecycleEvent"
+
+func init() {
+       coremodel.RegisterResourceSchema(LifecycleEventKind, 
NewLifecycleEventResource, NewLifecycleEventResourceList)
+}
+
+type LifecycleEventResource struct {
+       metav1.TypeMeta `json:",inline"`
+
+       metav1.ObjectMeta `json:"metadata,omitempty"`
+
+       // Mesh is the name of the dubbo mesh this resource belongs to.
+       Mesh string `json:"mesh,omitempty"`
+
+       // Spec is the specification of the LifecycleEvent resource.
+       Spec *meshproto.LifecycleEvent `json:"spec,omitempty"`
+
+       // Status is the status of the LifecycleEvent resource.
+       Status LifecycleEventResourceStatus `json:"status,omitempty"`
+}
+
+type LifecycleEventResourceStatus struct{}
+
+func (r *LifecycleEventResource) ResourceKind() coremodel.ResourceKind {
+       return LifecycleEventKind
+}
+
+func (r *LifecycleEventResource) ResourceMesh() string {
+       return r.Mesh
+}
+
+func (r *LifecycleEventResource) ResourceKey() string {
+       return coremodel.BuildResourceKey(r.Mesh, r.Name)
+}
+
+func (r *LifecycleEventResource) ResourceMeta() metav1.ObjectMeta {
+       return r.ObjectMeta
+}
+
+func (r *LifecycleEventResource) ResourceSpec() coremodel.ResourceSpec {
+       return r.Spec
+}
+
+func (r *LifecycleEventResource) DeepCopyObject() k8sruntime.Object {
+       out := &LifecycleEventResource{
+               TypeMeta: r.TypeMeta,
+               Mesh:     r.Mesh,
+               Status:   r.Status,
+       }
+
+       r.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+
+       if r.Spec != nil {
+               out.Spec = r.Spec.Clone()
+       }
+
+       return out
+}
+
+func (r *LifecycleEventResource) String() string {
+       jsonStr, err := json.Marshal(r)
+       if err != nil {
+               logger.Errorf("failed to encode LifecycleEventResource: %s to 
json, err: %v", r.ResourceKey(), err)
+               return ""
+       }
+       return string(jsonStr)
+}
+
+func NewLifecycleEventResourceWithAttributes(name string, mesh string) 
*LifecycleEventResource {
+       return &LifecycleEventResource{
+               TypeMeta: metav1.TypeMeta{
+                       Kind:       string(LifecycleEventKind),
+                       APIVersion: "v1alpha1",
+               },
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:   name,
+                       Labels: map[string]string{},
+               },
+               Mesh: mesh,
+               Spec: &meshproto.LifecycleEvent{},
+       }
+}
+
+func NewLifecycleEventResource() coremodel.Resource {
+       return &LifecycleEventResource{
+               TypeMeta: metav1.TypeMeta{
+                       Kind:       string(LifecycleEventKind),
+                       APIVersion: "v1alpha1",
+               },
+               Spec: &meshproto.LifecycleEvent{},
+       }
+}
+
+type LifecycleEventResourceList struct {
+       metav1.TypeMeta `json:",inline"`
+       metav1.ListMeta `json:"metadata,omitempty"`
+       Items           []*LifecycleEventResource `json:"items"`
+}
+
+func (r *LifecycleEventResourceList) DeepCopyObject() k8sruntime.Object {
+       out := &LifecycleEventResourceList{
+               TypeMeta: r.TypeMeta,
+       }
+       r.ListMeta.DeepCopyInto(&out.ListMeta)
+
+       if len(r.Items) == 0 {
+               return out
+       }
+       out.Items = make([]*LifecycleEventResource, len(r.Items))
+       for i := range r.Items {
+               out.Items[i] = 
r.Items[i].DeepCopyObject().(*LifecycleEventResource)
+       }
+       return out
+}
+
+func NewLifecycleEventResourceList() coremodel.ResourceList {
+       return &LifecycleEventResourceList{
+               TypeMeta: metav1.TypeMeta{
+                       Kind:       string(LifecycleEventKind),
+                       APIVersion: "v1alpha1",
+               },
+               Items: make([]*LifecycleEventResource, 0),
+       }
+}
+
+func (r *LifecycleEventResourceList) SetItems(items []coremodel.Resource) {
+       r.Items = make([]*LifecycleEventResource, len(items))
+       for i := range items {
+               res, ok := items[i].(*LifecycleEventResource)
+               if !ok {
+                       logger.Errorf("unexpected resource type in 
LifecycleEventResourceList.SetItems: expected %T, got %T", 
(*LifecycleEventResource)(nil), items[i])
+                       continue
+               }
+               r.Items[i] = res
+       }
+}
diff --git a/pkg/core/store/index/lifecycle_event.go 
b/pkg/core/store/index/lifecycle_event.go
new file mode 100644
index 00000000..0fa44619
--- /dev/null
+++ b/pkg/core/store/index/lifecycle_event.go
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package index
+
+import (
+       "reflect"
+
+       "k8s.io/client-go/tools/cache"
+
+       "github.com/apache/dubbo-admin/pkg/common/bizerror"
+       meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
+)
+
+const (
+       ByLifecycleEventInvolvedObjKind = 
"idx_lifecycle_event_involved_obj_kind"
+       ByLifecycleEventInvolvedObjName = 
"idx_lifecycle_event_involved_obj_name"
+       ByLifecycleEventType            = "idx_lifecycle_event_type"
+       ByLifecycleEventSource          = "idx_lifecycle_event_source"
+)
+
+func init() {
+       RegisterIndexers(meshresource.LifecycleEventKind, 
map[string]cache.IndexFunc{
+               ByLifecycleEventInvolvedObjKind: 
byLifecycleEventInvolvedObjKind,
+               ByLifecycleEventInvolvedObjName: 
byLifecycleEventInvolvedObjName,
+               ByLifecycleEventType:            byLifecycleEventType,
+               ByLifecycleEventSource:          byLifecycleEventSource,
+       })
+}
+
+func byLifecycleEventInvolvedObjKind(obj interface{}) ([]string, error) {
+       event, ok := obj.(*meshresource.LifecycleEventResource)
+       if !ok {
+               return nil, 
bizerror.NewAssertionError(meshresource.LifecycleEventKind, 
reflect.TypeOf(obj).Name())
+       }
+       if event.Spec == nil || event.Spec.InvolvedObjKind == "" {
+               return []string{}, nil
+       }
+       return []string{event.Spec.InvolvedObjKind}, nil
+}
+
+func byLifecycleEventInvolvedObjName(obj interface{}) ([]string, error) {
+       event, ok := obj.(*meshresource.LifecycleEventResource)
+       if !ok {
+               return nil, 
bizerror.NewAssertionError(meshresource.LifecycleEventKind, 
reflect.TypeOf(obj).Name())
+       }
+       if event.Spec == nil || event.Spec.InvolvedObjName == "" {
+               return []string{}, nil
+       }
+       return []string{event.Spec.InvolvedObjName}, nil
+}
+
+func byLifecycleEventType(obj interface{}) ([]string, error) {
+       event, ok := obj.(*meshresource.LifecycleEventResource)
+       if !ok {
+               return nil, 
bizerror.NewAssertionError(meshresource.LifecycleEventKind, 
reflect.TypeOf(obj).Name())
+       }
+       if event.Spec == nil || event.Spec.Type == "" {
+               return []string{}, nil
+       }
+       return []string{event.Spec.Type}, nil
+}
+
+func byLifecycleEventSource(obj interface{}) ([]string, error) {
+       event, ok := obj.(*meshresource.LifecycleEventResource)
+       if !ok {
+               return nil, 
bizerror.NewAssertionError(meshresource.LifecycleEventKind, 
reflect.TypeOf(obj).Name())
+       }
+       if event.Spec == nil || event.Spec.EventSource == "" {
+               return []string{}, nil
+       }
+       return []string{event.Spec.EventSource}, nil
+}
diff --git a/pkg/core/store/index/runtime_instance.go 
b/pkg/core/store/index/runtime_instance.go
index 745e8114..408c076c 100644
--- a/pkg/core/store/index/runtime_instance.go
+++ b/pkg/core/store/index/runtime_instance.go
@@ -27,11 +27,15 @@ import (
        meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
 )
 
-const ByRuntimeInstanceIPIndex = "idx_rt_instance_ip"
+const (
+       ByRuntimeInstanceIPIndex   = "idx_rt_instance_ip"
+       ByRuntimeInstanceNameIndex = "idx_rt_instance_name"
+)
 
 func init() {
        RegisterIndexers(meshresource.RuntimeInstanceKind, 
map[string]cache.IndexFunc{
-               ByRuntimeInstanceIPIndex: byRuntimeInstanceIp,
+               ByRuntimeInstanceIPIndex:   byRuntimeInstanceIp,
+               ByRuntimeInstanceNameIndex: byRuntimeInstanceName,
        })
 }
 
@@ -45,3 +49,14 @@ func byRuntimeInstanceIp(obj interface{}) ([]string, error) {
        }
        return []string{rtInstance.Spec.Ip}, nil
 }
+
+func byRuntimeInstanceName(obj interface{}) ([]string, error) {
+       rtInstance, ok := obj.(*meshresource.RuntimeInstanceResource)
+       if !ok {
+               return nil, 
bizerror.NewAssertionError(meshresource.RuntimeInstanceKind, 
reflect.TypeOf(obj).Name())
+       }
+       if rtInstance.Spec == nil || rtInstance.Spec.Name == "" {
+               return []string{}, nil
+       }
+       return []string{rtInstance.Spec.Name}, nil
+}
diff --git a/pkg/engine/kubernetes/factory.go b/pkg/engine/kubernetes/factory.go
index f1fa7917..652f7e04 100644
--- a/pkg/engine/kubernetes/factory.go
+++ b/pkg/engine/kubernetes/factory.go
@@ -72,5 +72,12 @@ func (e *EngineFactory) NewListWatchers(cfg 
*enginecfg.Config) ([]controller.Res
                return nil, fmt.Errorf("failed to init PodListerWatcher in 
kubernetes engine, %w", err)
        }
        lwList = append(lwList, podListerWatcher)
+
+       eventListerWatcher, err := 
listerwatcher.NewK8sEventListWatcher(clientset, cfg)
+       if err != nil {
+               return nil, fmt.Errorf("failed to init K8sEventListWatcher in 
kubernetes engine, %w", err)
+       }
+       lwList = append(lwList, eventListerWatcher)
+
        return lwList, nil
 }
diff --git a/pkg/engine/kubernetes/listerwatcher/k8s_event.go 
b/pkg/engine/kubernetes/listerwatcher/k8s_event.go
new file mode 100644
index 00000000..fef91138
--- /dev/null
+++ b/pkg/engine/kubernetes/listerwatcher/k8s_event.go
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package listerwatcher
+
+import (
+       "reflect"
+
+       v1 "k8s.io/api/core/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/fields"
+       k8sruntime "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/apimachinery/pkg/watch"
+       "k8s.io/client-go/kubernetes"
+       "k8s.io/client-go/tools/cache"
+
+       meshproto "github.com/apache/dubbo-admin/api/mesh/v1alpha1"
+       "github.com/apache/dubbo-admin/pkg/common/bizerror"
+       "github.com/apache/dubbo-admin/pkg/common/constants"
+       enginecfg "github.com/apache/dubbo-admin/pkg/config/engine"
+       "github.com/apache/dubbo-admin/pkg/core/controller"
+       "github.com/apache/dubbo-admin/pkg/core/logger"
+       meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
+       coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
+)
+
+type K8sEventListerWatcher struct {
+       cfg *enginecfg.Config
+       lw  cache.ListerWatcher
+}
+
+var _ controller.ResourceListerWatcher = &K8sEventListerWatcher{}
+
+func NewK8sEventListWatcher(clientset *kubernetes.Clientset, cfg 
*enginecfg.Config) (*K8sEventListerWatcher, error) {
+       // Only watch events related to Pods to avoid collecting cluster-wide 
noise
+       // (Node events, Namespace events, etc.). The LifecycleEventSubscriber 
on the
+       // EventBus performs further filtering using DubboAppIdentifier.
+       lw := cache.NewListWatchFromClient(
+               clientset.CoreV1().RESTClient(),
+               "events",
+               metav1.NamespaceAll,
+               fields.ParseSelectorOrDie("involvedObject.kind=Pod"),
+       )
+       return &K8sEventListerWatcher{cfg: cfg, lw: lw}, nil
+}
+
+func (k *K8sEventListerWatcher) List(options metav1.ListOptions) 
(k8sruntime.Object, error) {
+       return k.lw.List(options)
+}
+
+func (k *K8sEventListerWatcher) Watch(options metav1.ListOptions) 
(watch.Interface, error) {
+       return k.lw.Watch(options)
+}
+
+func (k *K8sEventListerWatcher) ResourceKind() coremodel.ResourceKind {
+       return meshresource.LifecycleEventKind
+}
+
+func (k *K8sEventListerWatcher) TransformFunc() cache.TransformFunc {
+       return func(obj interface{}) (interface{}, error) {
+               k8sEvent, ok := obj.(*v1.Event)
+               if !ok {
+                       return nil, bizerror.NewAssertionError("v1.Event", 
reflect.TypeOf(obj).Name())
+               }
+
+               firstTs := ""
+               if !k8sEvent.FirstTimestamp.IsZero() {
+                       firstTs = 
k8sEvent.FirstTimestamp.Format(constants.TimeFormatStr)
+               }
+               lastTs := ""
+               if !k8sEvent.LastTimestamp.IsZero() {
+                       lastTs = 
k8sEvent.LastTimestamp.Format(constants.TimeFormatStr)
+               }
+
+               res := 
meshresource.NewLifecycleEventResourceWithAttributes(k8sEvent.Namespace+"/"+k8sEvent.Name,
 k.cfg.ID)
+               res.Spec = &meshproto.LifecycleEvent{
+                       Namespace:       k8sEvent.Namespace,
+                       Reason:          k8sEvent.Reason,
+                       Message:         k8sEvent.Message,
+                       Type:            k8sEvent.Type,
+                       InvolvedObjKind: k8sEvent.InvolvedObject.Kind,
+                       InvolvedObjName: k8sEvent.InvolvedObject.Name,
+                       SourceComponent: k8sEvent.Source.Component,
+                       SourceHost:      k8sEvent.Source.Host,
+                       FirstTimestamp:  firstTs,
+                       LastTimestamp:   lastTs,
+                       Count:           k8sEvent.Count,
+                       EventSource:     "KUBERNETES",
+               }
+               logger.Debugf("transformed k8s event %s/%s", 
k8sEvent.Namespace, k8sEvent.Name)
+               return res, nil
+       }
+}
diff --git a/ui-vue3/src/api/service/app.ts b/ui-vue3/src/api/service/app.ts
index c38f28ea..a463ba2d 100644
--- a/ui-vue3/src/api/service/app.ts
+++ b/ui-vue3/src/api/service/app.ts
@@ -72,7 +72,12 @@ export const getApplicationTraceDashboard = (params: any): 
Promise<any> => {
     params
   })
 }
-export const listApplicationEvent = (params: any): Promise<any> => {
+export const listApplicationEvent = (params: {
+  appName?: string
+  mesh?: string
+  pageOffset?: number
+  pageSize?: number
+}): Promise<any> => {
   return request({
     url: '/application/event',
     method: 'get',
diff --git a/ui-vue3/src/api/service/instance.ts 
b/ui-vue3/src/api/service/instance.ts
index c2d58ef6..90d540e3 100644
--- a/ui-vue3/src/api/service/instance.ts
+++ b/ui-vue3/src/api/service/instance.ts
@@ -25,12 +25,18 @@ export const searchInstances = (params: any): Promise<any> 
=> {
   })
 }
 
-export const getInstanceDetail = (params: any): Promise<any> => {
+export const getInstanceDetail = (
+  params: any,
+  options?: {
+    silentError?: boolean
+  }
+): Promise<any> => {
   return request({
     url: '/instance/detail',
     method: 'get',
-    params
-  })
+    params,
+    silentError: options?.silentError
+  } as any)
 }
 
 export const getInstanceMetricsDashboard = (params: any): Promise<any> => {
@@ -108,6 +114,21 @@ export const getInstanceTrafficSwitchAPI = (instanceIP: 
string, appName: string)
  * @param appName
  * @param trafficDisable
  */
+export const listInstanceEvent = (params: {
+  instanceName?: string
+  ip?: string
+  appName?: string
+  mesh?: string
+  pageOffset?: number
+  pageSize?: number
+}): Promise<any> => {
+  return request({
+    url: '/instance/event',
+    method: 'get',
+    params
+  })
+}
+
 export const updateInstanceTrafficSwitchAPI = (
   instanceIP: string,
   appName: string,
diff --git a/ui-vue3/src/api/service/service.ts 
b/ui-vue3/src/api/service/service.ts
index f26a4a2a..99d1c542 100644
--- a/ui-vue3/src/api/service/service.ts
+++ b/ui-vue3/src/api/service/service.ts
@@ -145,6 +145,19 @@ export const updateParamRouteAPI = (data: {
   })
 }
 
+export const listServiceEvent = (params: {
+  serviceName?: string
+  mesh?: string
+  pageOffset?: number
+  pageSize?: number
+}): Promise<any> => {
+  return request({
+    url: '/service/event',
+    method: 'get',
+    params
+  })
+}
+
 export const getServiceGraph = (serviceName: string): Promise<any> => {
   return request({
     url: '/service/graph',
diff --git a/ui-vue3/src/base/http/request.ts b/ui-vue3/src/base/http/request.ts
index bc5956ec..535a22d1 100644
--- a/ui-vue3/src/base/http/request.ts
+++ b/ui-vue3/src/base/http/request.ts
@@ -39,8 +39,8 @@ const isSilentErrorUrl = (url?: string): boolean => {
   return SILENT_ERROR_URLS.some((silentUrl) => url.includes(silentUrl))
 }
 
-const shouldShowErrorMessage = (url?: string): boolean => {
-  return !isSilentErrorUrl(url)
+const shouldSilenceError = (config?: { url?: string; silentError?: boolean }): 
boolean => {
+  return Boolean(config?.silentError) || isSilentErrorUrl(config?.url)
 }
 
 const service: AxiosInstance = axios.create({
@@ -86,10 +86,12 @@ response.use(
 
     // Show error toast message
     const errorMsg = `${response.data.code}:${response.data.message}`
-    if (shouldShowErrorMessage(response.config.url)) {
+    if (!shouldSilenceError(response.config as any)) {
       message.error(errorMsg)
     }
-    console.error(errorMsg)
+    if (!shouldSilenceError(response.config as any)) {
+      console.error(errorMsg)
+    }
     return Promise.reject(response.data)
   },
   (error) => {
@@ -124,16 +126,20 @@ response.use(
     }
     if (response?.data) {
       const errorMsg = `${response.data?.code}:${response.data?.message}`
-      if (shouldShowErrorMessage(error.config?.url)) {
+      if (!shouldSilenceError(error.config as any)) {
         message.error(errorMsg)
       }
-      console.error(errorMsg)
+      if (!shouldSilenceError(error.config as any)) {
+        console.error(errorMsg)
+      }
     } else {
       // Handle network or other errors
-      if (!isSilentErrorUrl(error.config?.url)) {
+      if (!shouldSilenceError(error.config as any)) {
         message.error('NetworkError:请求失败,请检查网络连接')
       }
-      console.error(error)
+      if (!shouldSilenceError(error.config as any)) {
+        console.error(error)
+      }
     }
     return Promise.reject(error.response?.data)
   }
diff --git a/ui-vue3/src/base/i18n/en.ts b/ui-vue3/src/base/i18n/en.ts
index ebdf3443..169aac1c 100644
--- a/ui-vue3/src/base/i18n/en.ts
+++ b/ui-vue3/src/base/i18n/en.ts
@@ -600,6 +600,7 @@ const words: I18nType = {
   distribution: 'Distribution',
   tracing: 'Tracing',
   sceneConfig: 'Scene Config',
+  eventExpiryHint: 'Expired events are not stored',
 
   provideService: 'Provide Service',
   dependentService: 'Dependent Service',
diff --git a/ui-vue3/src/base/i18n/zh.ts b/ui-vue3/src/base/i18n/zh.ts
index 776cf595..557a5401 100644
--- a/ui-vue3/src/base/i18n/zh.ts
+++ b/ui-vue3/src/base/i18n/zh.ts
@@ -589,6 +589,7 @@ const words: I18nType = {
   tracing: '链路追踪',
   sceneConfig: '场景配置',
   event: '事件',
+  eventExpiryHint: '过期事件不会存储',
 
   provideService: '提供服务',
   dependentService: '依赖服务',
diff --git a/ui-vue3/src/components/EventTimeline.vue 
b/ui-vue3/src/components/EventTimeline.vue
new file mode 100644
index 00000000..4e1d15a8
--- /dev/null
+++ b/ui-vue3/src/components/EventTimeline.vue
@@ -0,0 +1,208 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+-->
+<template>
+  <div class="event-timeline-container">
+    <a-spin :spinning="loading">
+      <a-timeline mode="left" class="event-timeline">
+        <a-timeline-item
+          v-for="(item, index) in events"
+          :key="index"
+          :color="item.type === 'warning' ? '#faad14' : '#1890ff'"
+        >
+          <!-- Time label on the left -->
+          <template #label>
+            <span class="event-time">{{ item.time }}</span>
+          </template>
+
+          <!-- Custom dot -->
+          <template #dot>
+            <div class="event-dot" :class="item.type">
+              <CheckCircleOutlined v-if="item.type !== 'warning'" 
class="dot-icon normal" />
+              <WarningOutlined v-else class="dot-icon warning" />
+            </div>
+          </template>
+
+          <!-- Event card -->
+          <div class="event-card" :class="item.type">
+            <span class="event-message">{{ item.message }}</span>
+            <a-tag :color="item.type === 'warning' ? 'orange' : 'blue'" 
class="event-source-tag">
+              {{ item.source }}
+            </a-tag>
+          </div>
+        </a-timeline-item>
+
+        <!-- Bottom hint -->
+        <a-timeline-item>
+          <template #dot>
+            <ArrowDownOutlined class="bottom-arrow" />
+          </template>
+          <div class="bottom-hint">
+            <span>{{ $t('eventExpiryHint') || '过期事件不会存储' }}</span>
+            <a-spin v-if="loadingMore" size="small" class="load-more-spinner" 
/>
+          </div>
+        </a-timeline-item>
+      </a-timeline>
+      <div ref="loadMoreTriggerRef" class="load-more-trigger" />
+    </a-spin>
+
+    <a-empty v-if="!loading && events.length === 0" description="暂无事件" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onBeforeUnmount, onMounted, ref } from 'vue'
+import type { EventItem } from '@/types/api'
+import { CheckCircleOutlined, WarningOutlined, ArrowDownOutlined } from 
'@ant-design/icons-vue'
+
+const props = defineProps<{
+  events: EventItem[]
+  loading: boolean
+  loadingMore?: boolean
+  hasMore?: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'loadMore'): void
+}>()
+
+const loadMoreTriggerRef = ref<HTMLElement>()
+let observer: IntersectionObserver | null = null
+
+const tryLoadMore = () => {
+  if (!props.loading && !props.loadingMore && props.hasMore) {
+    emit('loadMore')
+  }
+}
+
+onMounted(() => {
+  if (!window.IntersectionObserver || !loadMoreTriggerRef.value) {
+    return
+  }
+  observer = new IntersectionObserver((entries) => {
+    if (entries.some((entry) => entry.isIntersecting)) {
+      tryLoadMore()
+    }
+  })
+  observer.observe(loadMoreTriggerRef.value)
+})
+
+onBeforeUnmount(() => {
+  observer?.disconnect()
+  observer = null
+})
+</script>
+
+<style lang="less" scoped>
+.event-timeline-container {
+  padding: 40px 20px 20px;
+
+  .event-timeline {
+    :deep(.ant-timeline-item-label) {
+      width: 180px;
+    }
+
+    :deep(.ant-timeline-item-tail) {
+      border-left: 2px solid #e8f4ff;
+    }
+  }
+
+  .event-time {
+    font-size: 13px;
+    color: #8c8c8c;
+    white-space: nowrap;
+    display: inline-block;
+    width: 160px;
+    text-align: right;
+  }
+
+  .event-dot {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .dot-icon {
+      font-size: 16px;
+
+      &.normal {
+        color: #1890ff;
+      }
+
+      &.warning {
+        color: #faad14;
+      }
+    }
+  }
+
+  .event-card {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 12px 16px;
+    border-radius: 6px;
+    border-left: 4px solid #1890ff;
+    background: #fff;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+    transition: box-shadow 0.2s;
+
+    &:hover {
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
+    }
+
+    &.warning {
+      border-left-color: #faad14;
+      background: #fff7e6;
+    }
+
+    .event-message {
+      flex: 1;
+      font-size: 14px;
+      color: #262626;
+      margin-right: 12px;
+      line-height: 1.5;
+    }
+
+    .event-source-tag {
+      flex-shrink: 0;
+      font-size: 12px;
+      border-radius: 4px;
+    }
+  }
+
+  .bottom-arrow {
+    font-size: 14px;
+    color: #bfbfbf;
+  }
+
+  .bottom-hint {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 13px;
+    color: #bfbfbf;
+    padding: 8px 0;
+  }
+
+  .load-more-spinner {
+    flex-shrink: 0;
+  }
+
+  .load-more-trigger {
+    width: 100%;
+    height: 1px;
+  }
+}
+</style>
diff --git a/ui-vue3/src/mocks/handlers/app.ts 
b/ui-vue3/src/mocks/handlers/app.ts
index 484ae9f3..b44bc051 100644
--- a/ui-vue3/src/mocks/handlers/app.ts
+++ b/ui-vue3/src/mocks/handlers/app.ts
@@ -146,7 +146,13 @@ export const appHandlers: HttpHandler[] = [
       time: '2024-03-31 12:00:00',
       type: 'deployment-controller'
     }))
-    return success({ list })
+    const eventList = list.map((item) => ({
+      time: item.time,
+      type: (Math.random() > 0.3 ? 'normal' : 'warning') as 'normal' | 
'warning',
+      message: item.desc,
+      source: item.type
+    }))
+    return success({ list: eventList, total: eventList.length })
   }),
 
   http.get(`${base}/application/service/form`, () =>
diff --git a/ui-vue3/src/mocks/handlers/instance.ts 
b/ui-vue3/src/mocks/handlers/instance.ts
index 1cffffd6..4643aaf0 100644
--- a/ui-vue3/src/mocks/handlers/instance.ts
+++ b/ui-vue3/src/mocks/handlers/instance.ts
@@ -91,5 +91,25 @@ export const instanceHandlers: HttpHandler[] = [
 
   http.get(`${base}/instance/config/trafficDisable`, () => success({ 
trafficDisable: false })),
 
-  http.put(`${base}/instance/config/trafficDisable`, () => success(null))
+  http.put(`${base}/instance/config/trafficDisable`, () => success(null)),
+
+  http.get(`${base}/instance/event`, () => {
+    const sources = ['deployment-controller', 'nacos', 
'replicaset-controller', 'scheduler']
+    const messages = [
+      'Scaled down replica set shop-detail-v1-5847b7cdfd to 1 from 2',
+      'Scaled up replica set shop-detail-v1-74fd98bc9d to 2 from 1',
+      'Successfully assigned shop-user/shop-detail-v1-5847b7cdfd to node 
hz-ali-30.33.0.1',
+      'Created container shop-detail',
+      'Started container shop-detail',
+      'Pulling image apache/org.apahce.dubbo.samples.shop-user:v1',
+      'Instance registered via Nacos: 45.7.37.227:20880'
+    ]
+    const list = Array.from({ length: 8 }, (_, i) => ({
+      time: `2024/2/17 ${String(20 - i).padStart(2, '0')}:04:38`,
+      type: (i === 0 ? 'warning' : 'normal') as 'normal' | 'warning',
+      message: messages[i % messages.length],
+      source: sources[i % sources.length]
+    }))
+    return success({ list, total: list.length })
+  })
 ]
diff --git a/ui-vue3/src/mocks/handlers/service.ts 
b/ui-vue3/src/mocks/handlers/service.ts
index 352fd3a2..6c7aec98 100644
--- a/ui-vue3/src/mocks/handlers/service.ts
+++ b/ui-vue3/src/mocks/handlers/service.ts
@@ -253,5 +253,23 @@ export const serviceHandlers: HttpHandler[] = [
 
   http.post(`${base}/service/generic/invoke`, () =>
     success({ elapsedMs: 12, rawResult: { id: '1001', name: 'Alice', age: 18 } 
})
-  )
+  ),
+
+  http.get(`${base}/service/event`, () => {
+    const sources = ['deployment-controller', 'nacos', 'zookeeper', 'kubelet']
+    const messages = [
+      'Service provider metadata updated: 
org.apache.dubbo.samples.UserService:v1',
+      'Service consumer registered: shop-user app',
+      'Instance registered via Nacos: 10.20.30.11:20880',
+      'Condition route rule applied to org.apache.dubbo.samples.UserService',
+      'Instance deregistered from Zookeeper: 10.20.30.12:20880'
+    ]
+    const list = Array.from({ length: 5 }, (_, i) => ({
+      time: `2024/2/17 ${String(20 - i).padStart(2, '0')}:04:38`,
+      type: (i === 4 ? 'warning' : 'normal') as 'normal' | 'warning',
+      message: messages[i],
+      source: sources[i % sources.length]
+    }))
+    return success({ list, total: list.length })
+  })
 ]
diff --git a/ui-vue3/src/router/defaultRoutes.ts 
b/ui-vue3/src/router/defaultRoutes.ts
index ecb2c9d4..ccb2146a 100644
--- a/ui-vue3/src/router/defaultRoutes.ts
+++ b/ui-vue3/src/router/defaultRoutes.ts
@@ -169,7 +169,6 @@ export const routes: Readonly<RouteRecordType[]> = [
                 component: () => 
import('../views/resources/applications/tabs/event.vue'),
                 meta: {
                   tab: true,
-                  hidden: true,
                   icon: 'material-symbols:date-range',
                   back: '/resources/applications/list'
                 }
@@ -243,7 +242,6 @@ export const routes: Readonly<RouteRecordType[]> = [
                 component: () => 
import('../views/resources/instances/tabs/event.vue'),
                 meta: {
                   tab: true,
-                  hidden: true,
                   icon: 'material-symbols:date-range',
                   back: '/resources/instances/list'
                 }
@@ -346,7 +344,6 @@ export const routes: Readonly<RouteRecordType[]> = [
                 component: () => 
import('../views/resources/services/tabs/event.vue'),
                 meta: {
                   tab: true,
-                  hidden: true,
                   back: '/resources/services/list',
                   icon: 'material-symbols:date-range'
                 }
diff --git a/ui-vue3/src/types/api.ts b/ui-vue3/src/types/api.ts
index 2d4ab101..e870f2bc 100644
--- a/ui-vue3/src/types/api.ts
+++ b/ui-vue3/src/types/api.ts
@@ -115,6 +115,13 @@ export interface ApplicationEventItem {
   type: string
 }
 
+export interface EventItem {
+  time: string
+  type: 'normal' | 'warning'
+  message: string
+  source: string
+}
+
 export interface InstanceSearchItem {
   ip: string
   name: string
diff --git a/ui-vue3/src/views/resources/applications/tabs/event.vue 
b/ui-vue3/src/views/resources/applications/tabs/event.vue
index a91a2d24..e8f1cbd6 100644
--- a/ui-vue3/src/views/resources/applications/tabs/event.vue
+++ b/ui-vue3/src/views/resources/applications/tabs/event.vue
@@ -14,143 +14,68 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
 -->
-<template>
-  <div class="__container_app_event">
-    <a-timeline mode="left">
-      <a-timeline-item v-for="(item, i) in events.list">
-        <div class="box">
-          <div class="label" :class="{ yellow: i === 0 }">
-            <div class="type"></div>
-            <div class="body">
-              <b class="title">{{ item.type }}</b>
-              <p>{{ item.desc }}</p>
-            </div>
-          </div>
-          <span class="time">
-            {{ item.time }}
-          </span>
-        </div>
-        <template v-if="i === 0" #dot>
-          <clock-circle-outlined style="font-size: 16px; color: red" />
-        </template>
 
-        <!--        <a-card>-->
-        <!--          <a-row>-->
-        <!--            <a-col :span="4">-->
-        <!--&lt;!&ndash;             <div class="box">&ndash;&gt;-->
-        <!--&lt;!&ndash;               <div class="type"></div>&ndash;&gt;-->
-        <!--&lt;!&ndash;               <div class="body"></div>&ndash;&gt;-->
-        <!--&lt;!&ndash;             </div>&ndash;&gt;-->
-        <!--            </a-col>-->
-        <!--            <a-col :span="10">{{item.desc}}</a-col>-->
-        <!--          </a-row>-->
-        <!--        </a-card>-->
-      </a-timeline-item>
-    </a-timeline>
-  </div>
+<template>
+  <EventTimeline
+    :events="eventList"
+    :loading="loading"
+    :loadingMore="loadingMore"
+    :hasMore="hasMore"
+    @loadMore="loadEvents()"
+  />
 </template>
 
-<script setup lang="ts">
-import { onMounted, reactive } from 'vue'
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { useMeshStore } from '@/stores/mesh'
 import { listApplicationEvent } from '@/api/service/app'
-import { ClockCircleOutlined } from '@ant-design/icons-vue'
-import { PRIMARY_COLOR } from '@/base/constants'
-
-let __ = PRIMARY_COLOR
-let events: any = reactive({ list: [] })
-onMounted(async () => {
-  let eventsRes = await listApplicationEvent({})
-  events.list = eventsRes.data.list
-  console.log(events)
-})
-</script>
-<style lang="less" scoped>
-.__container_app_event {
-  :deep(.ant-timeline-item-label) {
-    width: 200px;
-  }
+import EventTimeline from '@/components/EventTimeline.vue'
+import type { EventItem } from '@/types/api'
 
-  background: #fafafa;
-  border-radius: 10px;
-  padding: 80px 300px 20px;
+const route = useRoute()
+const meshStore = useMeshStore()
+const defaultPageSize = 20
 
-  .box {
-    position: relative;
-    height: 100px;
-    margin-bottom: 20px;
-    //top:-38px;
+const eventList = ref<EventItem[]>([])
+const loading = ref(false)
+const loadingMore = ref(false)
+const hasMore = ref(false)
+const pageOffset = ref(0)
 
-    .label {
-      position: absolute;
-      height: 100px;
-      top: -40px;
-
-      &.yellow {
-        .type {
-          border-right-color: #f8d347;
-        }
-        .body {
-          background: #f8d347;
-        }
-      }
-      &.red {
-        .type {
-          border-right-color: #eb4325;
-        }
-        .body {
-          background: #eb4325;
-        }
-      }
-      &.blue {
-        .type {
-          border-right-color: #3d89f6;
-        }
-        .body {
-          background: #3d89f6;
-        }
-      }
-      &.green {
-        .type {
-          border-right-color: #9cac35;
-        }
-        .body {
-          background: #9cac35;
-        }
-      }
-      .type {
-        position: absolute;
-        width: 50px;
-        height: 50px;
-        border-style: solid;
-        border-color: transparent;
-        border-width: 50px 26px 50px 0px;
-        border-right-color: v-bind('PRIMARY_COLOR');
-        display: inline;
-        border-radius: 4px;
-      }
-      .body {
-        position: absolute;
-        left: 49px;
-        width: 50vw;
-        border-radius: 3px 5px 5px 3px;
-        height: 100%;
-        display: inline;
-        color: white;
-        padding-left: 20px;
-        background: v-bind('PRIMARY_COLOR');
-        box-shadow: 8px 5px 10px #9f9c9c;
-
-        .title {
-          font-size: 30px;
-          line-height: 40px;
-        }
-      }
-    }
-    .time {
-      position: absolute;
-      left: -200px;
-      //top: 38px;
-    }
+const loadEvents = async (reset = false) => {
+  if (reset) {
+    loading.value = true
+  } else if (loading.value || loadingMore.value || !hasMore.value) {
+    return
+  } else {
+    loadingMore.value = true
+  }
+  try {
+    const appName = (route.params.pathId as string) || ''
+    const mesh = meshStore.mesh || 'default'
+    const currentOffset = reset ? 0 : pageOffset.value
+    const res = await listApplicationEvent({
+      appName,
+      mesh,
+      pageOffset: currentOffset,
+      pageSize: defaultPageSize
+    })
+    const list = res?.data?.list || []
+    const total = res?.data?.total || 0
+    eventList.value = reset ? list : [...eventList.value, ...list]
+    pageOffset.value = currentOffset + list.length
+    hasMore.value = pageOffset.value < total && list.length > 0
+  } finally {
+    loading.value = false
+    loadingMore.value = false
   }
 }
-</style>
+
+onMounted(async () => {
+  pageOffset.value = 0
+  eventList.value = []
+  hasMore.value = true
+  await loadEvents(true)
+})
+</script>
diff --git 
a/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue 
b/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue
index cf21a233..a58a8514 100644
--- a/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue
+++ b/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue
@@ -51,10 +51,15 @@ const fetchInstanceLifecycleState = async () => {
   }
 
   try {
-    const { data } = await getInstanceDetail({
-      instanceName,
-      instanceIP: route.params?.pathId
-    })
+    const { data } = await getInstanceDetail(
+      {
+        instanceName,
+        instanceIP: route.params?.pathId
+      },
+      {
+        silentError: true
+      }
+    )
     instanceLifecycleState.value = data?.lifecycleState || 'Unknown'
   } catch {
     instanceLifecycleState.value = 'Unknown'
diff --git a/ui-vue3/src/views/resources/instances/tabs/detail.vue 
b/ui-vue3/src/views/resources/instances/tabs/detail.vue
index 495a359b..235876a8 100644
--- a/ui-vue3/src/views/resources/instances/tabs/detail.vue
+++ b/ui-vue3/src/views/resources/instances/tabs/detail.vue
@@ -17,227 +17,230 @@
 
 <template>
   <div class="__container_instance_detail">
+    <a-empty v-if="instanceMissing" description="实例不存在或已下线" />
     <a-flex>
-      <a-card-grid>
-        <a-row :gutter="10">
-          <a-col :span="12">
-            <a-card class="_detail" style="height: 100%">
-              <a-descriptions class="description-column" :column="1">
-                <!-- registerState -->
-                <a-descriptions-item
-                  :label="$t('instanceDomain.registerState')"
-                  :labelStyle="{ fontWeight: 'bold' }"
-                >
-                  <a-typography-paragraph
-                    :type="instanceDetail?.registerState === 'Registered' ? 
'success' : 'danger'"
+      <template v-if="!instanceMissing">
+        <a-card-grid>
+          <a-row :gutter="10">
+            <a-col :span="12">
+              <a-card class="_detail" style="height: 100%">
+                <a-descriptions class="description-column" :column="1">
+                  <!-- registerState -->
+                  <a-descriptions-item
+                    :label="$t('instanceDomain.registerState')"
+                    :labelStyle="{ fontWeight: 'bold' }"
                   >
-                    {{ instanceDetail?.registerState }}
-                  </a-typography-paragraph>
-                </a-descriptions-item>
+                    <a-typography-paragraph
+                      :type="instanceDetail?.registerState === 'Registered' ? 
'success' : 'danger'"
+                    >
+                      {{ instanceDetail?.registerState }}
+                    </a-typography-paragraph>
+                  </a-descriptions-item>
 
-                <!-- Register Time -->
-                <a-descriptions-item
-                  :label="$t('instanceDomain.registerTime')"
-                  :labelStyle="{ fontWeight: 'bold' }"
-                >
-                  <a-typography-paragraph>
-                    {{ formattedDate(instanceDetail?.registerTime) }}
-                  </a-typography-paragraph>
-                </a-descriptions-item>
-              </a-descriptions>
-            </a-card>
-          </a-col>
+                  <!-- Register Time -->
+                  <a-descriptions-item
+                    :label="$t('instanceDomain.registerTime')"
+                    :labelStyle="{ fontWeight: 'bold' }"
+                  >
+                    <a-typography-paragraph>
+                      {{ formattedDate(instanceDetail?.registerTime) }}
+                    </a-typography-paragraph>
+                  </a-descriptions-item>
+                </a-descriptions>
+              </a-card>
+            </a-col>
 
-          <a-col :span="12">
-            <a-card class="_detail" style="height: 100%">
-              <a-descriptions class="description-column" :column="1">
-                <a-descriptions-item
-                  :label="$t('instanceDomain.deployState')"
-                  :labelStyle="{ fontWeight: 'bold' }"
-                >
-                  <a-tag :color="deployColor(instanceDetail?.deployState)">
-                    {{ instanceDetail?.deployState }}
-                  </a-tag>
-                </a-descriptions-item>
+            <a-col :span="12">
+              <a-card class="_detail" style="height: 100%">
+                <a-descriptions class="description-column" :column="1">
+                  <a-descriptions-item
+                    :label="$t('instanceDomain.deployState')"
+                    :labelStyle="{ fontWeight: 'bold' }"
+                  >
+                    <a-tag :color="deployColor(instanceDetail?.deployState)">
+                      {{ instanceDetail?.deployState }}
+                    </a-tag>
+                  </a-descriptions-item>
 
-                <!-- Start time -->
-                <a-descriptions-item
-                  :label="$t('instanceDomain.startTime_k8s')"
-                  :labelStyle="{ fontWeight: 'bold' }"
-                >
-                  <a-typography-paragraph>
-                    {{ formattedDate(instanceDetail?.startTime) }}
-                  </a-typography-paragraph>
-                </a-descriptions-item>
+                  <!-- Start time -->
+                  <a-descriptions-item
+                    :label="$t('instanceDomain.startTime_k8s')"
+                    :labelStyle="{ fontWeight: 'bold' }"
+                  >
+                    <a-typography-paragraph>
+                      {{ formattedDate(instanceDetail?.startTime) }}
+                    </a-typography-paragraph>
+                  </a-descriptions-item>
 
-                <!-- Ready time -->
-                <!-- <a-descriptions-item 
:label="$t('instanceDomain.readyTime_k8s')" :labelStyle="{ fontWeight: 'bold' 
}">
+                  <!-- Ready time -->
+                  <!-- <a-descriptions-item 
:label="$t('instanceDomain.readyTime_k8s')" :labelStyle="{ fontWeight: 'bold' 
}">
                   <a-typography-paragraph>
                     {{ formattedDate(instanceDetail?.readyTime) }}
                   </a-typography-paragraph>
                 </a-descriptions-item> -->
-              </a-descriptions>
-            </a-card>
-          </a-col>
-        </a-row>
-
-        <a-card style="margin-top: 10px" class="_detail">
-          <a-descriptions class="description-column" :column="1">
-            <!-- instanceIP -->
-            <a-descriptions-item
-              :label="$t('instanceDomain.instanceIP')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <p @click="copyIt(instanceDetail?.ip)" 
class="description-item-content with-card">
-                {{ instanceDetail?.ip }}
-                <CopyOutlined />
-              </p>
-            </a-descriptions-item>
-
-            <!-- deploy cluster -->
-            <a-descriptions-item
-              :label="$t('instanceDomain.deployCluster')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <a-typography-paragraph>
-                {{ instanceDetail?.deployCluster }}
-              </a-typography-paragraph>
-            </a-descriptions-item>
+                </a-descriptions>
+              </a-card>
+            </a-col>
+          </a-row>
 
-            <!-- Dubbo Port -->
-            <a-descriptions-item
-              :label="$t('instanceDomain.dubboPort')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <p
-                v-if="instanceDetail?.rpcPort"
-                @click="copyIt(instanceDetail?.rpcPort)"
-                class="description-item-content with-card"
+          <a-card style="margin-top: 10px" class="_detail">
+            <a-descriptions class="description-column" :column="1">
+              <!-- instanceIP -->
+              <a-descriptions-item
+                :label="$t('instanceDomain.instanceIP')"
+                :labelStyle="{ fontWeight: 'bold' }"
               >
-                {{ instanceDetail?.rpcPort }}
-                <CopyOutlined />
-              </p>
-            </a-descriptions-item>
+                <p @click="copyIt(instanceDetail?.ip)" 
class="description-item-content with-card">
+                  {{ instanceDetail?.ip }}
+                  <CopyOutlined />
+                </p>
+              </a-descriptions-item>
 
-            <!-- Register cluster -->
-            <a-descriptions-item
-              :label="$t('instanceDomain.registerCluster')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <a-space>
-                <a-typography-link v-for="cluster in 
instanceDetail?.registerClusters">
-                  {{ cluster }}
-                </a-typography-link>
-              </a-space>
-            </a-descriptions-item>
+              <!-- deploy cluster -->
+              <a-descriptions-item
+                :label="$t('instanceDomain.deployCluster')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
+                <a-typography-paragraph>
+                  {{ instanceDetail?.deployCluster }}
+                </a-typography-paragraph>
+              </a-descriptions-item>
 
-            <!-- whichApplication -->
-            <a-descriptions-item
-              :label="$t('instanceDomain.whichApplication')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <a-typography-link 
@click="checkApplication(instanceDetail?.appName)">
-                {{ instanceDetail?.appName }}
-              </a-typography-link>
-            </a-descriptions-item>
+              <!-- Dubbo Port -->
+              <a-descriptions-item
+                :label="$t('instanceDomain.dubboPort')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
+                <p
+                  v-if="instanceDetail?.rpcPort"
+                  @click="copyIt(instanceDetail?.rpcPort)"
+                  class="description-item-content with-card"
+                >
+                  {{ instanceDetail?.rpcPort }}
+                  <CopyOutlined />
+                </p>
+              </a-descriptions-item>
 
-            <!-- Node IP -->
-            <a-descriptions-item
-              :label="$t('instanceDomain.node')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <p
-                v-if="instanceDetail?.node"
-                @click="copyIt(instanceDetail?.node)"
-                class="description-item-content with-card"
+              <!-- Register cluster -->
+              <a-descriptions-item
+                :label="$t('instanceDomain.registerCluster')"
+                :labelStyle="{ fontWeight: 'bold' }"
               >
-                {{ instanceDetail?.node }}
-                <CopyOutlined />
-              </p>
-            </a-descriptions-item>
+                <a-space>
+                  <a-typography-link v-for="cluster in 
instanceDetail?.registerClusters">
+                    {{ cluster }}
+                  </a-typography-link>
+                </a-space>
+              </a-descriptions-item>
 
-            <!-- Owning workload(k8s) -->
-            <a-descriptions-item
-              :label="$t('instanceDomain.owningWorkload_k8s')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <a-typography-paragraph>
-                {{ instanceDetail?.workloadName }}
-              </a-typography-paragraph>
-            </a-descriptions-item>
+              <!-- whichApplication -->
+              <a-descriptions-item
+                :label="$t('instanceDomain.whichApplication')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
+                <a-typography-link 
@click="checkApplication(instanceDetail?.appName)">
+                  {{ instanceDetail?.appName }}
+                </a-typography-link>
+              </a-descriptions-item>
 
-            <!-- image -->
-            <a-descriptions-item
-              v-if="instanceDetail?.image"
-              :label="$t('instanceDomain.instanceImage_k8s')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <a-card class="description-item-card">
+              <!-- Node IP -->
+              <a-descriptions-item
+                :label="$t('instanceDomain.node')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
                 <p
-                  @click="copyIt(instanceDetail?.image)"
+                  v-if="instanceDetail?.node"
+                  @click="copyIt(instanceDetail?.node)"
                   class="description-item-content with-card"
                 >
-                  {{ instanceDetail?.image }}
+                  {{ instanceDetail?.node }}
                   <CopyOutlined />
                 </p>
-              </a-card>
-            </a-descriptions-item>
+              </a-descriptions-item>
 
-            <!-- instanceLabel -->
-            <a-descriptions-item
-              v-if="instanceDetail?.labels && 
Object.keys(instanceDetail?.labels).length > 0"
-              :label="$t('instanceDomain.instanceLabel')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <a-card class="description-item-card">
-                <a-tag v-for="(value, key) in instanceDetail?.labels">
-                  {{ key }} : {{ value }}
-                </a-tag>
-              </a-card>
-            </a-descriptions-item>
+              <!-- Owning workload(k8s) -->
+              <a-descriptions-item
+                :label="$t('instanceDomain.owningWorkload_k8s')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
+                <a-typography-paragraph>
+                  {{ instanceDetail?.workloadName }}
+                </a-typography-paragraph>
+              </a-descriptions-item>
 
-            <!-- health examination -->
-            <a-descriptions-item
-              v-if="instanceDetail?.probes"
-              :label="$t('instanceDomain.healthExamination_k8s')"
-              :labelStyle="{ fontWeight: 'bold' }"
-            >
-              <a-card class="description-item-card">
-                <p class="white_space">
-                  启动探针(StartupProbe):{{
-                    isProbeOpen(instanceDetail?.probes?.startupProbe.open)
-                  }}
-                  类型: {{ instanceDetail?.probes?.startupProbe.type }} 端口:{{
-                    instanceDetail?.probes?.startupProbe.port
-                  }}
-                </p>
-                <p class="white_space">
-                  就绪探针(ReadinessProbe):{{
-                    isProbeOpen(instanceDetail?.probes?.readinessProbe.open)
-                  }}
-                  类型: {{ instanceDetail?.probes?.readinessProbe.type }} 端口:{{
-                    instanceDetail?.probes?.readinessProbe.port
-                  }}
-                </p>
-                <p class="white_space">
-                  存活探针(LivenessProbe):{{
-                    isProbeOpen(instanceDetail?.probes?.livenessProbe.open)
-                  }}
-                  类型: {{ instanceDetail?.probes?.livenessProbe.type }} 端口:{{
-                    instanceDetail?.probes?.livenessProbe.port
-                  }}
-                </p>
-              </a-card>
-            </a-descriptions-item>
-          </a-descriptions>
-        </a-card>
-      </a-card-grid>
+              <!-- image -->
+              <a-descriptions-item
+                v-if="instanceDetail?.image"
+                :label="$t('instanceDomain.instanceImage_k8s')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
+                <a-card class="description-item-card">
+                  <p
+                    @click="copyIt(instanceDetail?.image)"
+                    class="description-item-content with-card"
+                  >
+                    {{ instanceDetail?.image }}
+                    <CopyOutlined />
+                  </p>
+                </a-card>
+              </a-descriptions-item>
+
+              <!-- instanceLabel -->
+              <a-descriptions-item
+                v-if="instanceDetail?.labels && 
Object.keys(instanceDetail?.labels).length > 0"
+                :label="$t('instanceDomain.instanceLabel')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
+                <a-card class="description-item-card">
+                  <a-tag v-for="(value, key) in instanceDetail?.labels">
+                    {{ key }} : {{ value }}
+                  </a-tag>
+                </a-card>
+              </a-descriptions-item>
+
+              <!-- health examination -->
+              <a-descriptions-item
+                v-if="instanceDetail?.probes"
+                :label="$t('instanceDomain.healthExamination_k8s')"
+                :labelStyle="{ fontWeight: 'bold' }"
+              >
+                <a-card class="description-item-card">
+                  <p class="white_space">
+                    启动探针(StartupProbe):{{
+                      isProbeOpen(instanceDetail?.probes?.startupProbe.open)
+                    }}
+                    类型: {{ instanceDetail?.probes?.startupProbe.type }} 端口:{{
+                      instanceDetail?.probes?.startupProbe.port
+                    }}
+                  </p>
+                  <p class="white_space">
+                    就绪探针(ReadinessProbe):{{
+                      isProbeOpen(instanceDetail?.probes?.readinessProbe.open)
+                    }}
+                    类型: {{ instanceDetail?.probes?.readinessProbe.type }} 端口:{{
+                      instanceDetail?.probes?.readinessProbe.port
+                    }}
+                  </p>
+                  <p class="white_space">
+                    存活探针(LivenessProbe):{{
+                      isProbeOpen(instanceDetail?.probes?.livenessProbe.open)
+                    }}
+                    类型: {{ instanceDetail?.probes?.livenessProbe.type }} 端口:{{
+                      instanceDetail?.probes?.livenessProbe.port
+                    }}
+                  </p>
+                </a-card>
+              </a-descriptions-item>
+            </a-descriptions>
+          </a-card>
+        </a-card-grid>
+      </template>
     </a-flex>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { type ComponentInternalInstance, getCurrentInstance, onMounted, 
reactive } from 'vue'
+import { type ComponentInternalInstance, getCurrentInstance, onMounted, 
reactive, ref } from 'vue'
 import { CopyOutlined } from '@ant-design/icons-vue'
 import useClipboard from 'vue-clipboard3'
 import { message } from 'ant-design-vue'
@@ -260,6 +263,7 @@ let PRIMARY_COLOR_20 = PRIMARY_COLOR_T('20')
 
 // instance detail information
 const instanceDetail = <any>reactive({})
+const instanceMissing = ref(false)
 
 onMounted(async () => {
   const { name, pathId } = route.params
@@ -267,8 +271,12 @@ onMounted(async () => {
     instanceName: name,
     instanceIP: pathId
   }
-  apiData.detail = await getInstanceDetail(params)
-  Object.assign(instanceDetail, apiData.detail.data)
+  try {
+    apiData.detail = await getInstanceDetail(params, { silentError: true })
+    Object.assign(instanceDetail, apiData.detail.data)
+  } catch {
+    instanceMissing.value = true
+  }
 })
 
 // Click on the application name to view the application
diff --git a/ui-vue3/src/views/resources/instances/tabs/event.vue 
b/ui-vue3/src/views/resources/instances/tabs/event.vue
index a3cf4d1f..3151194c 100644
--- a/ui-vue3/src/views/resources/instances/tabs/event.vue
+++ b/ui-vue3/src/views/resources/instances/tabs/event.vue
@@ -16,11 +16,68 @@
 -->
 
 <template>
-  <div>event todo</div>
+  <EventTimeline
+    :events="eventList"
+    :loading="loading"
+    :loadingMore="loadingMore"
+    :hasMore="hasMore"
+    @loadMore="loadEvents()"
+  />
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue'
-</script>
+import { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { useMeshStore } from '@/stores/mesh'
+import { listInstanceEvent } from '@/api/service/instance'
+import EventTimeline from '@/components/EventTimeline.vue'
+import type { EventItem } from '@/types/api'
+
+const route = useRoute()
+const meshStore = useMeshStore()
+const defaultPageSize = 20
+
+const eventList = ref<EventItem[]>([])
+const loading = ref(false)
+const loadingMore = ref(false)
+const hasMore = ref(false)
+const pageOffset = ref(0)
 
-<style lang="less" scoped></style>
+const loadEvents = async (reset = false) => {
+  if (reset) {
+    loading.value = true
+  } else if (loading.value || loadingMore.value || !hasMore.value) {
+    return
+  } else {
+    loadingMore.value = true
+  }
+  try {
+    const instanceName = (route.params.name as string) || ''
+    const ip = (route.params.pathId as string) || ''
+    const mesh = meshStore.mesh || 'default'
+    const currentOffset = reset ? 0 : pageOffset.value
+    const res = await listInstanceEvent({
+      instanceName,
+      ip,
+      mesh,
+      pageOffset: currentOffset,
+      pageSize: defaultPageSize
+    })
+    const list = res?.data?.list || []
+    const total = res?.data?.total || 0
+    eventList.value = reset ? list : [...eventList.value, ...list]
+    pageOffset.value = currentOffset + list.length
+    hasMore.value = pageOffset.value < total && list.length > 0
+  } finally {
+    loading.value = false
+    loadingMore.value = false
+  }
+}
+
+onMounted(async () => {
+  pageOffset.value = 0
+  eventList.value = []
+  hasMore.value = true
+  await loadEvents(true)
+})
+</script>
diff --git a/ui-vue3/src/views/resources/services/tabs/event.vue 
b/ui-vue3/src/views/resources/services/tabs/event.vue
index d2d41afa..a746529d 100644
--- a/ui-vue3/src/views/resources/services/tabs/event.vue
+++ b/ui-vue3/src/views/resources/services/tabs/event.vue
@@ -14,66 +14,68 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
 -->
+
 <template>
-  <div class="__container_services_tabs_event">
-    <a-card class="timeline-container">
-      <a-timeline class="timeline">
-        <a-timeline-item>
-          <template #dot><MinusCircleOutlined style="font-size: 18px" 
/></template>
-        </a-timeline-item>
-        <a-timeline-item v-for="(item, index) in eventData" :key="index">
-          <a-tag class="time" :color="PRIMARY_COLOR">{{ item.time }}</a-tag>
-          <span class="description">{{ item.description }}</span>
-        </a-timeline-item>
-        <a-timeline-item>
-          <template #dot></template>
-          <span>过期事件不会存储</span>
-        </a-timeline-item>
-      </a-timeline>
-    </a-card>
-  </div>
+  <EventTimeline
+    :events="eventList"
+    :loading="loading"
+    :loadingMore="loadingMore"
+    :hasMore="hasMore"
+    @loadMore="loadEvents()"
+  />
 </template>
 
-<script setup lang="ts">
-import { PRIMARY_COLOR } from '@/base/constants'
-import { MinusCircleOutlined } from '@ant-design/icons-vue'
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { useMeshStore } from '@/stores/mesh'
+import { listServiceEvent } from '@/api/service/service'
+import EventTimeline from '@/components/EventTimeline.vue'
+import type { EventItem } from '@/types/api'
 
-let __null = PRIMARY_COLOR
-const eventData = [
-  {
-    time: '2022-01-01',
-    description: 'description'
-  },
-  {
-    time: '2022-01-02',
-    description: 'description'
-  },
-  {
-    time: '2022-01-03',
-    description: 'description'
-  },
-  {
-    time: '2022-01-04',
-    description: 'description'
-  },
-  {
-    time: '2022-01-05',
-    description: 'description'
-  }
-]
-</script>
+const route = useRoute()
+const meshStore = useMeshStore()
+const defaultPageSize = 20
+
+const eventList = ref<EventItem[]>([])
+const loading = ref(false)
+const loadingMore = ref(false)
+const hasMore = ref(false)
+const pageOffset = ref(0)
 
-<style lang="less" scoped>
-.__container_services_tabs_event {
-  display: flex;
-  .timeline-container {
-    width: 100%;
-    .timeline {
-      margin-left: 30px;
-      .description {
-        font-size: 18px;
-      }
-    }
+const loadEvents = async (reset = false) => {
+  if (reset) {
+    loading.value = true
+  } else if (loading.value || loadingMore.value || !hasMore.value) {
+    return
+  } else {
+    loadingMore.value = true
+  }
+  try {
+    const serviceName = (route.params.pathId as string) || ''
+    const mesh = meshStore.mesh || 'default'
+    const currentOffset = reset ? 0 : pageOffset.value
+    const res = await listServiceEvent({
+      serviceName,
+      mesh,
+      pageOffset: currentOffset,
+      pageSize: defaultPageSize
+    })
+    const list = res?.data?.list || []
+    const total = res?.data?.total || 0
+    eventList.value = reset ? list : [...eventList.value, ...list]
+    pageOffset.value = currentOffset + list.length
+    hasMore.value = pageOffset.value < total && list.length > 0
+  } finally {
+    loading.value = false
+    loadingMore.value = false
   }
 }
-</style>
+
+onMounted(async () => {
+  pageOffset.value = 0
+  eventList.value = []
+  hasMore.value = true
+  await loadEvents(true)
+})
+</script>

Reply via email to