diff --git a/backend/modules/observability/application/openapi.go b/backend/modules/observability/application/openapi.go index 72a28e9e2..04360b615 100644 --- a/backend/modules/observability/application/openapi.go +++ b/backend/modules/observability/application/openapi.go @@ -18,6 +18,7 @@ import ( "github.com/bytedance/sonic" "github.com/coze-dev/coze-loop/backend/kitex_gen/base" "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/collector" + "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/time_range" "github.com/coze-dev/coze-loop/backend/modules/observability/lib/otel" "github.com/coze-dev/coze-loop/backend/pkg/lang/ptr" coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" @@ -66,6 +67,7 @@ func NewOpenAPIApplication( traceConfig config.ITraceConfig, metrics metrics.ITraceMetrics, collector collector.ICollectorProvider, + timeRange time_range.ITimeRangeProvider, ) (IObservabilityOpenAPIApplication, error) { return &OpenAPIApplication{ traceService: traceService, @@ -77,6 +79,7 @@ func NewOpenAPIApplication( traceConfig: traceConfig, metrics: metrics, collector: collector, + timeRange: timeRange, }, nil } @@ -90,6 +93,7 @@ type OpenAPIApplication struct { traceConfig config.ITraceConfig metrics metrics.ITraceMetrics collector collector.ICollectorProvider + timeRange time_range.ITimeRangeProvider } func (o *OpenAPIApplication) IngestTraces(ctx context.Context, req *openapi.IngestTracesRequest) (*openapi.IngestTracesResponse, error) { @@ -558,17 +562,7 @@ func (o *OpenAPIApplication) validateSearchTraceOApiReq(ctx context.Context, req } else if req.Limit > MaxListSpansLimit || req.Limit < 0 { return errorx.NewByCode(obErrorx.CommercialCommonInvalidParamCodeCode, errorx.WithExtraMsg("invalid limit")) } - v := utils.DateValidator{ - Start: req.GetStartTime(), - End: req.GetEndTime(), - EarliestDays: 365, - } - newStartTime, newEndTime, err := v.CorrectDate() - if err != nil { - return err - } - req.SetStartTime(newStartTime) - req.SetEndTime(newEndTime) + return nil } @@ -578,14 +572,35 @@ func (o *OpenAPIApplication) buildSearchTraceOApiReq(ctx context.Context, req *o platformType = loop_span.PlatformCozeLoop } + startTime := req.GetStartTime() + endTime := req.GetEndTime() + + if startTime == 0 && endTime == 0 { + st, et := o.timeRange.GetTimeRange(ctx, strconv.FormatInt(req.WorkspaceID, 10), req.GetLogid(), req.GetTraceID(), 1000*60*60*24) + if st != nil && et != nil { + startTime = *st + endTime = *et + } + } + + v := utils.DateValidator{ + Start: startTime, + End: endTime, + EarliestDays: 365, + } + newStartTime, newEndTime, err := v.CorrectDate() + if err != nil { + return nil, err + } + ret := &service.SearchTraceOApiReq{ WorkspaceID: req.WorkspaceID, ThirdPartyWorkspaceID: o.workspace.GetThirdPartyQueryWorkSpaceID(ctx, req.WorkspaceID), Tenants: o.tenant.GetOAPIQueryTenants(ctx, platformType), TraceID: req.GetTraceID(), LogID: req.GetLogid(), - StartTime: req.GetStartTime(), - EndTime: req.GetEndTime(), + StartTime: newStartTime, + EndTime: newEndTime, Limit: req.GetLimit(), PlatformType: platformType, WithDetail: true, diff --git a/backend/modules/observability/application/openapi_test.go b/backend/modules/observability/application/openapi_test.go index cc9e6f359..ddcbf54ca 100755 --- a/backend/modules/observability/application/openapi_test.go +++ b/backend/modules/observability/application/openapi_test.go @@ -30,6 +30,7 @@ import ( rpcmocks "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/rpc/mocks" "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/tenant" tenantmocks "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/tenant/mocks" + time_rangemocks "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/time_range/mocks" "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/workspace" workspacemocks "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/workspace/mocks" "github.com/coze-dev/coze-loop/backend/modules/observability/domain/trace/entity" @@ -964,6 +965,7 @@ func TestNewOpenAPIApplication(t *testing.T) { traceConfigMock := configmocks.NewMockITraceConfig(ctrl) metricsMock := metricsmocks.NewMockITraceMetrics(ctrl) collectorMock := collectormocks.NewMockICollectorProvider(ctrl) + timeRangeMock := time_rangemocks.NewMockITimeRangeProvider(ctrl) rateLimiterFactoryMock.EXPECT().NewRateLimiter().Return(rateLimiterMock) @@ -977,6 +979,7 @@ func TestNewOpenAPIApplication(t *testing.T) { traceConfigMock, metricsMock, collectorMock, + timeRangeMock, ) assert.NoError(t, err) @@ -994,6 +997,7 @@ func TestNewOpenAPIApplication(t *testing.T) { assert.NotNil(t, openAPIApp.traceConfig) assert.NotNil(t, openAPIApp.metrics) assert.NotNil(t, openAPIApp.collector) + assert.NotNil(t, openAPIApp.timeRange) } // 补充IngestTraces的边界测试场景 @@ -2016,19 +2020,8 @@ func TestOpenAPIApplication_validateSearchTraceOApiReq(t *testing.T) { negativeLimit.Limit = -1 assert.Error(t, app.validateSearchTraceOApiReq(ctx, &negativeLimit)) - // invalid time range (zero values) - invalidTime := *validReq - invalidTime.StartTime = 0 - invalidTime.EndTime = 0 - assert.Error(t, app.validateSearchTraceOApiReq(ctx, &invalidTime)) - // valid request should pass assert.NoError(t, app.validateSearchTraceOApiReq(ctx, validReq)) - - // start time later than end time - invalidRange := *validReq - invalidRange.StartTime = now + 1000 - assert.Error(t, app.validateSearchTraceOApiReq(ctx, &invalidRange)) } func TestOpenAPIApplication_buildSearchTraceOApiReq(t *testing.T) { @@ -2808,3 +2801,57 @@ func TestUngzip(t *testing.T) { assert.Nil(t, result) }) } + +func TestOpenAPIApplication_buildSearchTraceOApiReq_TimeRangeFallback(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tenantMock := tenantmocks.NewMockITenantProvider(ctrl) + workspaceMock := workspacemocks.NewMockIWorkSpaceProvider(ctrl) + timeRangeMock := time_rangemocks.NewMockITimeRangeProvider(ctrl) + + app := &OpenAPIApplication{ + tenant: tenantMock, + workspace: workspaceMock, + timeRange: timeRangeMock, + } + + ctx := context.Background() + + // Case: StartTime=0, EndTime=0 -> use TimeRangeProvider + workspaceMock.EXPECT().GetThirdPartyQueryWorkSpaceID(gomock.Any(), int64(1)).Return("third-1") + tenantMock.EXPECT().GetOAPIQueryTenants(gomock.Any(), loop_span.PlatformCozeLoop).Return([]string{"tenant-a"}) + + now := time.Now().UnixMilli() + start := now - 10000 + end := now + timeRangeMock.EXPECT().GetTimeRange(gomock.Any(), "1", "log-id", "trace-id", gomock.Any()).Return(&start, &end) + + req := &openapi.SearchTraceOApiRequest{ + WorkspaceID: 1, + TraceID: ptr.Of("trace-id"), + Logid: ptr.Of("log-id"), + StartTime: 0, + EndTime: 0, + Limit: 50, + } + + res, err := app.buildSearchTraceOApiReq(ctx, req) + assert.NoError(t, err) + if assert.NotNil(t, res) { + assert.Equal(t, start, res.StartTime) + assert.Equal(t, end, res.EndTime) + } + + // Case: StartTime=0, EndTime=0 -> TimeRangeProvider returns nil -> should return error because DateValidator requires non-zero time + timeRangeMock.EXPECT().GetTimeRange(gomock.Any(), "2", "", "", gomock.Any()).Return(nil, nil) + + req2 := &openapi.SearchTraceOApiRequest{ + WorkspaceID: 2, + StartTime: 0, + EndTime: 0, + } + res2, err := app.buildSearchTraceOApiReq(ctx, req2) + assert.Error(t, err) + assert.Nil(t, res2) +} diff --git a/backend/modules/observability/application/wire.go b/backend/modules/observability/application/wire.go index f66351cc0..39f6cda88 100644 --- a/backend/modules/observability/application/wire.go +++ b/backend/modules/observability/application/wire.go @@ -73,6 +73,7 @@ import ( "github.com/coze-dev/coze-loop/backend/modules/observability/infra/rpc/user" obstorage "github.com/coze-dev/coze-loop/backend/modules/observability/infra/storage" "github.com/coze-dev/coze-loop/backend/modules/observability/infra/tenant" + "github.com/coze-dev/coze-loop/backend/modules/observability/infra/time_range" "github.com/coze-dev/coze-loop/backend/modules/observability/infra/workspace" "github.com/coze-dev/coze-loop/backend/pkg/conf" "github.com/google/wire" @@ -137,6 +138,7 @@ var ( NewOpenAPIApplication, auth.NewAuthProvider, traceDomainSet, + time_range.NewTimeRangeProvider, ) taskSet = wire.NewSet( tracehub.NewTraceHubImpl, diff --git a/backend/modules/observability/application/wire_gen.go b/backend/modules/observability/application/wire_gen.go index 22dc1b3ae..6cb5f93f3 100644 --- a/backend/modules/observability/application/wire_gen.go +++ b/backend/modules/observability/application/wire_gen.go @@ -73,6 +73,7 @@ import ( "github.com/coze-dev/coze-loop/backend/modules/observability/infra/rpc/user" "github.com/coze-dev/coze-loop/backend/modules/observability/infra/storage" "github.com/coze-dev/coze-loop/backend/modules/observability/infra/tenant" + "github.com/coze-dev/coze-loop/backend/modules/observability/infra/time_range" "github.com/coze-dev/coze-loop/backend/modules/observability/infra/workspace" "github.com/coze-dev/coze-loop/backend/pkg/conf" "github.com/google/wire" @@ -184,7 +185,8 @@ func InitOpenAPIApplication(mqFactory mq.IFactory, configFactory conf.IConfigLoa iAuthProvider := auth.NewAuthProvider(authClient) iWorkSpaceProvider := workspace.NewWorkspaceProvider() iCollectorProvider := collector.NewEventCollectorProvider() - iObservabilityOpenAPIApplication, err := NewOpenAPIApplication(iTraceService, iAuthProvider, benefit2, iTenantProvider, iWorkSpaceProvider, limiterFactory, iTraceConfig, iTraceMetrics, iCollectorProvider) + iTimeRangeProvider := time_range.NewTimeRangeProvider() + iObservabilityOpenAPIApplication, err := NewOpenAPIApplication(iTraceService, iAuthProvider, benefit2, iTenantProvider, iWorkSpaceProvider, limiterFactory, iTraceConfig, iTraceMetrics, iCollectorProvider, iTimeRangeProvider) if err != nil { return nil, err } @@ -339,7 +341,7 @@ var ( NewIngestionCollectorFactory, producer.NewSpanWithAnnotationProducerImpl, redis2.NewSpansRedisDaoImpl, mysql.NewTrajectoryConfigDaoImpl, ) openApiSet = wire.NewSet( - NewOpenAPIApplication, auth.NewAuthProvider, traceDomainSet, + NewOpenAPIApplication, auth.NewAuthProvider, traceDomainSet, time_range.NewTimeRangeProvider, ) taskSet = wire.NewSet(tracehub.NewTraceHubImpl, NewTaskApplication, auth.NewAuthProvider, user.NewUserRPCProvider, evaluation.NewEvaluationRPCProvider, NewTaskLocker, traceDomainSet, service3.NewTaskCallbackServiceImpl, diff --git a/backend/modules/observability/domain/component/time_range/mocks/time_range_mock.go b/backend/modules/observability/domain/component/time_range/mocks/time_range_mock.go new file mode 100644 index 000000000..9a55d9b35 --- /dev/null +++ b/backend/modules/observability/domain/component/time_range/mocks/time_range_mock.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/time_range (interfaces: ITimeRangeProvider) +// +// Generated by this command: +// +// mockgen -destination=mocks/time_range_mock.go -package=mocks . ITimeRangeProvider +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockITimeRangeProvider is a mock of ITimeRangeProvider interface. +type MockITimeRangeProvider struct { + ctrl *gomock.Controller + recorder *MockITimeRangeProviderMockRecorder + isgomock struct{} +} + +// MockITimeRangeProviderMockRecorder is the mock recorder for MockITimeRangeProvider. +type MockITimeRangeProviderMockRecorder struct { + mock *MockITimeRangeProvider +} + +// NewMockITimeRangeProvider creates a new mock instance. +func NewMockITimeRangeProvider(ctrl *gomock.Controller) *MockITimeRangeProvider { + mock := &MockITimeRangeProvider{ctrl: ctrl} + mock.recorder = &MockITimeRangeProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockITimeRangeProvider) EXPECT() *MockITimeRangeProviderMockRecorder { + return m.recorder +} + +// GetTimeRange mocks base method. +func (m *MockITimeRangeProvider) GetTimeRange(ctx context.Context, workSpaceID, logID, traceID string, delayTime int64) (*int64, *int64) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTimeRange", ctx, workSpaceID, logID, traceID, delayTime) + ret0, _ := ret[0].(*int64) + ret1, _ := ret[1].(*int64) + return ret0, ret1 +} + +// GetTimeRange indicates an expected call of GetTimeRange. +func (mr *MockITimeRangeProviderMockRecorder) GetTimeRange(ctx, workSpaceID, logID, traceID, delayTime any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeRange", reflect.TypeOf((*MockITimeRangeProvider)(nil).GetTimeRange), ctx, workSpaceID, logID, traceID, delayTime) +} diff --git a/backend/modules/observability/domain/component/time_range/time_range.go b/backend/modules/observability/domain/component/time_range/time_range.go new file mode 100644 index 000000000..c87a69aea --- /dev/null +++ b/backend/modules/observability/domain/component/time_range/time_range.go @@ -0,0 +1,12 @@ +// Copyright (c) 2026 coze-dev Authors +// SPDX-License-Identifier: Apache-2.0 +package time_range + +import ( + "context" +) + +//go:generate mockgen -destination=mocks/time_range_mock.go -package=mocks . ITimeRangeProvider +type ITimeRangeProvider interface { + GetTimeRange(ctx context.Context, workSpaceID, logID, traceID string, delayTime int64) (*int64, *int64) +} diff --git a/backend/modules/observability/infra/rpc/auth/auth.go b/backend/modules/observability/infra/rpc/auth/auth.go index 511df0ab1..0786c34b2 100644 --- a/backend/modules/observability/infra/rpc/auth/auth.go +++ b/backend/modules/observability/infra/rpc/auth/auth.go @@ -53,7 +53,7 @@ func (a *AuthProviderImpl) CheckWorkspacePermission(ctx context.Context, action, } resp, err := a.cli.MCheckPermission(ctx, req) if err != nil { - return errorx.WrapByCode(err, obErrorx.CommercialCommonRPCErrorCodeCode) + return err } else if resp == nil { logs.CtxWarn(ctx, "MCheckPermission returned nil response") return errorx.NewByCode(obErrorx.CommercialCommonRPCErrorCodeCode) diff --git a/backend/modules/observability/infra/rpc/auth/auth_test.go b/backend/modules/observability/infra/rpc/auth/auth_test.go index 49e1e552f..0281c5288 100644 --- a/backend/modules/observability/infra/rpc/auth/auth_test.go +++ b/backend/modules/observability/infra/rpc/auth/auth_test.go @@ -98,7 +98,7 @@ func TestAuthProviderImpl_CheckWorkspacePermission(t *testing.T) { mockClient.EXPECT().MCheckPermission(gomock.Any(), gomock.Any()).Return(nil, errors.New("RPC error")) }, wantErr: true, - expectedErr: obErrorx.CommercialCommonRPCErrorCodeCode, + expectedErr: 0, }, { name: "nil response", diff --git a/backend/modules/observability/infra/time_range/time_range.go b/backend/modules/observability/infra/time_range/time_range.go new file mode 100644 index 000000000..65711c577 --- /dev/null +++ b/backend/modules/observability/infra/time_range/time_range.go @@ -0,0 +1,19 @@ +// Copyright (c) 2026 coze-dev Authors +// SPDX-License-Identifier: Apache-2.0 +package time_range + +import ( + "context" + + "github.com/coze-dev/coze-loop/backend/modules/observability/domain/component/time_range" +) + +type TimeRangeProvider struct{} + +func NewTimeRangeProvider() time_range.ITimeRangeProvider { + return &TimeRangeProvider{} +} + +func (p *TimeRangeProvider) GetTimeRange(ctx context.Context, workSpaceID, logID, traceID string, delayTime int64) (*int64, *int64) { + return nil, nil +} diff --git a/backend/modules/observability/infra/time_range/time_range_test.go b/backend/modules/observability/infra/time_range/time_range_test.go new file mode 100644 index 000000000..ff5eaabf8 --- /dev/null +++ b/backend/modules/observability/infra/time_range/time_range_test.go @@ -0,0 +1,27 @@ +// Copyright (c) 2026 coze-dev Authors +// SPDX-License-Identifier: Apache-2.0 +package time_range + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewTimeRangeProvider(t *testing.T) { + provider := NewTimeRangeProvider() + assert.NotNil(t, provider) + assert.IsType(t, &TimeRangeProvider{}, provider) +} + +func TestTimeRangeProvider_GetTimeRange(t *testing.T) { + provider := NewTimeRangeProvider() + ctx := context.Background() + + t.Run("returns nil for any input", func(t *testing.T) { + start, end := provider.GetTimeRange(ctx, "workspace1", "log1", "trace1", 0) + assert.Nil(t, start) + assert.Nil(t, end) + }) +}