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
This commit is contained in:
Ekansh Gupta
2025-10-29 21:35:59 +05:30
committed by GitHub
parent bdecbfb7f5
commit e90bb016f7
12 changed files with 598 additions and 48 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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),
}
}

View File

@@ -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),
}
}

View File

@@ -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,
)

View File

@@ -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),
},
}
}

View File

@@ -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"`
}

View File

@@ -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
}