blob: 1bae199ea62f54d54bbcd0d9a895bfaa06ce5e58 [file]
/*
* 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,
}
}