mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-07 15:10:37 +01:00
Compare commits
3 Commits
issue-5535
...
platform-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1798abfc6b | ||
|
|
f155f71051 | ||
|
|
4559a78752 |
@@ -24118,9 +24118,17 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
summary: Query range
|
||||
tags:
|
||||
- querier
|
||||
@@ -24187,9 +24195,17 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
summary: Query range preview
|
||||
tags:
|
||||
- querier
|
||||
|
||||
@@ -5,12 +5,25 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func telemetryReadScopes() []string {
|
||||
return []string{
|
||||
coretypes.ResourceTelemetryResourceLogs.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceTraces.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceAuditLogs.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceMeterMetrics.Scope(coretypes.VerbRead),
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v5/query_range", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.QueryRange), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v5/query_range", handler.New(provider.authzMiddleware.CheckResources(provider.querierHandler.QueryRange, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName), handler.OpenAPIDef{
|
||||
ID: "QueryRangeV5",
|
||||
Tags: []string{"querier"},
|
||||
Summary: "Query range",
|
||||
@@ -446,12 +459,17 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes(telemetryReadScopes()),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: telemetrytypes.PrefixSelector,
|
||||
Resources: telemetrytypes.QueryRangeResources,
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v5/query_range/preview", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.QueryRangePreview), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v5/query_range/preview", handler.New(provider.authzMiddleware.CheckResources(provider.querierHandler.QueryRangePreview, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName), handler.OpenAPIDef{
|
||||
ID: "QueryRangePreviewV5",
|
||||
Tags: []string{"querier"},
|
||||
Summary: "Query range preview",
|
||||
@@ -463,8 +481,13 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes(telemetryReadScopes()),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: telemetrytypes.PrefixSelector,
|
||||
Resources: telemetrytypes.QueryRangeResources,
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package handler
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
)
|
||||
|
||||
type ResourceDef interface {
|
||||
// resolveRequest is unexported to seal the interface. It returns a slice so a
|
||||
@@ -97,3 +100,31 @@ func (def AttachDetachParentChildResourceDef) resolveRequest(ec coretypes.Extrac
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type TelemetryResourceDef struct {
|
||||
Verb coretypes.Verb
|
||||
Category coretypes.ActionCategory
|
||||
Selector coretypes.SelectorFunc
|
||||
Resources coretypes.ResourceExtractor
|
||||
}
|
||||
|
||||
func (def TelemetryResourceDef) resolveRequest(ec coretypes.ExtractorContext) []coretypes.ResolvedResource {
|
||||
refs, err := def.Resources(ec)
|
||||
if err != nil {
|
||||
return []coretypes.ResolvedResource{coretypes.NewResolvedResourceWithError(def.Verb, def.Category, err)}
|
||||
}
|
||||
if len(refs) == 0 {
|
||||
return []coretypes.ResolvedResource{coretypes.NewResolvedResourceWithError(
|
||||
def.Verb,
|
||||
def.Category,
|
||||
errors.NewInvalidInputf(errors.CodeInvalidInput, "request resolved to no resources"),
|
||||
)}
|
||||
}
|
||||
|
||||
resolved := make([]coretypes.ResolvedResource, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
resolved = append(resolved, coretypes.NewResolvedResourceWithID(def.Verb, def.Category, ref.Resource, ref.ID, def.Selector))
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
@@ -118,6 +118,10 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
extractorCtx := coretypes.ExtractorContext{Request: req, ResponseBody: writer.BodyBytes()}
|
||||
|
||||
for _, resource := range resolved {
|
||||
if err := resource.Err(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resource.ResolveResponse(extractorCtx)
|
||||
verb, category := resource.Verb(), resource.Category()
|
||||
|
||||
|
||||
@@ -218,6 +218,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateSSORoleMappingNamesFactory(sqlstore),
|
||||
sqlmigration.NewAddMetricReductionRulesFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewRemoveOrganizationTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddTelemetryTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
144
pkg/sqlmigration/099_add_telemetry_tuples.go
Normal file
144
pkg/sqlmigration/099_add_telemetry_tuples.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addTelemetryTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddTelemetryTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_telemetry_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addTelemetryTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addTelemetryTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addTelemetryTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
rows, err := tx.QueryContext(ctx, `SELECT id FROM organizations`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var orgID string
|
||||
if err := rows.Scan(&orgID); err != nil {
|
||||
return err
|
||||
}
|
||||
orgIDs = append(orgIDs, orgID)
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
tuples := []migrationTuple{
|
||||
{authtypes.SigNozAdminRoleName, "telemetryresource", "logs", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "telemetryresource", "traces", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "telemetryresource", "metrics", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "telemetryresource", "audit-logs", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "telemetryresource", "meter-metrics", "read"},
|
||||
{authtypes.SigNozEditorRoleName, "telemetryresource", "logs", "read"},
|
||||
{authtypes.SigNozEditorRoleName, "telemetryresource", "traces", "read"},
|
||||
{authtypes.SigNozEditorRoleName, "telemetryresource", "metrics", "read"},
|
||||
{authtypes.SigNozViewerRoleName, "telemetryresource", "logs", "read"},
|
||||
{authtypes.SigNozViewerRoleName, "telemetryresource", "traces", "read"},
|
||||
{authtypes.SigNozViewerRoleName, "telemetryresource", "metrics", "read"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
|
||||
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
|
||||
|
||||
if isPG {
|
||||
user := "role:" + roleSubject + "#assignee"
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "TUPLE_OPERATION_WRITE", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addTelemetryTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -55,6 +55,13 @@ func OneID(extractor ResourceIDExtractor) ResourceIDsExtractor {
|
||||
}}
|
||||
}
|
||||
|
||||
type ResourceWithID struct {
|
||||
Resource Resource
|
||||
ID string
|
||||
}
|
||||
|
||||
type ResourceExtractor func(ExtractorContext) ([]ResourceWithID, error)
|
||||
|
||||
func PathParam(name string) ResourceIDExtractor {
|
||||
return ResourceIDExtractor{Phase: PhaseRequest, Fn: func(ec ExtractorContext) (string, error) {
|
||||
if ec.Request == nil {
|
||||
|
||||
@@ -23,5 +23,5 @@ var (
|
||||
TypeRole = Type{valuer.NewString("role"), regexp.MustCompile(`^([a-z-]{1,50}|\*)$`), []Verb{VerbAssignee, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
|
||||
TypeOrganization = Type{valuer.NewString("organization"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbRead, VerbUpdate}}
|
||||
TypeMetaResource = Type{valuer.NewString("metaresource"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
|
||||
TypeTelemetryResource = Type{valuer.NewString("telemetryresource"), regexp.MustCompile(`^\*$`), []Verb{VerbRead}}
|
||||
TypeTelemetryResource = Type{valuer.NewString("telemetryresource"), regexp.MustCompile(`^(\*|(builder_query|builder_sub_query|builder_trace_operator|promql|clickhouse_sql)(/([a-f0-9]{32}|\*)){0,2})$`), []Verb{VerbRead}}
|
||||
)
|
||||
|
||||
@@ -30,6 +30,19 @@ func NewResolvedResource(
|
||||
return resolved
|
||||
}
|
||||
|
||||
func NewResolvedResourceWithID(verb Verb, category ActionCategory, resource Resource, id string, selector SelectorFunc) ResolvedResource {
|
||||
resolved := &resolvedResource{verb: verb, category: category, resource: resource, selector: selector}
|
||||
if id != "" {
|
||||
resolved.ids = []string{id}
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
func NewResolvedResourceWithError(verb Verb, category ActionCategory, err error) ResolvedResource {
|
||||
return &resolvedResource{verb: verb, category: category, err: err}
|
||||
}
|
||||
|
||||
func (resolved *resolvedResource) fill(phase ExtractPhase, ec ExtractorContext) {
|
||||
if !resolved.idExtractor.IsPhase(phase) {
|
||||
return
|
||||
|
||||
42
pkg/types/coretypes/selector_test.go
Normal file
42
pkg/types/coretypes/selector_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package coretypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTelemetryResourceSelectorRegex(t *testing.T) {
|
||||
segment := "abcdef0123456789abcdef0123456789"
|
||||
|
||||
valid := []string{
|
||||
"*",
|
||||
"builder_query",
|
||||
"promql",
|
||||
"clickhouse_sql",
|
||||
"builder_trace_operator",
|
||||
"builder_sub_query",
|
||||
"builder_query/" + segment,
|
||||
"builder_query/*",
|
||||
"builder_query/" + segment + "/" + segment,
|
||||
"builder_query/" + segment + "/*",
|
||||
}
|
||||
for _, value := range valid {
|
||||
_, err := TypeTelemetryResource.Selector(value)
|
||||
assert.NoError(t, err, "expected %q to be a valid telemetry selector", value)
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"",
|
||||
"builder_formula",
|
||||
"builder_join",
|
||||
"trace_operator",
|
||||
"builder_query/abc",
|
||||
"builder_query/" + segment + "/" + segment + "/" + segment,
|
||||
"unknown_type/" + segment,
|
||||
}
|
||||
for _, value := range invalid {
|
||||
_, err := TypeTelemetryResource.Selector(value)
|
||||
assert.Error(t, err, "expected %q to be rejected as a telemetry selector", value)
|
||||
}
|
||||
}
|
||||
93
pkg/types/telemetrytypes/resources.go
Normal file
93
pkg/types/telemetrytypes/resources.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func QueryRangeResources(ec coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
||||
queries := gjson.GetBytes(ec.RequestBody, "compositeQuery.queries")
|
||||
if !queries.IsArray() || len(queries.Array()) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "composite query has no queries")
|
||||
}
|
||||
|
||||
refs := make([]coretypes.ResourceWithID, 0, len(queries.Array()))
|
||||
seen := make(map[string]struct{})
|
||||
for _, query := range queries.Array() {
|
||||
queryRefs, err := resourcesForQuery(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ref := range queryRefs {
|
||||
key := ref.Resource.Kind().String() + ":" + ref.ID
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func resourcesForQuery(query gjson.Result) ([]coretypes.ResourceWithID, error) {
|
||||
queryType := query.Get("type").String()
|
||||
|
||||
switch queryType {
|
||||
case "builder_query", "builder_sub_query":
|
||||
return resourcesForBuilderQuery(queryType, query.Get("spec"))
|
||||
case "builder_trace_operator":
|
||||
return []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceTraces, ID: queryType}}, nil
|
||||
case "promql":
|
||||
return []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: queryType}}, nil
|
||||
case "clickhouse_sql":
|
||||
return []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: queryType},
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: queryType},
|
||||
{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: queryType},
|
||||
}, nil
|
||||
case "builder_formula", "builder_join":
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported query type %q", queryType)
|
||||
}
|
||||
}
|
||||
|
||||
func resourcesForBuilderQuery(queryType string, spec gjson.Result) ([]coretypes.ResourceWithID, error) {
|
||||
whereSegment := selectorSegment(spec.Get("filter.expression").String())
|
||||
source := spec.Get("source").String()
|
||||
|
||||
switch spec.Get("signal").String() {
|
||||
case SignalTraces.StringValue():
|
||||
return []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceTraces, ID: queryType + "/" + whereSegment}}, nil
|
||||
case SignalLogs.StringValue():
|
||||
resource := coretypes.ResourceTelemetryResourceLogs
|
||||
if source == SourceAudit.StringValue() {
|
||||
resource = coretypes.ResourceTelemetryResourceAuditLogs
|
||||
}
|
||||
return []coretypes.ResourceWithID{{Resource: resource, ID: queryType + "/" + whereSegment}}, nil
|
||||
case SignalMetrics.StringValue():
|
||||
resource := coretypes.ResourceTelemetryResourceMetrics
|
||||
if source == SourceMeter.StringValue() {
|
||||
resource = coretypes.ResourceTelemetryResourceMeterMetrics
|
||||
}
|
||||
|
||||
refs := make([]coretypes.ResourceWithID, 0, 1)
|
||||
for _, aggregation := range spec.Get("aggregations").Array() {
|
||||
refs = append(refs, coretypes.ResourceWithID{
|
||||
Resource: resource,
|
||||
ID: queryType + "/" + selectorSegment(aggregation.Get("metricName").String()) + "/" + whereSegment,
|
||||
})
|
||||
}
|
||||
if len(refs) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metrics query has no aggregations")
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported signal %q", spec.Get("signal").String())
|
||||
}
|
||||
}
|
||||
133
pkg/types/telemetrytypes/resources_test.go
Normal file
133
pkg/types/telemetrytypes/resources_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQueryRangeResources(t *testing.T) {
|
||||
whereSegment := selectorSegment("service.name = 'frontend'")
|
||||
emptyWhereSegment := selectorSegment("")
|
||||
metricSegment := selectorSegment("http.server.duration.count")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "traces builder query with filter",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"traces","filter":{"expression":"service.name = 'frontend'"}}}]}}`,
|
||||
expected: map[string]string{
|
||||
"traces": "builder_query/" + whereSegment,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "logs builder query without filter",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs"}}]}}`,
|
||||
expected: map[string]string{
|
||||
"logs": "builder_query/" + emptyWhereSegment,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "audit logs",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs","source":"audit"}}]}}`,
|
||||
expected: map[string]string{
|
||||
"audit-logs": "builder_query/" + emptyWhereSegment,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "metrics builder query",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"metrics","filter":{"expression":"service.name = 'frontend'"},"aggregations":[{"metricName":"http.server.duration.count"}]}}]}}`,
|
||||
expected: map[string]string{
|
||||
"metrics": "builder_query/" + metricSegment + "/" + whereSegment,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "meter metrics",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"metrics","source":"meter","aggregations":[{"metricName":"http.server.duration.count"}]}}]}}`,
|
||||
expected: map[string]string{
|
||||
"meter-metrics": "builder_query/" + metricSegment + "/" + emptyWhereSegment,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "promql",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"promql","spec":{"query":"sum(rate(foo[5m]))"}}]}}`,
|
||||
expected: map[string]string{
|
||||
"metrics": "promql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trace operator",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_trace_operator","spec":{"expression":"A => B"}}]}}`,
|
||||
expected: map[string]string{
|
||||
"traces": "builder_trace_operator",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clickhouse sql fans out to core signals",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"clickhouse_sql","spec":{"query":"SELECT 1"}}]}}`,
|
||||
expected: map[string]string{
|
||||
"logs": "clickhouse_sql",
|
||||
"traces": "clickhouse_sql",
|
||||
"metrics": "clickhouse_sql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate queries deduplicated",
|
||||
body: `{"compositeQuery":{"queries":[
|
||||
{"type":"builder_query","spec":{"signal":"traces","filter":{"expression":"service.name = 'frontend'"}}},
|
||||
{"type":"builder_query","spec":{"signal":"traces","filter":{"expression":"service.name = 'frontend'"}}},
|
||||
{"type":"builder_formula","spec":{"name":"error_rate","expression":"A / B * 100"}}
|
||||
]}}`,
|
||||
expected: map[string]string{
|
||||
"traces": "builder_query/" + whereSegment,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
refs, err := QueryRangeResources(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, refs, len(testCase.expected))
|
||||
|
||||
actual := make(map[string]string, len(refs))
|
||||
for _, ref := range refs {
|
||||
actual[ref.Resource.Kind().String()] = ref.ID
|
||||
}
|
||||
assert.Equal(t, testCase.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRangeResourcesErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{name: "empty body", body: ``},
|
||||
{name: "empty object", body: `{}`},
|
||||
{name: "empty queries", body: `{"compositeQuery":{"queries":[]}}`},
|
||||
{name: "unknown query type", body: `{"compositeQuery":{"queries":[{"type":"magic","spec":{}}]}}`},
|
||||
{name: "missing signal", body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{}}]}}`},
|
||||
{name: "unknown signal", body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"events"}}]}}`},
|
||||
{name: "metrics without aggregations", body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"metrics"}}]}}`},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
_, err := QueryRangeResources(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRangeResourcesFormulaOnly(t *testing.T) {
|
||||
refs, err := QueryRangeResources(coretypes.ExtractorContext{RequestBody: []byte(`{"compositeQuery":{"queries":[{"type":"builder_formula","spec":{"expression":"A / B"}}]}}`)})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, refs)
|
||||
}
|
||||
51
pkg/types/telemetrytypes/selector.go
Normal file
51
pkg/types/telemetrytypes/selector.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var errCodeInvalidResourceID = errors.MustNewCode("invalid_resource_id")
|
||||
|
||||
var PrefixSelector coretypes.SelectorFunc = func(_ context.Context, resource coretypes.Resource, id string, _ valuer.UUID) ([]coretypes.Selector, error) {
|
||||
if id == "" {
|
||||
return nil, errors.Newf(
|
||||
errors.TypeInvalidInput,
|
||||
errCodeInvalidResourceID,
|
||||
"resource id is required for %s",
|
||||
resource.Kind().String(),
|
||||
)
|
||||
}
|
||||
|
||||
segments := strings.Split(id, "/")
|
||||
values := make([]string, 0, len(segments)+1)
|
||||
values = append(values, id)
|
||||
for level := len(segments) - 1; level >= 1; level-- {
|
||||
values = append(values, strings.Join(segments[:level], "/")+"/*")
|
||||
}
|
||||
values = append(values, coretypes.WildCardSelectorString)
|
||||
|
||||
selectors := make([]coretypes.Selector, 0, len(values))
|
||||
for _, value := range values {
|
||||
selector, err := resource.Type().Selector(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectors = append(selectors, selector)
|
||||
}
|
||||
|
||||
return selectors, nil
|
||||
}
|
||||
|
||||
// Must stay stable: grant-time callers rely on producing the same segment for the same input.
|
||||
func selectorSegment(input string) string {
|
||||
normalized := strings.Join(strings.Fields(input), " ")
|
||||
sum := sha256.Sum256([]byte(normalized))
|
||||
return hex.EncodeToString(sum[:16])
|
||||
}
|
||||
78
pkg/types/telemetrytypes/selector_test.go
Normal file
78
pkg/types/telemetrytypes/selector_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSelectorSegment(t *testing.T) {
|
||||
assert.Len(t, selectorSegment("service.name = 'frontend'"), 32)
|
||||
assert.Equal(t,
|
||||
selectorSegment("service.name = 'frontend'"),
|
||||
selectorSegment("service.name = 'frontend'"),
|
||||
)
|
||||
assert.NotEqual(t,
|
||||
selectorSegment("service.name = 'frontend'"),
|
||||
selectorSegment("service.name = 'backend'"),
|
||||
)
|
||||
assert.Equal(t, selectorSegment(""), selectorSegment(" "))
|
||||
}
|
||||
|
||||
func TestPrefixSelector(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
orgID := valuer.GenerateUUID()
|
||||
metricSegment := selectorSegment("http.server.duration.count")
|
||||
whereSegment := selectorSegment("service.name = 'frontend'")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
id string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "two segments",
|
||||
id: "builder_query/" + metricSegment + "/" + whereSegment,
|
||||
expected: []string{
|
||||
"builder_query/" + metricSegment + "/" + whereSegment,
|
||||
"builder_query/" + metricSegment + "/*",
|
||||
"builder_query/*",
|
||||
"*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one segment",
|
||||
id: "builder_query/" + whereSegment,
|
||||
expected: []string{
|
||||
"builder_query/" + whereSegment,
|
||||
"builder_query/*",
|
||||
"*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "query type only",
|
||||
id: "promql",
|
||||
expected: []string{"promql", "*"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
selectors, err := PrefixSelector(ctx, coretypes.ResourceTelemetryResourceMetrics, testCase.id, orgID)
|
||||
require.NoError(t, err)
|
||||
|
||||
values := make([]string, len(selectors))
|
||||
for idx, selector := range selectors {
|
||||
values[idx] = selector.String()
|
||||
}
|
||||
assert.Equal(t, testCase.expected, values)
|
||||
})
|
||||
}
|
||||
|
||||
_, err := PrefixSelector(ctx, coretypes.ResourceTelemetryResourceMetrics, "", orgID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user