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:
58
pkg/modules/spanpercentile/implspanpercentile/handler.go
Normal file
58
pkg/modules/spanpercentile/implspanpercentile/handler.go
Normal 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
|
||||
}
|
||||
126
pkg/modules/spanpercentile/implspanpercentile/module.go
Normal file
126
pkg/modules/spanpercentile/implspanpercentile/module.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
17
pkg/modules/spanpercentile/spanpercentile.go
Normal file
17
pkg/modules/spanpercentile/spanpercentile.go
Normal 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)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
pkg/types/spanpercentiletypes/response.go
Normal file
17
pkg/types/spanpercentiletypes/response.go
Normal 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"`
|
||||
}
|
||||
43
pkg/types/spanpercentiletypes/spanpercentile.go
Normal file
43
pkg/types/spanpercentiletypes/spanpercentile.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user