From e90bb016f7df1c70fc9a0433f387d8045449faba Mon Sep 17 00:00:00 2001 From: Ekansh Gupta Date: Wed, 29 Oct 2025 21:35:59 +0530 Subject: [PATCH] feat: add span percentile for traces (#8955) * feat: add span percentile for traces * feat: fixed merge conflicts * feat: fixed merge conflicts * feat: fixed merge conflicts * feat: added span percentile * feat: added span percentile * feat: added test for span percentiles * feat: added test for span percentiles * feat: added test for span percentiles * feat: added test for span percentiles * feat: removed comments * feat: moved everything to module * feat: refactored span percentile * feat: refactored span percentile * feat: refactored module package * feat: fixed tests for span percentile * feat: refactored span percentile and changed query * feat: refactored span percentile and changed query * feat: refactored span percentile and changed query * feat: refactored span percentile and changed query * feat: added better error handling * feat: added better error handling * feat: addressed pr comments * feat: addressed pr comments * feat: renamed translator.go * feat: added query settings * feat: added full query test * feat: added fingerprinting * feat: refactored tests * feat: refactored to use fingerprinting and changed tests * feat: refactored to use fingerprinting and changed tests * feat: refactored to use fingerprinting and changed tests * feat: changed errors * feat: removed redundant tests * feat: removed redundant tests * feat: moved everything to trace aggregation and updated tests * feat: addressed comments regarding metadatastore * feat: addressed comments regarding metadatastore * feat: addressed comments regarding metadatastore * feat: addressed comments for float64 * feat: cleaned up code * feat: cleaned up code --- .../implspanpercentile/handler.go | 58 +++++++ .../implspanpercentile/module.go | 126 +++++++++++++++ .../implspanpercentile/statement_builder.go | 118 ++++++++++++++ .../statement_builder_test.go | 149 ++++++++++++++++++ pkg/modules/spanpercentile/spanpercentile.go | 17 ++ pkg/query-service/app/http_handler.go | 2 + pkg/signoz/handler.go | 48 +++--- pkg/signoz/module.go | 57 ++++--- pkg/types/preferencetypes/name.go | 2 + pkg/types/preferencetypes/preference.go | 9 ++ pkg/types/spanpercentiletypes/response.go | 17 ++ .../spanpercentiletypes/spanpercentile.go | 43 +++++ 12 files changed, 598 insertions(+), 48 deletions(-) create mode 100644 pkg/modules/spanpercentile/implspanpercentile/handler.go create mode 100644 pkg/modules/spanpercentile/implspanpercentile/module.go create mode 100644 pkg/modules/spanpercentile/implspanpercentile/statement_builder.go create mode 100644 pkg/modules/spanpercentile/implspanpercentile/statement_builder_test.go create mode 100644 pkg/modules/spanpercentile/spanpercentile.go create mode 100644 pkg/types/spanpercentiletypes/response.go create mode 100644 pkg/types/spanpercentiletypes/spanpercentile.go diff --git a/pkg/modules/spanpercentile/implspanpercentile/handler.go b/pkg/modules/spanpercentile/implspanpercentile/handler.go new file mode 100644 index 0000000000..fb6263886e --- /dev/null +++ b/pkg/modules/spanpercentile/implspanpercentile/handler.go @@ -0,0 +1,58 @@ +package implspanpercentile + +import ( + "encoding/json" + "net/http" + + errorsV2 "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/modules/spanpercentile" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/spanpercentiletypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type handler struct { + module spanpercentile.Module +} + +func NewHandler(module spanpercentile.Module) spanpercentile.Handler { + return &handler{ + module: module, + } +} + +func (h *handler) GetSpanPercentileDetails(w http.ResponseWriter, r *http.Request) { + claims, err := authtypes.ClaimsFromContext(r.Context()) + if err != nil { + render.Error(w, err) + return + } + + spanPercentileRequest, err := parseSpanPercentileRequestBody(r) + if err != nil { + render.Error(w, err) + return + } + + result, err := h.module.GetSpanPercentile(r.Context(), valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), spanPercentileRequest) + if err != nil { + render.Error(w, err) + return + } + + render.Success(w, http.StatusOK, result) +} + +func parseSpanPercentileRequestBody(r *http.Request) (*spanpercentiletypes.SpanPercentileRequest, error) { + req := new(spanpercentiletypes.SpanPercentileRequest) + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + return nil, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "cannot parse the request body: %v", err) + } + + if err := req.Validate(); err != nil { + return nil, err + } + + return req, nil +} diff --git a/pkg/modules/spanpercentile/implspanpercentile/module.go b/pkg/modules/spanpercentile/implspanpercentile/module.go new file mode 100644 index 0000000000..e0247fb6f0 --- /dev/null +++ b/pkg/modules/spanpercentile/implspanpercentile/module.go @@ -0,0 +1,126 @@ +package implspanpercentile + +import ( + "context" + "fmt" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/modules/spanpercentile" + "github.com/SigNoz/signoz/pkg/querier" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/spanpercentiletypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type module struct { + querier querier.Querier +} + +func NewModule( + querier querier.Querier, + _ factory.ProviderSettings, +) spanpercentile.Module { + return &module{ + querier: querier, + } +} + +func (m *module) GetSpanPercentile(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, req *spanpercentiletypes.SpanPercentileRequest) (*spanpercentiletypes.SpanPercentileResponse, error) { + queryRangeRequest, err := buildSpanPercentileQuery(ctx, req) + if err != nil { + return nil, err + } + + if err := queryRangeRequest.Validate(); err != nil { + return nil, err + } + + result, err := m.querier.QueryRange(ctx, orgID, queryRangeRequest) + if err != nil { + return nil, err + } + + return transformToSpanPercentileResponse(result) +} + +func transformToSpanPercentileResponse(queryResult *qbtypes.QueryRangeResponse) (*spanpercentiletypes.SpanPercentileResponse, error) { + if len(queryResult.Data.Results) == 0 { + return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "no data returned from query") + } + + scalarData, ok := queryResult.Data.Results[0].(*qbtypes.ScalarData) + if !ok { + return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "unexpected result type") + } + + if len(scalarData.Data) == 0 { + return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "no rows returned from query") + } + + row := scalarData.Data[0] + + columnMap := make(map[string]int) + for i, col := range scalarData.Columns { + columnMap[col.Name] = i + } + + p50Idx, ok := columnMap["__result_0"] + if !ok { + return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "missing __result_0 column") + } + p90Idx, ok := columnMap["__result_1"] + if !ok { + return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "missing __result_1 column") + } + p99Idx, ok := columnMap["__result_2"] + if !ok { + return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "missing __result_2 column") + } + positionIdx, ok := columnMap["__result_3"] + if !ok { + return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "missing __result_3 column") + } + + p50, err := toFloat64(row[p50Idx]) + if err != nil { + return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "no spans found matching the specified criteria") + } + p90, err := toFloat64(row[p90Idx]) + if err != nil { + return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "no spans found matching the specified criteria") + } + p99, err := toFloat64(row[p99Idx]) + if err != nil { + return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "no spans found matching the specified criteria") + } + position, err := toFloat64(row[positionIdx]) + if err != nil { + return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "no spans found matching the specified criteria") + } + + description := fmt.Sprintf("faster than %.1f%% of spans", position) + if position < 50 { + description = fmt.Sprintf("slower than %.1f%% of spans", 100-position) + } + + return &spanpercentiletypes.SpanPercentileResponse{ + Percentiles: spanpercentiletypes.PercentileStats{ + P50: p50, + P90: p90, + P99: p99, + }, + Position: spanpercentiletypes.PercentilePosition{ + Percentile: position, + Description: description, + }, + }, nil +} + +func toFloat64(val any) (float64, error) { + result, ok := val.(float64) + if !ok { + return 0, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot convert %T to float64", val) + } + return result, nil +} diff --git a/pkg/modules/spanpercentile/implspanpercentile/statement_builder.go b/pkg/modules/spanpercentile/implspanpercentile/statement_builder.go new file mode 100644 index 0000000000..c44caa1b3e --- /dev/null +++ b/pkg/modules/spanpercentile/implspanpercentile/statement_builder.go @@ -0,0 +1,118 @@ +package implspanpercentile + +import ( + "context" + "fmt" + "sort" + "strings" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/spanpercentiletypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" +) + +func buildSpanPercentileQuery( + _ context.Context, + req *spanpercentiletypes.SpanPercentileRequest, +) (*qbtypes.QueryRangeRequest, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + var attrKeys []string + for key := range req.ResourceAttributes { + attrKeys = append(attrKeys, key) + } + sort.Strings(attrKeys) + + filterConditions := []string{ + fmt.Sprintf("service.name = '%s'", strings.ReplaceAll(req.ServiceName, "'", `\'`)), + fmt.Sprintf("name = '%s'", strings.ReplaceAll(req.Name, "'", `\'`)), + } + + for _, key := range attrKeys { + value := req.ResourceAttributes[key] + filterConditions = append(filterConditions, + fmt.Sprintf("%s = '%s'", key, strings.ReplaceAll(value, "'", `\'`))) + } + + filterExpr := strings.Join(filterConditions, " AND ") + + groupByKeys := []qbtypes.GroupByKey{ + { + TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{ + Name: "service.name", + Signal: telemetrytypes.SignalTraces, + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + { + TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{ + Name: "name", + Signal: telemetrytypes.SignalTraces, + FieldContext: telemetrytypes.FieldContextSpan, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + } + + for _, key := range attrKeys { + groupByKeys = append(groupByKeys, qbtypes.GroupByKey{ + TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{ + Name: key, + Signal: telemetrytypes.SignalTraces, + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }) + } + + query := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Name: "span_percentile", + Signal: telemetrytypes.SignalTraces, + Aggregations: []qbtypes.TraceAggregation{ + { + Expression: "p50(duration_nano)", + Alias: "p50_duration_nano", + }, + { + Expression: "p90(duration_nano)", + Alias: "p90_duration_nano", + }, + { + Expression: "p99(duration_nano)", + Alias: "p99_duration_nano", + }, + { + Expression: fmt.Sprintf( + "(100.0 * countIf(duration_nano <= %d)) / count()", + req.DurationNano, + ), + Alias: "percentile_position", + }, + }, + GroupBy: groupByKeys, + Filter: &qbtypes.Filter{ + Expression: filterExpr, + }, + } + + queryEnvelope := qbtypes.QueryEnvelope{ + Type: qbtypes.QueryTypeBuilder, + Spec: query, + } + + return &qbtypes.QueryRangeRequest{ + SchemaVersion: "v5", + Start: req.Start, + End: req.End, + RequestType: qbtypes.RequestTypeScalar, + CompositeQuery: qbtypes.CompositeQuery{ + Queries: []qbtypes.QueryEnvelope{queryEnvelope}, + }, + FormatOptions: &qbtypes.FormatOptions{ + FormatTableResultForUI: true, + }, + }, nil +} diff --git a/pkg/modules/spanpercentile/implspanpercentile/statement_builder_test.go b/pkg/modules/spanpercentile/implspanpercentile/statement_builder_test.go new file mode 100644 index 0000000000..7cb2045bc5 --- /dev/null +++ b/pkg/modules/spanpercentile/implspanpercentile/statement_builder_test.go @@ -0,0 +1,149 @@ +package implspanpercentile + +import ( + "context" + "fmt" + "sort" + "testing" + + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/spanpercentiletypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/stretchr/testify/require" +) + +func TestBuildSpanPercentileQuery(t *testing.T) { + req := &spanpercentiletypes.SpanPercentileRequest{ + DurationNano: 100000, + Name: "test", + ServiceName: "test-service", + ResourceAttributes: map[string]string{}, + Start: 1640995200000, + End: 1640995800000, + } + + ctx := context.Background() + result, err := buildSpanPercentileQuery(ctx, req) + require.NoError(t, err) + require.NotNil(t, result) + + require.Equal(t, 1, len(result.CompositeQuery.Queries)) + require.Equal(t, qbtypes.QueryTypeBuilder, result.CompositeQuery.Queries[0].Type) + + query, ok := result.CompositeQuery.Queries[0].Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) + require.True(t, ok, "Spec should be QueryBuilderQuery type") + + require.Equal(t, "span_percentile", query.Name) + require.Equal(t, telemetrytypes.SignalTraces, query.Signal) + + require.Equal(t, 4, len(query.Aggregations)) + require.Equal(t, "p50(duration_nano)", query.Aggregations[0].Expression) + require.Equal(t, "p50_duration_nano", query.Aggregations[0].Alias) + require.Equal(t, "p90(duration_nano)", query.Aggregations[1].Expression) + require.Equal(t, "p90_duration_nano", query.Aggregations[1].Alias) + require.Equal(t, "p99(duration_nano)", query.Aggregations[2].Expression) + require.Equal(t, "p99_duration_nano", query.Aggregations[2].Alias) + require.Equal(t, "(100.0 * countIf(duration_nano <= 100000)) / count()", query.Aggregations[3].Expression) + require.Equal(t, "percentile_position", query.Aggregations[3].Alias) + + require.NotNil(t, query.Filter) + require.Equal(t, "service.name = 'test-service' AND name = 'test'", query.Filter.Expression) + + require.Equal(t, 2, len(query.GroupBy)) + require.Equal(t, "service.name", query.GroupBy[0].TelemetryFieldKey.Name) + require.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].TelemetryFieldKey.FieldContext) + require.Equal(t, "name", query.GroupBy[1].TelemetryFieldKey.Name) + require.Equal(t, telemetrytypes.FieldContextSpan, query.GroupBy[1].TelemetryFieldKey.FieldContext) + + require.Equal(t, qbtypes.RequestTypeScalar, result.RequestType) +} + +func TestBuildSpanPercentileQueryWithResourceAttributes(t *testing.T) { + testCases := []struct { + name string + request *spanpercentiletypes.SpanPercentileRequest + expectedFilterExpr string + }{ + { + name: "query with service.name only (no additional resource attributes)", + request: &spanpercentiletypes.SpanPercentileRequest{ + DurationNano: 100000, + Name: "GET /api/users", + ServiceName: "user-service", + ResourceAttributes: map[string]string{}, + Start: 1640995200000, + End: 1640995800000, + }, + expectedFilterExpr: "service.name = 'user-service' AND name = 'GET /api/users'", + }, + { + name: "query with service.name and deployment.environment", + request: &spanpercentiletypes.SpanPercentileRequest{ + DurationNano: 250000, + Name: "POST /api/orders", + ServiceName: "order-service", + ResourceAttributes: map[string]string{ + "deployment.environment": "production", + }, + Start: 1640995200000, + End: 1640995800000, + }, + expectedFilterExpr: "service.name = 'order-service' AND name = 'POST /api/orders' AND deployment.environment = 'production'", + }, + { + name: "query with multiple resource attributes", + request: &spanpercentiletypes.SpanPercentileRequest{ + DurationNano: 500000, + Name: "DELETE /api/items", + ServiceName: "inventory-service", + ResourceAttributes: map[string]string{ + "cloud.platform": "aws", + "deployment.environment": "staging", + "k8s.cluster.name": "staging-cluster", + }, + Start: 1640995200000, + End: 1640995800000, + }, + expectedFilterExpr: "service.name = 'inventory-service' AND name = 'DELETE /api/items' AND cloud.platform = 'aws' AND deployment.environment = 'staging' AND k8s.cluster.name = 'staging-cluster'", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + result, err := buildSpanPercentileQuery(ctx, tc.request) + require.NoError(t, err) + require.NotNil(t, result) + + query, ok := result.CompositeQuery.Queries[0].Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) + require.True(t, ok, "Spec should be QueryBuilderQuery type") + + require.Equal(t, tc.expectedFilterExpr, query.Filter.Expression) + + require.Equal(t, 4, len(query.Aggregations)) + require.Equal(t, "p50(duration_nano)", query.Aggregations[0].Expression) + require.Equal(t, "p90(duration_nano)", query.Aggregations[1].Expression) + require.Equal(t, "p99(duration_nano)", query.Aggregations[2].Expression) + require.Contains(t, query.Aggregations[3].Expression, fmt.Sprintf("countIf(duration_nano <= %d)", tc.request.DurationNano)) + + expectedGroupByCount := 2 + len(tc.request.ResourceAttributes) + require.Equal(t, expectedGroupByCount, len(query.GroupBy)) + require.Equal(t, "service.name", query.GroupBy[0].TelemetryFieldKey.Name) + require.Equal(t, "name", query.GroupBy[1].TelemetryFieldKey.Name) + + for i, key := range getSortedKeys(tc.request.ResourceAttributes) { + require.Equal(t, key, query.GroupBy[2+i].TelemetryFieldKey.Name) + require.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[2+i].TelemetryFieldKey.FieldContext) + } + }) + } +} + +func getSortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/pkg/modules/spanpercentile/spanpercentile.go b/pkg/modules/spanpercentile/spanpercentile.go new file mode 100644 index 0000000000..c1fd1b55c8 --- /dev/null +++ b/pkg/modules/spanpercentile/spanpercentile.go @@ -0,0 +1,17 @@ +package spanpercentile + +import ( + "context" + "net/http" + + "github.com/SigNoz/signoz/pkg/types/spanpercentiletypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type Module interface { + GetSpanPercentile(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, req *spanpercentiletypes.SpanPercentileRequest) (*spanpercentiletypes.SpanPercentileResponse, error) +} + +type Handler interface { + GetSpanPercentileDetails(http.ResponseWriter, *http.Request) +} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index be3646415f..d6615c9224 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -625,6 +625,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { // Export router.HandleFunc("/api/v1/export_raw_data", am.ViewAccess(aH.Signoz.Handlers.RawDataExport.ExportRawData)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/span_percentile", am.ViewAccess(aH.Signoz.Handlers.SpanPercentile.GetSpanPercentileDetails)).Methods(http.MethodPost) + } func (ah *APIHandler) MetricExplorerRoutes(router *mux.Router, am *middleware.AuthZ) { diff --git a/pkg/signoz/handler.go b/pkg/signoz/handler.go index 432cf31d42..ae1461a613 100644 --- a/pkg/signoz/handler.go +++ b/pkg/signoz/handler.go @@ -20,6 +20,8 @@ import ( "github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview" "github.com/SigNoz/signoz/pkg/modules/session" "github.com/SigNoz/signoz/pkg/modules/session/implsession" + "github.com/SigNoz/signoz/pkg/modules/spanpercentile" + "github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile" "github.com/SigNoz/signoz/pkg/modules/tracefunnel" "github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel" "github.com/SigNoz/signoz/pkg/modules/user" @@ -27,31 +29,33 @@ import ( ) type Handlers struct { - Organization organization.Handler - Preference preference.Handler - User user.Handler - SavedView savedview.Handler - Apdex apdex.Handler - Dashboard dashboard.Handler - QuickFilter quickfilter.Handler - TraceFunnel tracefunnel.Handler - RawDataExport rawdataexport.Handler - AuthDomain authdomain.Handler - Session session.Handler + Organization organization.Handler + Preference preference.Handler + User user.Handler + SavedView savedview.Handler + Apdex apdex.Handler + Dashboard dashboard.Handler + QuickFilter quickfilter.Handler + TraceFunnel tracefunnel.Handler + RawDataExport rawdataexport.Handler + AuthDomain authdomain.Handler + Session session.Handler + SpanPercentile spanpercentile.Handler } func NewHandlers(modules Modules, providerSettings factory.ProviderSettings) Handlers { return Handlers{ - Organization: implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter), - Preference: implpreference.NewHandler(modules.Preference), - User: impluser.NewHandler(modules.User, modules.UserGetter), - SavedView: implsavedview.NewHandler(modules.SavedView), - Apdex: implapdex.NewHandler(modules.Apdex), - Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings), - QuickFilter: implquickfilter.NewHandler(modules.QuickFilter), - TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel), - RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport), - AuthDomain: implauthdomain.NewHandler(modules.AuthDomain), - Session: implsession.NewHandler(modules.Session), + Organization: implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter), + Preference: implpreference.NewHandler(modules.Preference), + User: impluser.NewHandler(modules.User, modules.UserGetter), + SavedView: implsavedview.NewHandler(modules.SavedView), + Apdex: implapdex.NewHandler(modules.Apdex), + Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings), + QuickFilter: implquickfilter.NewHandler(modules.QuickFilter), + TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel), + RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport), + AuthDomain: implauthdomain.NewHandler(modules.AuthDomain), + Session: implsession.NewHandler(modules.Session), + SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile), } } diff --git a/pkg/signoz/module.go b/pkg/signoz/module.go index 323582c733..dbeb98460d 100644 --- a/pkg/signoz/module.go +++ b/pkg/signoz/module.go @@ -24,6 +24,8 @@ import ( "github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview" "github.com/SigNoz/signoz/pkg/modules/session" "github.com/SigNoz/signoz/pkg/modules/session/implsession" + "github.com/SigNoz/signoz/pkg/modules/spanpercentile" + "github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile" "github.com/SigNoz/signoz/pkg/modules/tracefunnel" "github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel" "github.com/SigNoz/signoz/pkg/modules/user" @@ -36,19 +38,20 @@ import ( ) type Modules struct { - OrgGetter organization.Getter - OrgSetter organization.Setter - Preference preference.Module - User user.Module - UserGetter user.Getter - SavedView savedview.Module - Apdex apdex.Module - Dashboard dashboard.Module - QuickFilter quickfilter.Module - TraceFunnel tracefunnel.Module - RawDataExport rawdataexport.Module - AuthDomain authdomain.Module - Session session.Module + OrgGetter organization.Getter + OrgSetter organization.Setter + Preference preference.Module + User user.Module + UserGetter user.Getter + SavedView savedview.Module + Apdex apdex.Module + Dashboard dashboard.Module + QuickFilter quickfilter.Module + TraceFunnel tracefunnel.Module + RawDataExport rawdataexport.Module + AuthDomain authdomain.Module + Session session.Module + SpanPercentile spanpercentile.Module } func NewModules( @@ -66,19 +69,21 @@ func NewModules( orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter) user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics) userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings)) + return Modules{ - OrgGetter: orgGetter, - OrgSetter: orgSetter, - Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()), - SavedView: implsavedview.NewModule(sqlstore), - Apdex: implapdex.NewModule(sqlstore), - Dashboard: impldashboard.NewModule(sqlstore, providerSettings, analytics), - User: user, - UserGetter: userGetter, - QuickFilter: quickfilter, - TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), - RawDataExport: implrawdataexport.NewModule(querier), - AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore)), - Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore)), tokenizer, orgGetter), + OrgGetter: orgGetter, + OrgSetter: orgSetter, + Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()), + SavedView: implsavedview.NewModule(sqlstore), + Apdex: implapdex.NewModule(sqlstore), + Dashboard: impldashboard.NewModule(sqlstore, providerSettings, analytics), + User: user, + UserGetter: userGetter, + QuickFilter: quickfilter, + TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), + RawDataExport: implrawdataexport.NewModule(querier), + AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore)), + Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore)), tokenizer, orgGetter), + SpanPercentile: implspanpercentile.NewModule(querier, providerSettings), } } diff --git a/pkg/types/preferencetypes/name.go b/pkg/types/preferencetypes/name.go index 2760ee40c8..38c07bb95b 100644 --- a/pkg/types/preferencetypes/name.go +++ b/pkg/types/preferencetypes/name.go @@ -20,6 +20,7 @@ var ( NameNavShortcuts = Name{valuer.NewString("nav_shortcuts")} NameLastSeenChangelogVersion = Name{valuer.NewString("last_seen_changelog_version")} NameSpanDetailsPinnedAttributes = Name{valuer.NewString("span_details_pinned_attributes")} + NameSpanPercentileResourceAttributes = Name{valuer.NewString("span_percentile_resource_attributes")} ) type Name struct{ valuer.String } @@ -39,6 +40,7 @@ func NewName(name string) (Name, error) { NameNavShortcuts.StringValue(), NameLastSeenChangelogVersion.StringValue(), NameSpanDetailsPinnedAttributes.StringValue(), + NameSpanPercentileResourceAttributes.StringValue(), }, name, ) diff --git a/pkg/types/preferencetypes/preference.go b/pkg/types/preferencetypes/preference.go index 42d1f11d8c..862e7796de 100644 --- a/pkg/types/preferencetypes/preference.go +++ b/pkg/types/preferencetypes/preference.go @@ -163,6 +163,15 @@ func NewAvailablePreference() map[Name]Preference { AllowedValues: []string{}, Value: MustNewValue([]any{}, ValueTypeArray), }, + NameSpanPercentileResourceAttributes: { + Name: NameSpanPercentileResourceAttributes, + Description: "Additional resource attributes for span percentile filtering (beyond mandatory name and service.name).", + ValueType: ValueTypeArray, + DefaultValue: MustNewValue([]any{"deployment.environment"}, ValueTypeArray), + AllowedScopes: []Scope{ScopeUser}, + AllowedValues: []string{}, + Value: MustNewValue([]any{"deployment.environment"}, ValueTypeArray), + }, } } diff --git a/pkg/types/spanpercentiletypes/response.go b/pkg/types/spanpercentiletypes/response.go new file mode 100644 index 0000000000..be1d372ad9 --- /dev/null +++ b/pkg/types/spanpercentiletypes/response.go @@ -0,0 +1,17 @@ +package spanpercentiletypes + +type SpanPercentileResponse struct { + Percentiles PercentileStats `json:"percentiles"` + Position PercentilePosition `json:"position"` +} + +type PercentileStats struct { + P50 float64 `json:"p50"` + P90 float64 `json:"p90"` + P99 float64 `json:"p99"` +} + +type PercentilePosition struct { + Percentile float64 `json:"percentile"` + Description string `json:"description"` +} diff --git a/pkg/types/spanpercentiletypes/spanpercentile.go b/pkg/types/spanpercentiletypes/spanpercentile.go new file mode 100644 index 0000000000..5f2a2b4d69 --- /dev/null +++ b/pkg/types/spanpercentiletypes/spanpercentile.go @@ -0,0 +1,43 @@ +package spanpercentiletypes + +import ( + "github.com/SigNoz/signoz/pkg/errors" +) + +type SpanPercentileRequest struct { + DurationNano int64 `json:"spanDuration"` + Name string `json:"name"` + ServiceName string `json:"serviceName"` + ResourceAttributes map[string]string `json:"resourceAttributes"` + Start uint64 `json:"start"` + End uint64 `json:"end"` +} + +func (req *SpanPercentileRequest) Validate() error { + if req.Name == "" { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "name is required") + } + + if req.ServiceName == "" { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "service_name is required") + } + + if req.DurationNano <= 0 { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "duration_nano must be greater than 0") + } + + if req.Start >= req.End { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "start time must be before end time") + } + + for key, val := range req.ResourceAttributes { + if key == "" { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "resource attribute key cannot be empty") + } + if val == "" { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "resource attribute value cannot be empty") + } + } + + return nil +}