Compare commits

..

3 Commits

Author SHA1 Message Date
nityanandagohain
4b06b4546f Merge remote-tracking branch 'origin/issue_4361' into issue_4361 2026-04-12 22:02:12 +05:30
nityanandagohain
526b08a179 feat: 1.types and handler for ai-o11y attribute mapping 2026-04-12 17:32:30 +05:30
nityanandagohain
c7ab610bd8 feat: 1.types and handler for ai-o11y attribute mapping 2026-04-12 17:31:40 +05:30
20 changed files with 1715 additions and 567 deletions

View File

@@ -1,5 +1,199 @@
components:
schemas:
Aio11YmappingtypesCondition:
properties:
attributes:
items:
type: string
nullable: true
type: array
resource:
items:
type: string
nullable: true
type: array
type: object
Aio11YmappingtypesFieldContext:
enum:
- span_attribute
- resource
nullable: true
type: string
Aio11YmappingtypesGettableMapper:
properties:
config:
$ref: '#/components/schemas/Aio11YmappingtypesMapperConfig'
created_at:
format: date-time
type: string
created_by:
type: string
enabled:
type: boolean
field_context:
$ref: '#/components/schemas/Aio11YmappingtypesFieldContext'
group_id:
type: string
id:
type: string
name:
type: string
updated_at:
format: date-time
type: string
updated_by:
type: string
required:
- id
- group_id
- name
- field_context
- config
- enabled
- created_at
- updated_at
- created_by
- updated_by
type: object
Aio11YmappingtypesGettableMappingGroup:
properties:
category:
$ref: '#/components/schemas/Aio11YmappingtypesGroupCategory'
condition:
$ref: '#/components/schemas/Aio11YmappingtypesCondition'
created_at:
format: date-time
type: string
created_by:
type: string
enabled:
type: boolean
id:
type: string
name:
type: string
updated_at:
format: date-time
type: string
updated_by:
type: string
required:
- id
- name
- category
- condition
- enabled
- created_at
- updated_at
- created_by
- updated_by
type: object
Aio11YmappingtypesGroupCategory:
enum:
- llm
- tool
- agent
type: string
Aio11YmappingtypesListMappersResponse:
properties:
items:
items:
$ref: '#/components/schemas/Aio11YmappingtypesGettableMapper'
nullable: true
type: array
required:
- items
type: object
Aio11YmappingtypesListMappingGroupsResponse:
properties:
items:
items:
$ref: '#/components/schemas/Aio11YmappingtypesGettableMappingGroup'
nullable: true
type: array
required:
- items
type: object
Aio11YmappingtypesMapperConfig:
properties:
sources:
items:
$ref: '#/components/schemas/Aio11YmappingtypesMapperSource'
nullable: true
type: array
type: object
Aio11YmappingtypesMapperOperation:
enum:
- move
- copy
type: string
Aio11YmappingtypesMapperSource:
properties:
context:
$ref: '#/components/schemas/Aio11YmappingtypesSourceContext'
key:
type: string
operation:
$ref: '#/components/schemas/Aio11YmappingtypesMapperOperation'
priority:
type: integer
type: object
Aio11YmappingtypesPostableMapper:
properties:
config:
$ref: '#/components/schemas/Aio11YmappingtypesMapperConfig'
enabled:
type: boolean
field_context:
$ref: '#/components/schemas/Aio11YmappingtypesFieldContext'
name:
type: string
required:
- name
- field_context
- config
type: object
Aio11YmappingtypesPostableMappingGroup:
properties:
category:
$ref: '#/components/schemas/Aio11YmappingtypesGroupCategory'
condition:
$ref: '#/components/schemas/Aio11YmappingtypesCondition'
enabled:
type: boolean
name:
type: string
required:
- name
- category
- condition
type: object
Aio11YmappingtypesSourceContext:
enum:
- attribute
- resource
type: string
Aio11YmappingtypesUpdatableMapper:
properties:
config:
$ref: '#/components/schemas/Aio11YmappingtypesMapperConfig'
enabled:
nullable: true
type: boolean
field_context:
$ref: '#/components/schemas/Aio11YmappingtypesFieldContext'
type: object
Aio11YmappingtypesUpdatableMappingGroup:
properties:
condition:
$ref: '#/components/schemas/Aio11YmappingtypesCondition'
enabled:
nullable: true
type: boolean
name:
nullable: true
type: string
type: object
AuthtypesAttributeMapping:
properties:
email:
@@ -3058,6 +3252,514 @@ info:
version: ""
openapi: 3.0.3
paths:
/api/v1/ai-o11y/mapping/groups:
get:
deprecated: false
description: Returns all span attribute mapping groups for the authenticated
org.
operationId: ListMappingGroups
parameters:
- in: query
name: category
schema:
$ref: '#/components/schemas/Aio11YmappingtypesGroupCategory'
- in: query
name: enabled
schema:
nullable: true
type: boolean
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YmappingtypesListMappingGroupsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List mapping groups
tags:
- ai-o11y
post:
deprecated: false
description: Creates a new span attribute mapping group for the org.
operationId: CreateMappingGroup
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Aio11YmappingtypesPostableMappingGroup'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YmappingtypesGettableMappingGroup'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create a mapping group
tags:
- ai-o11y
/api/v1/ai-o11y/mapping/groups/{groupId}/mappers/{mapperId}:
delete:
deprecated: false
description: Hard-deletes a mapper from a mapping group.
operationId: DeleteMapper
parameters:
- in: path
name: groupId
required: true
schema:
type: string
- in: path
name: mapperId
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete a mapper
tags:
- ai-o11y
put:
deprecated: false
description: Partially updates an existing mapper's field context, config, or
enabled state.
operationId: UpdateMapper
parameters:
- in: path
name: groupId
required: true
schema:
type: string
- in: path
name: mapperId
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Aio11YmappingtypesUpdatableMapper'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YmappingtypesGettableMapper'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update a mapper
tags:
- ai-o11y
/api/v1/ai-o11y/mapping/groups/{id}:
delete:
deprecated: false
description: Hard-deletes a mapping group and cascades to all its mappers.
operationId: DeleteMappingGroup
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete a mapping group
tags:
- ai-o11y
put:
deprecated: false
description: Partially updates an existing mapping group's name, condition,
or enabled state.
operationId: UpdateMappingGroup
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Aio11YmappingtypesUpdatableMappingGroup'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YmappingtypesGettableMappingGroup'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update a mapping group
tags:
- ai-o11y
/api/v1/ai-o11y/mapping/groups/{id}/mappers:
get:
deprecated: false
description: Returns all attribute mappers belonging to a mapping group.
operationId: ListMappers
parameters:
- in: query
name: enabled
schema:
nullable: true
type: boolean
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YmappingtypesListMappersResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List mappers for a group
tags:
- ai-o11y
post:
deprecated: false
description: Adds a new attribute mapper to the specified mapping group.
operationId: CreateMapper
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Aio11YmappingtypesPostableMapper'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YmappingtypesGettableMapper'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create a mapper
tags:
- ai-o11y
/api/v1/authz/check:
post:
deprecated: false
@@ -4465,10 +5167,6 @@ paths:
name: metricName
schema:
type: string
- in: query
name: metricNamespace
schema:
type: string
- in: query
name: searchText
schema:
@@ -4554,10 +5252,6 @@ paths:
name: metricName
schema:
type: string
- in: query
name: metricNamespace
schema:
type: string
- in: query
name: searchText
schema:
@@ -8320,10 +9014,6 @@ paths:
name: metricName
schema:
type: string
- in: query
name: metricNamespace
schema:
type: string
- in: query
name: searchText
schema:
@@ -8421,10 +9111,6 @@ paths:
name: metricName
schema:
type: string
- in: query
name: metricNamespace
schema:
type: string
- in: query
name: searchText
schema:

View File

@@ -3836,11 +3836,6 @@ export type GetFieldsKeysParams = {
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
metricNamespace?: string;
/**
* @type string
* @description undefined
@@ -3895,11 +3890,6 @@ export type GetFieldsValuesParams = {
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
metricNamespace?: string;
/**
* @type string
* @description undefined
@@ -4579,11 +4569,6 @@ export type GetRuleHistoryFilterKeysParams = {
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
metricNamespace?: string;
/**
* @type string
* @description undefined
@@ -4641,11 +4626,6 @@ export type GetRuleHistoryFilterValuesParams = {
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
metricNamespace?: string;
/**
* @type string
* @description undefined

View File

@@ -0,0 +1,180 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/aio11ymappingtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addAIO11yMappingRoutes(router *mux.Router) error {
// --- Mapping Groups ---
if err := router.Handle("/api/v1/ai-o11y/mapping/groups", handler.New(
provider.authZ.ViewAccess(provider.aio11yMappingHandler.ListGroups),
handler.OpenAPIDef{
ID: "ListMappingGroups",
Tags: []string{"ai-o11y"},
Summary: "List mapping groups",
Description: "Returns all span attribute mapping groups for the authenticated org.",
Request: nil,
RequestContentType: "",
RequestQuery: new(aio11ymappingtypes.ListMappingGroupsQuery),
Response: new(aio11ymappingtypes.ListMappingGroupsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/mapping/groups", handler.New(
provider.authZ.AdminAccess(provider.aio11yMappingHandler.CreateGroup),
handler.OpenAPIDef{
ID: "CreateMappingGroup",
Tags: []string{"ai-o11y"},
Summary: "Create a mapping group",
Description: "Creates a new span attribute mapping group for the org.",
Request: new(aio11ymappingtypes.PostableMappingGroup),
RequestContentType: "application/json",
Response: new(aio11ymappingtypes.GettableMappingGroup),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/mapping/groups/{id}", handler.New(
provider.authZ.AdminAccess(provider.aio11yMappingHandler.UpdateGroup),
handler.OpenAPIDef{
ID: "UpdateMappingGroup",
Tags: []string{"ai-o11y"},
Summary: "Update a mapping group",
Description: "Partially updates an existing mapping group's name, condition, or enabled state.",
Request: new(aio11ymappingtypes.UpdatableMappingGroup),
RequestContentType: "application/json",
Response: new(aio11ymappingtypes.GettableMappingGroup),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/mapping/groups/{id}", handler.New(
provider.authZ.AdminAccess(provider.aio11yMappingHandler.DeleteGroup),
handler.OpenAPIDef{
ID: "DeleteMappingGroup",
Tags: []string{"ai-o11y"},
Summary: "Delete a mapping group",
Description: "Hard-deletes a mapping group and cascades to all its mappers.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
// --- Mappers ---
if err := router.Handle("/api/v1/ai-o11y/mapping/groups/{id}/mappers", handler.New(
provider.authZ.ViewAccess(provider.aio11yMappingHandler.ListMappers),
handler.OpenAPIDef{
ID: "ListMappers",
Tags: []string{"ai-o11y"},
Summary: "List mappers for a group",
Description: "Returns all attribute mappers belonging to a mapping group.",
Request: nil,
RequestContentType: "",
RequestQuery: new(aio11ymappingtypes.ListMappersQuery),
Response: new(aio11ymappingtypes.ListMappersResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/mapping/groups/{id}/mappers", handler.New(
provider.authZ.AdminAccess(provider.aio11yMappingHandler.CreateMapper),
handler.OpenAPIDef{
ID: "CreateMapper",
Tags: []string{"ai-o11y"},
Summary: "Create a mapper",
Description: "Adds a new attribute mapper to the specified mapping group.",
Request: new(aio11ymappingtypes.PostableMapper),
RequestContentType: "application/json",
Response: new(aio11ymappingtypes.GettableMapper),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/mapping/groups/{groupId}/mappers/{mapperId}", handler.New(
provider.authZ.AdminAccess(provider.aio11yMappingHandler.UpdateMapper),
handler.OpenAPIDef{
ID: "UpdateMapper",
Tags: []string{"ai-o11y"},
Summary: "Update a mapper",
Description: "Partially updates an existing mapper's field context, config, or enabled state.",
Request: new(aio11ymappingtypes.UpdatableMapper),
RequestContentType: "application/json",
Response: new(aio11ymappingtypes.GettableMapper),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/mapping/groups/{groupId}/mappers/{mapperId}", handler.New(
provider.authZ.AdminAccess(provider.aio11yMappingHandler.DeleteMapper),
handler.OpenAPIDef{
ID: "DeleteMapper",
Tags: []string{"ai-o11y"},
Summary: "Delete a mapper",
Description: "Hard-deletes a mapper from a mapping group.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/aio11ymapping"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
@@ -57,6 +58,7 @@ type provider struct {
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
aio11yMappingHandler aio11ymapping.Handler
}
func NewFactory(
@@ -83,6 +85,7 @@ func NewFactory(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
aio11yMappingHandler aio11ymapping.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -112,6 +115,7 @@ func NewFactory(
factoryHandler,
cloudIntegrationHandler,
ruleStateHistoryHandler,
aio11yMappingHandler,
)
})
}
@@ -143,6 +147,7 @@ func newProvider(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
aio11yMappingHandler aio11ymapping.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -172,6 +177,7 @@ func newProvider(
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
aio11yMappingHandler: aio11yMappingHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -272,6 +278,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addAIO11yMappingRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,43 @@
package aio11ymapping
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/aio11ymappingtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// Module defines the business logic for span attribute mapping groups and mappers.
type Module interface {
// Group operations
ListGroups(ctx context.Context, orgID valuer.UUID, q *aio11ymappingtypes.ListMappingGroupsQuery) ([]*aio11ymappingtypes.MappingGroup, error)
GetGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*aio11ymappingtypes.MappingGroup, error)
CreateGroup(ctx context.Context, orgID valuer.UUID, createdBy string, req *aio11ymappingtypes.PostableMappingGroup) (*aio11ymappingtypes.MappingGroup, error)
UpdateGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, req *aio11ymappingtypes.UpdatableMappingGroup) (*aio11ymappingtypes.MappingGroup, error)
DeleteGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
// Mapper operations
ListMappers(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, q *aio11ymappingtypes.ListMappersQuery) ([]*aio11ymappingtypes.Mapper, error)
GetMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) (*aio11ymappingtypes.Mapper, error)
CreateMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, createdBy string, req *aio11ymappingtypes.PostableMapper) (*aio11ymappingtypes.Mapper, error)
UpdateMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID, updatedBy string, req *aio11ymappingtypes.UpdatableMapper) (*aio11ymappingtypes.Mapper, error)
DeleteMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) error
}
// Handler defines the HTTP handler interface for mapping group and mapper endpoints.
type Handler interface {
// Group handlers
ListGroups(rw http.ResponseWriter, r *http.Request)
CreateGroup(rw http.ResponseWriter, r *http.Request)
UpdateGroup(rw http.ResponseWriter, r *http.Request)
DeleteGroup(rw http.ResponseWriter, r *http.Request)
// Mapper handlers
ListMappers(rw http.ResponseWriter, r *http.Request)
CreateMapper(rw http.ResponseWriter, r *http.Request)
UpdateMapper(rw http.ResponseWriter, r *http.Request)
DeleteMapper(rw http.ResponseWriter, r *http.Request)
}

View File

@@ -0,0 +1,356 @@
package impiaio11ymapping
import (
"context"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/aio11ymapping"
"github.com/SigNoz/signoz/pkg/types/aio11ymappingtypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
module aio11ymapping.Module
providerSettings factory.ProviderSettings
}
func NewHandler(module aio11ymapping.Module, providerSettings factory.ProviderSettings) aio11ymapping.Handler {
return &handler{module: module, providerSettings: providerSettings}
}
// ListGroups handles GET /api/v1/ai-o11y/mapping/groups.
func (h *handler) ListGroups(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
var q aio11ymappingtypes.ListMappingGroupsQuery
if err := binding.Query.BindQuery(r.URL.Query(), &q); err != nil {
render.Error(rw, err)
return
}
groups, err := h.module.ListGroups(ctx, orgID, &q)
if err != nil {
render.Error(rw, err)
return
}
items := make([]*aio11ymappingtypes.GettableMappingGroup, len(groups))
for i, g := range groups {
items[i] = aio11ymappingtypes.NewGettableMappingGroup(g)
}
render.Success(rw, http.StatusOK, &aio11ymappingtypes.ListMappingGroupsResponse{Items: items})
}
// CreateGroup handles POST /api/v1/ai-o11y/mapping/groups.
func (h *handler) CreateGroup(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
req := new(aio11ymappingtypes.PostableMappingGroup)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
group, err := h.module.CreateGroup(ctx, orgID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, aio11ymappingtypes.NewGettableMappingGroup(group))
}
// UpdateGroup handles PUT /api/v1/ai-o11y/mapping/groups/{id}.
func (h *handler) UpdateGroup(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, err := groupIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
req := new(aio11ymappingtypes.UpdatableMappingGroup)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
group, err := h.module.UpdateGroup(ctx, orgID, id, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, aio11ymappingtypes.NewGettableMappingGroup(group))
}
// DeleteGroup handles DELETE /api/v1/ai-o11y/mapping/groups/{id}.
func (h *handler) DeleteGroup(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, err := groupIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
if err := h.module.DeleteGroup(ctx, orgID, id); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
// ListMappers handles GET /api/v1/ai-o11y/mapping/groups/{id}/mappers.
func (h *handler) ListMappers(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
groupID, err := groupIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
var q aio11ymappingtypes.ListMappersQuery
if err := binding.Query.BindQuery(r.URL.Query(), &q); err != nil {
render.Error(rw, err)
return
}
mappers, err := h.module.ListMappers(ctx, orgID, groupID, &q)
if err != nil {
render.Error(rw, err)
return
}
items := make([]*aio11ymappingtypes.GettableMapper, len(mappers))
for i, m := range mappers {
items[i] = aio11ymappingtypes.NewGettableMapper(m)
}
render.Success(rw, http.StatusOK, &aio11ymappingtypes.ListMappersResponse{Items: items})
}
// CreateMapper handles POST /api/v1/ai-o11y/mapping/groups/{id}/mappers.
func (h *handler) CreateMapper(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
groupID, err := groupIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
req := new(aio11ymappingtypes.PostableMapper)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
mapper, err := h.module.CreateMapper(ctx, orgID, groupID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, aio11ymappingtypes.NewGettableMapper(mapper))
}
// UpdateMapper handles PUT /api/v1/ai-o11y/mapping/groups/{groupId}/mappers/{mapperId}.
func (h *handler) UpdateMapper(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
groupID, err := groupIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
mapperID, err := mapperIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
req := new(aio11ymappingtypes.UpdatableMapper)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
mapper, err := h.module.UpdateMapper(ctx, orgID, groupID, mapperID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, aio11ymappingtypes.NewGettableMapper(mapper))
}
// DeleteMapper handles DELETE /api/v1/ai-o11y/mapping/groups/{groupId}/mappers/{mapperId}.
func (h *handler) DeleteMapper(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
groupID, err := groupIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
mapperID, err := mapperIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
if err := h.module.DeleteMapper(ctx, orgID, groupID, mapperID); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
// groupIDFromPath extracts and validates the {id} or {groupId} path variable.
func groupIDFromPath(r *http.Request) (valuer.UUID, error) {
vars := mux.Vars(r)
raw := vars["groupId"]
if raw == "" {
raw = vars["id"]
}
if raw == "" {
return valuer.UUID{}, errors.Newf(errors.TypeInvalidInput, aio11ymappingtypes.ErrCodeMappingInvalidInput, "group id is missing from the path")
}
id, err := valuer.NewUUID(raw)
if err != nil {
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, aio11ymappingtypes.ErrCodeMappingInvalidInput, "group id is not a valid uuid")
}
return id, nil
}
// mapperIDFromPath extracts and validates the {mapperId} path variable.
func mapperIDFromPath(r *http.Request) (valuer.UUID, error) {
raw := mux.Vars(r)["mapperId"]
if raw == "" {
return valuer.UUID{}, errors.Newf(errors.TypeInvalidInput, aio11ymappingtypes.ErrCodeMappingInvalidInput, "mapper id is missing from the path")
}
id, err := valuer.NewUUID(raw)
if err != nil {
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, aio11ymappingtypes.ErrCodeMappingInvalidInput, "mapper id is not a valid uuid")
}
return id, nil
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/global/signozglobal"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/aio11ymapping"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
@@ -62,6 +63,7 @@ type Handlers struct {
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
AIO11yMappingHandler aio11ymapping.Handler
}
func NewHandlers(

View File

@@ -16,6 +16,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/modules/aio11ymapping"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
@@ -69,6 +70,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ aio11ymapping.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -288,6 +288,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RegistryHandler,
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.AIO11yMappingHandler,
),
)
}

View File

@@ -889,12 +889,7 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
// }
if fieldKeySelector.MetricContext != nil {
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
}
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
conds = append(conds, sb.And(fieldConds...))
@@ -982,12 +977,7 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
fieldConds = append(fieldConds, sb.NotLike("attr_name", "\\_\\_%"))
if fieldKeySelector.MetricContext != nil {
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
}
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
conds = append(conds, sb.And(fieldConds...))
@@ -1081,8 +1071,8 @@ func enrichWithIntrinsicMetricKeys(keys map[string][]*telemetrytypes.TelemetryFi
if selector.Signal != telemetrytypes.SignalMetrics && selector.Signal != telemetrytypes.SignalUnspecified {
continue
}
// If metric filters are provided, do not surface intrinsic metric keys.
if selector.MetricContext != nil && (selector.MetricContext.MetricName != "" || selector.MetricContext.MetricNamespace != "") {
// If a metricName is provided, dont surface intrinsic metric keys
if selector.MetricContext != nil && selector.MetricContext.MetricName != "" {
continue
}
@@ -1738,12 +1728,9 @@ func (t *telemetryMetaStore) getMetricFieldValues(ctx context.Context, fieldValu
sb.Where(sb.E("attr_datatype", fieldValueSelector.FieldDataType.TagDataType()))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
if fieldValueSelector.MetricContext != nil {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.StartUnixMilli > 0 {
sb.Where(sb.GE("last_reported_unix_milli", fieldValueSelector.StartUnixMilli))
@@ -1825,9 +1812,6 @@ func (t *telemetryMetaStore) getIntrinsicMetricFieldValues(ctx context.Context,
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.StartUnixMilli > 0 {
sb.Where(sb.GE("unix_milli", fieldValueSelector.StartUnixMilli))
@@ -1885,13 +1869,6 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
}
sb.Where(sb.NotLike("attr.1", "\\_\\_%"))
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.Value != "" {
if fieldValueSelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
sb.Where(sb.E("attr.2", fieldValueSelector.Value))

View File

@@ -320,20 +320,6 @@ func TestEnrichWithIntrinsicMetricKeys(t *testing.T) {
},
)
assert.NotContains(t, result, "metric_name")
result = enrichWithIntrinsicMetricKeys(
map[string][]*telemetrytypes.TelemetryFieldKey{},
[]*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
)
assert.NotContains(t, result, "metric_name")
}
func TestGetMetricFieldValuesIntrinsicMetricName(t *testing.T) {
@@ -406,174 +392,3 @@ func TestGetMetricFieldValuesIntrinsicBoolReturnsEmpty(t *testing.T) {
assert.Empty(t, values.BoolValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricFieldValuesAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
valueRows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_string_value", Type: "String"},
}, [][]any{{"value.a"}})
mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT attr_string_value FROM signoz_metrics.distributed_metadata WHERE attr_name = ? AND metric_name LIKE ? LIMIT ?")).
WithArgs("custom_key", "system.cpu%", 11).
WillReturnRows(valueRows)
values, complete, err := metadata.(*telemetryMetaStore).getMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Name: "custom_key",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 10,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"value.a"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricFieldValuesIntrinsicMetricNameAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
valueRows := cmock.NewRows([]cmock.ColumnType{
{Name: "metric_name", Type: "String"},
}, [][]any{{"system.cpu.utilization"}})
mock.ExpectQuery(regexp.QuoteMeta("SELECT metric_name FROM signoz_metrics.distributed_time_series_v4_1week WHERE metric_name LIKE ? GROUP BY metric_name LIMIT ?")).
WithArgs("system.cpu%", 51).
WillReturnRows(valueRows)
metadataRows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_string_value", Type: "String"},
}, [][]any{})
mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT attr_string_value FROM signoz_metrics.distributed_metadata WHERE attr_name = ? AND metric_name LIKE ? LIMIT ?")).
WithArgs("metric_name", "system.cpu%", 50).
WillReturnRows(metadataRows)
values, complete, err := metadata.(*telemetryMetaStore).getMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Name: "metric_name",
Limit: 50,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 50,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"system.cpu.utilization"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMeterSourceMetricFieldValuesAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr", Type: "Array(String)"},
}, [][]any{{[]string{"service.name", "frontend"}}})
mock.ExpectQuery(`SELECT .*distributed_samples_agg_1d.*metric_name LIKE .*`).
WithArgs("service.name", "\\_\\_%", "system.cpu%", "", 11).
WillReturnRows(rows)
values, complete, err := metadata.(*telemetryMetaStore).getMeterSourceMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Source: telemetrytypes.SourceMeter,
Name: "service.name",
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 10,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"frontend"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricsKeysAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "name", Type: "String"},
{Name: "field_context", Type: "String"},
{Name: "field_data_type", Type: "String"},
{Name: "priority", Type: "UInt8"},
}, [][]any{{"service.name", "resource", "String", 1}})
mock.ExpectQuery(`(?s)SELECT.*distributed_metadata.*metric_name LIKE.*`).
WithArgs("%service%", "\\_\\_%", "system.cpu%", 11).
WillReturnRows(rows)
keys, complete, err := metadata.(*telemetryMetaStore).getMetricsKeys(context.Background(), []*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
Name: "service",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
})
require.NoError(t, err)
assert.True(t, complete)
assert.Len(t, keys, 1)
assert.Equal(t, "service.name", keys[0].Name)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMeterSourceMetricKeysAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_name", Type: "String"},
}, [][]any{{"service.name"}})
mock.ExpectQuery(`SELECT.*distributed_samples_agg_1d.*metric_name LIKE.*`).
WithArgs("%service%", "\\_\\_%", "system.cpu%", 10).
WillReturnRows(rows)
keys, complete, err := metadata.(*telemetryMetaStore).getMeterSourceMetricKeys(context.Background(), []*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
Source: telemetrytypes.SourceMeter,
Name: "service",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
})
require.NoError(t, err)
assert.True(t, complete)
assert.Len(t, keys, 1)
assert.Equal(t, "service.name", keys[0].Name)
require.NoError(t, mock.ExpectationsWereMet())
}

View File

@@ -0,0 +1,11 @@
package aio11ymappingtypes
import "github.com/SigNoz/signoz/pkg/errors"
var (
ErrCodeMappingGroupNotFound = errors.MustNewCode("mapping_group_not_found")
ErrCodeMappingGroupAlreadyExists = errors.MustNewCode("mapping_group_already_exists")
ErrCodeMapperNotFound = errors.MustNewCode("mapper_not_found")
ErrCodeMapperAlreadyExists = errors.MustNewCode("mapper_already_exists")
ErrCodeMappingInvalidInput = errors.MustNewCode("mapping_invalid_input")
)

View File

@@ -0,0 +1,147 @@
package aio11ymappingtypes
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type GroupCategory string
const (
GroupCategoryLLM GroupCategory = "llm"
GroupCategoryTool GroupCategory = "tool"
GroupCategoryAgent GroupCategory = "agent"
)
func (GroupCategory) Enum() []any {
return []any{GroupCategoryLLM, GroupCategoryTool, GroupCategoryAgent}
}
// Condition is the trigger condition for a mapping group.
// A group runs when any of the listed attribute/resource key patterns match.
// It implements driver.Valuer and sql.Scanner for JSON text column storage.
type Condition struct {
Attributes []string `json:"attributes"`
Resource []string `json:"resource"`
}
func (c Condition) Value() (driver.Value, error) {
b, err := json.Marshal(c)
if err != nil {
return nil, err
}
return string(b), nil
}
func (c *Condition) Scan(src any) error {
var raw []byte
switch v := src.(type) {
case string:
raw = []byte(v)
case []byte:
raw = v
case nil:
*c = Condition{}
return nil
default:
return fmt.Errorf("aio11ymappingtypes: cannot scan %T into Condition", src)
}
return json.Unmarshal(raw, c)
}
// MappingGroup is the domain model for a span attribute mapping group.
// It has no serialisation concerns — use GettableMappingGroup for HTTP responses.
type MappingGroup struct {
types.TimeAuditable
types.UserAuditable
ID string
OrgID valuer.UUID
Name string
Category GroupCategory
Condition Condition
Enabled bool
}
// NewMappingGroupFromStorable converts a StorableMappingGroup to a MappingGroup.
func NewMappingGroupFromStorable(s *StorableMappingGroup) *MappingGroup {
return &MappingGroup{
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
ID: s.ID.StringValue(),
OrgID: s.OrgID,
Name: s.Name,
Category: s.Category,
Condition: s.Condition,
Enabled: s.Enabled,
}
}
// NewMappingGroupsFromStorable converts a slice of StorableMappingGroup to a slice of MappingGroup.
func NewMappingGroupsFromStorable(ss []*StorableMappingGroup) []*MappingGroup {
groups := make([]*MappingGroup, len(ss))
for i, s := range ss {
groups[i] = NewMappingGroupFromStorable(s)
}
return groups
}
// GettableMappingGroup is the HTTP response representation of a mapping group.
type GettableMappingGroup struct {
ID string `json:"id" required:"true"`
Name string `json:"name" required:"true"`
Category GroupCategory `json:"category" required:"true"`
Condition Condition `json:"condition" required:"true"`
Enabled bool `json:"enabled" required:"true"`
CreatedAt time.Time `json:"created_at" required:"true"`
UpdatedAt time.Time `json:"updated_at" required:"true"`
CreatedBy string `json:"created_by" required:"true"`
UpdatedBy string `json:"updated_by" required:"true"`
}
// NewGettableMappingGroup converts a domain MappingGroup to a GettableMappingGroup.
func NewGettableMappingGroup(g *MappingGroup) *GettableMappingGroup {
return &GettableMappingGroup{
ID: g.ID,
Name: g.Name,
Category: g.Category,
Condition: g.Condition,
Enabled: g.Enabled,
CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt,
CreatedBy: g.CreatedBy,
UpdatedBy: g.UpdatedBy,
}
}
// PostableMappingGroup is the HTTP request body for creating a mapping group.
type PostableMappingGroup struct {
Name string `json:"name" required:"true"`
Category GroupCategory `json:"category" required:"true"`
Condition Condition `json:"condition" required:"true"`
Enabled bool `json:"enabled"`
}
// UpdatableMappingGroup is the HTTP request body for updating a mapping group.
// All fields are optional; only non-nil fields are applied.
type UpdatableMappingGroup struct {
Name *string `json:"name,omitempty"`
Condition *Condition `json:"condition,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
// ListMappingGroupsQuery holds optional filter parameters for listing mapping groups.
type ListMappingGroupsQuery struct {
Category *GroupCategory `query:"category"`
Enabled *bool `query:"enabled"`
}
// ListMappingGroupsResponse is the response for listing mapping groups.
type ListMappingGroupsResponse struct {
Items []*GettableMappingGroup `json:"items" required:"true" nullable:"true"`
}

View File

@@ -0,0 +1,183 @@
package aio11ymappingtypes
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
// FieldContext is where the target attribute is written.
type FieldContext string
const (
FieldContextSpanAttribute FieldContext = "span_attribute"
FieldContextResource FieldContext = "resource"
)
func (FieldContext) Enum() []any {
return []any{FieldContextSpanAttribute, FieldContextResource}
}
// MapperOperation determines whether the source attribute is moved (deleted) or copied.
type MapperOperation string
const (
MapperOperationMove MapperOperation = "move"
MapperOperationCopy MapperOperation = "copy"
)
func (MapperOperation) Enum() []any {
return []any{MapperOperationMove, MapperOperationCopy}
}
// SourceContext indicates whether the source key is read from span attributes or resource attributes.
type SourceContext string
const (
SourceContextAttribute SourceContext = "attribute"
SourceContextResource SourceContext = "resource"
)
func (SourceContext) Enum() []any {
return []any{SourceContextAttribute, SourceContextResource}
}
// MapperSource describes one candidate source for a target attribute.
type MapperSource struct {
// Key is the span/resource attribute key to read from.
Key string `json:"key"`
// Context indicates whether to read from span attributes or resource attributes.
Context SourceContext `json:"context"`
// Operation determines whether to move or copy the source value.
Operation MapperOperation `json:"operation"`
// Priority controls the evaluation order; lower value = higher priority.
Priority int `json:"priority"`
}
// MapperConfig holds the mapping logic for a single target attribute.
// It implements driver.Valuer and sql.Scanner for JSON text column storage.
type MapperConfig struct {
Sources []MapperSource `json:"sources"`
}
func (m MapperConfig) Value() (driver.Value, error) {
b, err := json.Marshal(m)
if err != nil {
return nil, err
}
return string(b), nil
}
func (m *MapperConfig) Scan(src any) error {
var raw []byte
switch v := src.(type) {
case string:
raw = []byte(v)
case []byte:
raw = v
case nil:
*m = MapperConfig{}
return nil
default:
return fmt.Errorf("aio11ymappingtypes: cannot scan %T into MapperConfig", src)
}
return json.Unmarshal(raw, m)
}
// Mapper is the domain model for a span attribute mapper.
type Mapper struct {
types.TimeAuditable
types.UserAuditable
ID string
OrgID valuer.UUID
GroupID valuer.UUID
Name string
FieldContext FieldContext
Config MapperConfig
Enabled bool
}
// NewMapperFromStorable converts a StorableMapper to a Mapper.
func NewMapperFromStorable(s *StorableMapper) *Mapper {
return &Mapper{
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
ID: s.ID.StringValue(),
OrgID: s.OrgID,
GroupID: s.GroupID,
Name: s.Name,
FieldContext: s.FieldContext,
Config: s.Config,
Enabled: s.Enabled,
}
}
// NewMappersFromStorable converts a slice of StorableMapper to a slice of Mapper.
func NewMappersFromStorable(ss []*StorableMapper) []*Mapper {
mappers := make([]*Mapper, len(ss))
for i, s := range ss {
mappers[i] = NewMapperFromStorable(s)
}
return mappers
}
// GettableMapper is the HTTP response representation of a mapper.
type GettableMapper struct {
ID string `json:"id" required:"true"`
GroupID string `json:"group_id" required:"true"`
Name string `json:"name" required:"true"`
FieldContext FieldContext `json:"field_context" required:"true"`
Config MapperConfig `json:"config" required:"true"`
Enabled bool `json:"enabled" required:"true"`
CreatedAt time.Time `json:"created_at" required:"true"`
UpdatedAt time.Time `json:"updated_at" required:"true"`
CreatedBy string `json:"created_by" required:"true"`
UpdatedBy string `json:"updated_by" required:"true"`
}
// NewGettableMapper converts a domain Mapper to a GettableMapper.
func NewGettableMapper(m *Mapper) *GettableMapper {
return &GettableMapper{
ID: m.ID,
GroupID: m.GroupID.StringValue(),
Name: m.Name,
FieldContext: m.FieldContext,
Config: m.Config,
Enabled: m.Enabled,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
CreatedBy: m.CreatedBy,
UpdatedBy: m.UpdatedBy,
}
}
// PostableMapper is the HTTP request body for creating a mapper.
type PostableMapper struct {
Name string `json:"name" required:"true"`
FieldContext FieldContext `json:"field_context" required:"true"`
Config MapperConfig `json:"config" required:"true"`
Enabled bool `json:"enabled"`
}
// UpdatableMapper is the HTTP request body for updating a mapper.
// All fields are optional; only non-nil fields are applied.
type UpdatableMapper struct {
FieldContext *FieldContext `json:"field_context,omitempty"`
Config *MapperConfig `json:"config,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
// ListMappersQuery holds optional filter parameters for listing mappers in a group.
type ListMappersQuery struct {
Enabled *bool `query:"enabled"`
}
// ListMappersResponse is the response for listing mappers within a group.
type ListMappersResponse struct {
Items []*GettableMapper `json:"items" required:"true" nullable:"true"`
}

View File

@@ -0,0 +1,38 @@
package aio11ymappingtypes
import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// StorableMappingGroup is the bun/DB representation of a span attribute mapping group.
type StorableMappingGroup struct {
bun.BaseModel `bun:"table:span_attribute_mapping_group,alias:span_attribute_mapping_group"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
Name string `bun:"name,type:text,notnull"`
Category GroupCategory `bun:"category,type:text,notnull"`
Condition Condition `bun:"condition,type:text,notnull"`
Enabled bool `bun:"enabled,notnull,default:true"`
}
// StorableMapper is the bun/DB representation of a span attribute mapper.
type StorableMapper struct {
bun.BaseModel `bun:"table:span_mapping_attribute,alias:span_mapping_attribute"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
GroupID valuer.UUID `bun:"group_id,type:text,notnull"`
Name string `bun:"name,type:text,notnull"`
FieldContext FieldContext `bun:"field_context,type:text,notnull"`
Config MapperConfig `bun:"config,type:text,notnull"`
Enabled bool `bun:"enabled,notnull,default:true"`
}

View File

@@ -0,0 +1,23 @@
package aio11ymappingtypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
// Group operations
ListGroups(ctx context.Context, orgID valuer.UUID, q *ListMappingGroupsQuery) ([]*StorableMappingGroup, error)
GetGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*StorableMappingGroup, error)
CreateGroup(ctx context.Context, group *StorableMappingGroup) error
UpdateGroup(ctx context.Context, group *StorableMappingGroup) error
DeleteGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
// Mapper operations
ListMappers(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, q *ListMappersQuery) ([]*StorableMapper, error)
GetMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) (*StorableMapper, error)
CreateMapper(ctx context.Context, mapper *StorableMapper) error
UpdateMapper(ctx context.Context, mapper *StorableMapper) error
DeleteMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) error
}

View File

@@ -268,8 +268,7 @@ func (t *TelemetryFieldValues) NumValues() int {
}
type MetricContext struct {
MetricName string `json:"metricName"`
MetricNamespace string `json:"metricNamespace,omitempty"`
MetricName string `json:"metricName"`
}
type FieldKeySelector struct {
@@ -298,16 +297,15 @@ type GettableFieldKeys struct {
}
type PostableFieldKeysParams struct {
Signal Signal `query:"signal"`
Source Source `query:"source"`
Limit int `query:"limit"`
StartUnixMilli int64 `query:"startUnixMilli"`
EndUnixMilli int64 `query:"endUnixMilli"`
FieldContext FieldContext `query:"fieldContext"`
FieldDataType FieldDataType `query:"fieldDataType"`
MetricName string `query:"metricName"`
MetricNamespace string `query:"metricNamespace"`
SearchText string `query:"searchText"`
Signal Signal `query:"signal"`
Source Source `query:"source"`
Limit int `query:"limit"`
StartUnixMilli int64 `query:"startUnixMilli"`
EndUnixMilli int64 `query:"endUnixMilli"`
FieldContext FieldContext `query:"fieldContext"`
FieldDataType FieldDataType `query:"fieldDataType"`
MetricName string `query:"metricName"`
SearchText string `query:"searchText"`
}
type GettableFieldValues struct {
@@ -346,10 +344,9 @@ func NewFieldKeySelectorFromPostableFieldKeysParams(params PostableFieldKeysPara
req.Limit = 1000
}
if params.MetricName != "" || params.MetricNamespace != "" {
if params.MetricName != "" {
req.MetricContext = &MetricContext{
MetricName: params.MetricName,
MetricNamespace: params.MetricNamespace,
MetricName: params.MetricName,
}
}

View File

@@ -394,20 +394,3 @@ func TestNormalize(t *testing.T) {
})
}
}
func TestNewFieldKeySelectorFromPostableFieldKeysParamsMetricNamespace(t *testing.T) {
selector := NewFieldKeySelectorFromPostableFieldKeysParams(PostableFieldKeysParams{
Signal: SignalMetrics,
MetricNamespace: "system.cpu",
})
if selector.MetricContext == nil {
t.Fatalf("expected metric context to be set")
}
if selector.MetricContext.MetricNamespace != "system.cpu" {
t.Fatalf("expected metric namespace to be propagated, got %q", selector.MetricContext.MetricNamespace)
}
if selector.MetricContext.MetricName != "" {
t.Fatalf("expected metric name to remain empty, got %q", selector.MetricContext.MetricName)
}
}

View File

@@ -658,211 +658,3 @@ def test_non_existent_metrics_returns_404(
get_error_message(response.json())
== "could not find the metric whatevergoennnsgoeshere"
)
# Verify /api/v1/fields/values filters label values by metricNamespace prefix.
# Inserts metrics under ns.a and ns.b, then asserts a specific prefix returns
# only matching values while a common prefix returns both.
def test_metric_namespace_values_filtering(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="ns.a.requests_total",
labels={"service": "svc-a"},
timestamp=now - timedelta(minutes=2),
value=10.0,
),
Metrics(
metric_name="ns.b.requests_total",
labels={"service": "svc-b"},
timestamp=now - timedelta(minutes=2),
value=20.0,
),
]
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Specific prefix: metricNamespace=ns.a should return only svc-a
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "service",
"searchText": "",
"metricNamespace": "ns.a",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "svc-a" in values
assert "svc-b" not in values
# Common prefix: metricNamespace=ns should return both
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "service",
"searchText": "",
"metricNamespace": "ns",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "svc-a" in values
assert "svc-b" in values
# Verify /api/v1/fields/values with name=metric_name filters metric names by
# metricNamespace prefix. A specific prefix returns only its metric names;
# a common prefix returns metric names from all matching namespaces.
def test_metric_namespace_metric_name_values_filtering(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="ns.a.cpu.utilization",
labels={"host": "host-a"},
timestamp=now - timedelta(minutes=2),
value=50.0,
),
Metrics(
metric_name="ns.b.cpu.utilization",
labels={"host": "host-b"},
timestamp=now - timedelta(minutes=2),
value=60.0,
),
]
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Specific prefix: metricNamespace=ns.a should return only ns.a.* metric names
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "metric_name",
"searchText": "",
"metricNamespace": "ns.a",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "ns.a.cpu.utilization" in values
assert "ns.b.cpu.utilization" not in values
# Common prefix: metricNamespace=ns should return both
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "metric_name",
"searchText": "",
"metricNamespace": "ns",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "ns.a.cpu.utilization" in values
assert "ns.b.cpu.utilization" in values
# Verify /api/v1/fields/keys filters attribute keys by metricNamespace prefix.
# Metrics under ns.a and ns.b carry distinct labels; a specific prefix returns
# only its keys while a common prefix returns keys from both namespaces.
def test_metric_namespace_keys_filtering(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="ns.a.cpu.utilization",
labels={"a_only_label": "val-a"},
timestamp=now - timedelta(minutes=2),
value=10.0,
),
Metrics(
metric_name="ns.b.cpu.utilization",
labels={"b_only_label": "val-b"},
timestamp=now - timedelta(minutes=2),
value=20.0,
),
]
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Specific prefix: metricNamespace=ns.a should return only a_only_label
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/keys"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"searchText": "label",
"metricNamespace": "ns.a",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
keys = response.json()["data"]["keys"]
assert "a_only_label" in keys
assert "b_only_label" not in keys
# Common prefix: metricNamespace=ns should return both keys
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/keys"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"searchText": "label",
"metricNamespace": "ns",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
keys = response.json()["data"]["keys"]
assert "a_only_label" in keys
assert "b_only_label" in keys

View File

@@ -108,81 +108,3 @@ def test_list_meter_metric_names(
assert (
metric_name in metric_names
), f"Expected {metric_name} in metric names, got: {metric_names}"
# Verify /api/v1/fields/values with source=meter filters label values by metricNamespace
# prefix. Inserts meter-source metrics under ns.a and ns.b, then asserts a specific
# prefix returns only matching values while a common prefix returns both.
def test_metric_namespace_meter_values_filtering(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_meter_samples: Callable[[List[MeterSample]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
samples_a = make_meter_samples(
"meter.ns.a.cost",
{"service": "billing-a"},
now,
count=5,
base_value=10.0,
temporality="Delta",
type_="Sum",
is_monotonic=True,
)
samples_b = make_meter_samples(
"meter.ns.b.cost",
{"service": "billing-b"},
now,
count=5,
base_value=20.0,
temporality="Delta",
type_="Sum",
is_monotonic=True,
)
insert_meter_samples(samples_a + samples_b)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Specific prefix: metricNamespace=meter.ns.a should return only billing-a
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"source": "meter",
"name": "service",
"searchText": "",
"metricNamespace": "meter.ns.a",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "billing-a" in values
assert "billing-b" not in values
# Common prefix: metricNamespace=meter.ns should return both
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"source": "meter",
"name": "service",
"searchText": "",
"metricNamespace": "meter.ns",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "billing-a" in values
assert "billing-b" in values