Compare commits

...

6 Commits

Author SHA1 Message Date
Vikrant Gupta
19712c3579 feat(authdomain): support custom roles in SSO group mapping (#11858)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* feat(authdomain): support custom roles in SSO group mapping

Role mappings now reference SigNoz roles by name (custom or managed)
instead of the legacy ADMIN/EDITOR/VIEWER enum. Legacy values sent by a
client are normalized to their managed names (signoz-admin/editor/viewer),
and a migration normalizes existing stored mappings.

- normalize legacy role names on unmarshal; resolve all matched roles on
  SSO callback (union of matched groups, else default, else viewer)
- validate referenced roles exist on auth domain create/update
- block deleting a role referenced by any auth domain mapping, naming the
  referencing domains (OnBeforeRoleDelete now also passes the role name)
- migration 096 to rewrite legacy names in existing auth_domain mappings

* refactor(sqlmigration): decode SSO role mapping into a typed struct

Decode the roleMapping object into a small frozen struct instead of
poking at json.RawMessage per field. The top-level config stays a raw
map so the other config fields are preserved untouched.

* fix(authdomain): validate SSO role attribute claim against existing roles

When the role mapping uses the IDP role attribute, the claimed role is
assigned only if it exists in the org (resolved case-insensitively, with
legacy ADMIN/EDITOR/VIEWER mapped to their managed names); otherwise the
mapping falls through to group mappings and the default role. This lets
custom roles be assigned via the attribute and avoids failing login on an
unknown claim.

Also normalize legacy role names case-insensitively and fold the
single-use managed-role lookup into NormalizeRoleName.
2026-06-26 14:26:43 +00:00
ElyesLaaribi
e31683be11 fix(metrics-explorer): resolve pagination overlap in table (#11812)
* fix: resolve pagination overlap in metrics explorer table

* fix: add padding-bottom to metrics table container per review feedback
2026-06-26 13:59:04 +00:00
Tushar Vats
1700ad06e6 chore: refactor error response + normalize ClickHouse telemetrystore errors (#11740)
* chore: mark required and nullable in json tag, renamed methods, and added more functionality

* fix: unit test

* fix: cast clickhouse exceptions

* fix: go mod tidy

* fix: telemetrystore now returns explicit base errors

* fix: typo

* fix: added all changes

* fix: added nil check

* fix: update test files

* fix: addressed comments

* fix: change errors and suggestions to be non-nullable
2026-06-26 13:44:38 +00:00
Vikrant Gupta
ee3b45b80d feat(statsreporter): track role counts (#11866)
Register the authz provider as a stats collector and emit per-org
role counts (role.count, role.custom.count, role.managed.count) using
the same dynamic key pattern as authdomain.
2026-06-26 13:24:59 +00:00
Vinicius Lourenço
4771e30c03 fix(keyboard-shortcut): monaco-editor should not trigger shortcut (#11848) 2026-06-26 12:22:03 +00:00
Pandey
e933fa74c7 feat(querybuilder): type untyped value fields in v5 openapi schema (#11850)
* feat(querybuilder): type untyped value fields in v5 openapi schema

The query-builder v5 types FunctionArg, VariableItem and Label carry a
`Value any` field that the reflector rendered as an untyped `{}` in the
OpenAPI spec, and QueryEnvelope left an untyped `spec: {}` on its oneOf
base. Add PrepareJSONSchema methods that document the real wire contract:

- FunctionArg.value  -> oneOf [number, string]
- VariableItem.value -> oneOf [string, number, boolean, array<scalar>]
- Label.value        -> oneOf [string, number, boolean]
- QueryEnvelope      -> drop the duplicate base properties so only the
                        typed oneOf-of-$ref variants remain

The Go fields stay `any`; this only shapes the generated schema, so the
runtime decode paths and the existing wire format are unchanged.

* chore(api): regenerate openapi spec and frontend client

Regenerates docs/api/openapi.yml (`generate openapi`) and the orval
frontend client (`generate:api`) for the v5 schema typing. The four
value/spec fields are now typed instead of untyped `{}`, and the
QueryEnvelope DTO collapses from a union of `& { spec?: unknown }`
intersections into a clean discriminated union.

* feat(querybuilder): expose builder_join variant and nullable variable value

- Uncomment the QueryEnvelope builder_join variant so the discriminated
  union includes joins (the runtime UnmarshalJSON already decodes them),
  and type QueryBuilderJoin.aggregations (a []any holding trace/log/metric
  aggregations) as a oneOf of the concrete aggregation schemas instead of
  an untyped {}.
- Mark VariableItem.value nullable: the frontend sends null for a dynamic
  variable whose "ALL" option is selected.

Go field types are unchanged; this only shapes the generated schema.

* chore(api): regenerate spec and frontend client for join + nullable value

Regenerates docs/api/openapi.yml and the orval frontend client. The
QueryEnvelope union gains the builder_join variant (with typed
aggregations), and VariableItem.value is now string | number | boolean |
array | null.

* refactor(querybuilder): extract join aggregations into a named JoinAggregation

The previous inline `items.oneOf` on QueryBuilderJoin.aggregations isn't
mappable by code generators — tfplugingen-openapi rejects a oneOf buried
inline in array items ("schema composition is currently not supported"),
and skaff's detectOneOfRewrites only rewrites top-level component schemas.

Introduce a named JoinAggregation element type exposing the trace/log/metric
shapes via JSONSchemaOneOf, so the schema becomes a named oneOf-of-$ref
component (which generators flatten into one object with three optional
sub-objects) instead of an inline union. The runtime value stays opaque and
the wire shape is unchanged via transparent Marshal/UnmarshalJSON.

* chore(api): regenerate spec and frontend client for JoinAggregation

QueryBuilderJoin.aggregations items now reference the named
Querybuildertypesv5JoinAggregation oneOf component.

* feat(querybuilder): add an OpenAPI discriminator to the QueryEnvelope union

Collapse the three signal-specific builder_query schema variants into a single
queryEnvelopeBuilder whose aggregations are a trace/log/metric union
(BuilderAggregation), so `type` maps 1:1 to one variant. Tag QueryEnvelope with
x-signoz-discriminator (propertyName: type); attachDiscriminators promotes it to
a real OpenAPI 3 discriminator so oapi-codegen emits ValueByDiscriminator and
generated clients can dispatch the union mechanically.

Runtime decoding is unchanged — UnmarshalJSON still dispatches builder queries
by signal. The generated frontend client and its dashboard-v2 consumers need a
follow-up to adopt the discriminated union.

* chore(api): regenerate openapi spec for the QueryEnvelope discriminator

Frontend client regeneration is deferred to the follow-up that adapts the
dashboard-v2 query consumers to the discriminated union.

* chore(api): regenerate frontend client for the QueryEnvelope discriminator

Matches the discriminator added in 120de27c8 and the openapi spec in
4d00c0f09: QueryEnvelopeDTO is now a discriminated union keyed on `type`,
with the builder variant collapsed and BuilderAggregation typed.

WIP: the dashboard-v2 queryV5 consumers (buildQueryRangeRequest,
persesQueryAdapters, prepareScalarTables) don't yet compile against the
discriminated union, so a whole-project `tsgo --noEmit` fails until the
follow-up commit adapts them. Committed with --no-verify intentionally.

* feat(querybuilder): make the QueryEnvelope discriminator generator-friendly

Two fixes so oapi-codegen/skaff can dispatch the QueryEnvelope union:
- Pin each variant's `type` to a plain-string enum (it was a $ref to the typed
  QueryType enum, which made oapi-codegen's `v.Type = "builder_query"`
  assignment fail to compile).
- Replace the builder variant's aggregation union with a signal-discriminated
  builderQuerySpec — a oneOf of QueryBuilderQuery[Trace|Log|Metric] keyed on the
  existing (already plain-string-pinned) `signal` field. The three aggregation
  structs stay separate, mirroring dashboardtypes.BuilderQuerySpec.

builder_join's aggregations stay a discriminator-less oneOf (JoinAggregation)
for now — deferred, since a join has no signal to discriminate on.

* chore(api): regenerate openapi spec for the QueryEnvelope discriminator fix

QueryEnvelope now has a plain-string `type` discriminator; builder_query nests a
signal-discriminated BuilderQuerySpec. Frontend client regeneration is deferred
to the single FE pass.

* chore(api): regenerate frontend client for the QueryEnvelope discriminator fix

Matches the plain-string two-level discriminator (cf5b2556e / openapi
1da75be19): QueryEnvelopeDTO discriminates on `type`; builder_query nests a
signal-discriminated BuilderQuerySpecDTO over the three QueryBuilderQuery[T].

WIP: the dashboard-v2 queryV5 consumers (buildQueryRangeRequest,
persesQueryAdapters, prepareScalarTables) still don't compile against the
discriminated union, so a whole-project `tsgo --noEmit` fails until the single
FE pass adapts them. Committed with --no-verify intentionally.

* fix(querybuilder): make QueryEnvelope discriminator `type` required so apitypes compile

The plain-string pinning was the wrong lever — the blocker is the pointer, not
the enum's underlying type. An optional discriminator field renders as `*T`, and
oapi-codegen's From<Variant> assigns the discriminator string literal directly,
which only compiles on a non-pointer field. Mark `type` required:"true" on each
variant (renders non-pointer, like dashboardtypes_layout's required `kind`), and
drop the pinQueryType plain-string override. The QueryType enum is unchanged.

* chore(api): regenerate openapi spec for the required `type` discriminator

QueryEnvelope variants now mark `type` required so the generated apitypes
compile. Frontend client regeneration follows.

* chore(api): regenerate frontend client for the required `type` discriminator

Matches the required-`type` fix (65afd890f / openapi b8d528666): QueryEnvelope
variants mark `type` required so the apitypes compile.

WIP: the dashboard-v2 queryV5 consumers still don't compile against the
discriminated union; a whole-project `tsgo --noEmit` fails until the single FE
pass adapts them. Committed with --no-verify intentionally.

* chore(querybuilder): defer builder_join until it has a proper aggregation discriminator

The join aggregation oneOf (trace/log/metric) has no discriminator — trace and
log aggregations are byte-identical, and a join carries no `signal` to dispatch
on (unlike a builder query) — so code generators can't map it. Comment out
JoinAggregation (with a TODO for when full join support lands), revert
QueryBuilderJoin.Aggregations to []any, and drop the builder_join variant from
QueryEnvelope's discriminated union (oneOf + `type` mapping). Runtime decoding of
builder_join is unchanged.

* chore(api): regenerate openapi spec — builder_join deferred out of the union

* chore(api): regenerate frontend client — builder_join deferred

Matches ee6228946 / openapi ac1255f7b: the builder_join variant (and its
JoinAggregation/QueryBuilderJoin DTOs) are dropped from QueryEnvelopeDTO while
joins are deferred.

WIP: the dashboard-v2 queryV5 consumers still don't compile against the
discriminated union; a whole-project `tsgo --noEmit` fails until the single FE
pass adapts them. Committed with --no-verify intentionally.

* fix(dashboards-v2): adapt queryV5 consumers to the discriminated QueryEnvelope union

The generated QueryEnvelopeDTO is now a discriminated union — orval splits `type`
into per-variant enums — so comparing/constructing against the shared
QueryTypeDTO no longer type-checks. Route the type/spec logic through the
hand-rolled QueryEnvelope model (plain-string `type`, typed `spec`) and cast to
the generated DTO at the wire boundary (reusing the existing toMapperEnvelopes
bridge). No behavior change; the queryV5 tests pass.

* refactor(dashboards-v2): compare envelope discriminator via generated enums

Replace the string-literal `type` checks with the generated per-variant
discriminator enums (Querybuildertypesv5QueryEnvelope{Builder,PromQL,ClickHouseSQL}DTOType).
They compare directly against `envelope.type` (no cast) and narrow the union,
so no magic strings and no hand-rolled QueryEnvelope routing for the type check.

* refactor(dashboards-v2): cast only the envelope spec in toQueryEnvelopes

plugin.spec is the un-narrowed plugin-spec union, so the construction needs to
pick out the specific spec — but casting the whole array `as unknown as
QueryEnvelopeDTO[]` also discarded the type-check on the `type` discriminator.
Cast just `spec` to the variant spec (single `as`, mirroring the CompositeQuery
case), keeping `type` and the array type-checked against QueryEnvelopeDTO.

* refactor(dashboards-v2): drop the as-unknown-as casts from queryV5 consumers

Discriminator narrowing makes envelope.spec typed, so the double casts I added
when adapting these files reduce to single `as` or none:
- extractClickhouseQueryNames: cast-free (ClickHouseQueryDTO has `name`).
- withBarStepInterval: read spec.stepInterval directly; the rebuilt spec keeps a
  single `as BuilderQuerySpecDTO` (spreading the optional union drops required
  `signal`).
- withPagination: single `as` on the rebuilt spec, same reason.
- extractAggregationsPerQuery: single `as` only on the heterogeneous aggregations.
- hasRunnableQueries: single `as QuerySpecView` (its boolean .filter doesn't
  narrow, and the view exposes signal-as-string + metricName).

No `as unknown as` remain in the files this PR touched.

* refactor(qbv5): inline discriminator helpers, trim schema comments

Inline the x-signoz-discriminator construction directly into the
builderQuerySpec and QueryEnvelope PrepareJSONSchema methods, dropping the
signozDiscriminatorKey const and the schemaRef/markDiscriminator helpers.
Trim verbose PrepareJSONSchema doc comments to 1-2 lines each.

Pure refactor: generated openapi.yml is unchanged.

* fix(qbv5): make variable value schema non-nullable

The ALL selection of a dynamic variable sends the marker string __all__,
not null, so the generated value schema should not allow null. Regenerate
the OpenAPI spec and frontend clients to drop the nullable type.
2026-06-26 10:09:00 +00:00
51 changed files with 1174 additions and 343 deletions

View File

@@ -3755,10 +3755,16 @@ components:
type:
type: string
url:
nullable: true
type: string
required:
- type
- code
- message
- url
- errors
- retry
- suggestions
type: object
ErrorsResponseerroradditional:
properties:
@@ -3768,11 +3774,17 @@ components:
items:
type: string
type: array
required:
- message
- suggestions
type: object
ErrorsResponseretryjson:
nullable: true
properties:
delay:
$ref: '#/components/schemas/TimeDuration'
required:
- delay
type: object
FactoryResponse:
properties:
@@ -5701,6 +5713,18 @@ components:
format: double
type: number
type: object
Querybuildertypesv5BuilderQuerySpec:
discriminator:
mapping:
logs: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation'
metrics: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation'
traces: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
propertyName: signal
oneOf:
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation'
- $ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation'
type: object
Querybuildertypesv5ClickHouseQuery:
properties:
disabled:
@@ -5800,7 +5824,10 @@ components:
properties:
name:
type: string
value: {}
value:
oneOf:
- type: number
- type: string
type: object
Querybuildertypesv5FunctionName:
enum:
@@ -5849,7 +5876,11 @@ components:
properties:
key:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
value: {}
value:
oneOf:
- type: string
- type: number
- type: boolean
type: object
Querybuildertypesv5LimitBy:
properties:
@@ -6172,39 +6203,29 @@ components:
type: array
type: object
Querybuildertypesv5QueryEnvelope:
discriminator:
mapping:
builder_formula: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
builder_query: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
builder_trace_operator: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
clickhouse_sql: '#/components/schemas/Querybuildertypesv5QueryEnvelopeClickHouseSQL'
promql: '#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL'
propertyName: type
oneOf:
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderTrace'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderLog'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderMetric'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeClickHouseSQL'
properties:
spec: {}
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeBuilderLog:
Querybuildertypesv5QueryEnvelopeBuilder:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeBuilderMetric:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
type: object
Querybuildertypesv5QueryEnvelopeBuilderTrace:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
$ref: '#/components/schemas/Querybuildertypesv5BuilderQuerySpec'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
required:
- type
type: object
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
properties:
@@ -6212,6 +6233,8 @@ components:
$ref: '#/components/schemas/Querybuildertypesv5ClickHouseQuery'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
required:
- type
type: object
Querybuildertypesv5QueryEnvelopeFormula:
properties:
@@ -6219,6 +6242,8 @@ components:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderFormula'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
required:
- type
type: object
Querybuildertypesv5QueryEnvelopePromQL:
properties:
@@ -6226,6 +6251,8 @@ components:
$ref: '#/components/schemas/Querybuildertypesv5PromQuery'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
required:
- type
type: object
Querybuildertypesv5QueryEnvelopeTraceOperator:
properties:
@@ -6233,6 +6260,8 @@ components:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderTraceOperator'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
required:
- type
type: object
Querybuildertypesv5QueryRangeRequest:
description: Request body for the v5 query range endpoint. Supports builder
@@ -6436,7 +6465,17 @@ components:
properties:
type:
$ref: '#/components/schemas/Querybuildertypesv5VariableType'
value: {}
value:
oneOf:
- type: string
- type: number
- type: boolean
- items:
oneOf:
- type: string
- type: number
- type: boolean
type: array
type: object
Querybuildertypesv5VariableType:
enum:

View File

@@ -36,6 +36,55 @@ var (
> 💡 **Note**: Error codes must match the regex `^[a-z_]+$` otherwise the code will panic.
### Message
The primary, human-readable summary of what went wrong, set when the error is created via `errors.New` / `errors.Newf`. Note there are two distinct `message` fields in the response: this top-level one states the overall failure, while each entry under [Additional](#additional) carries its own [message](#message-1) explaining one specific facet of it.
### Url
An optional link to documentation that explains the error in more depth, set with `WithUrl`. It is left empty when the error has no associated doc.
```go
return errors.New(errors.TypeInvalidInput, CodeBadThing, "bad thing").
WithUrl("https://signoz.io/docs/...")
```
### Additional
`errors` is a list of supplementary details that explain the top-level `message`. Each entry has its own `message` and `suggestions`, so a single error can surface several distinct problems individually. Attach details with `WithAdditional` (message only) or `WithSuggestiveAdditional` (message plus the suggestions that belong to it):
#### Message
A single, self-contained sentence describing one specific facet of the error (e.g. ``field `filed` not found``), distinct from the top-level [Message](#message). Prefer one detail per distinct problem over concatenating several into one message.
#### Suggestions
The suggestions tied to that specific detail — typically a ``did you mean: `x` `` correction for the value the detail is about. These are distinct from the error-wide [Suggestions](#suggestions) below: detail-scoped suggestions never leak into the top-level list.
```go
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown field %q", field).
WithAdditional("field `field` not found")
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown field %q", field).
WithSuggestiveAdditional("field `filed` not found", "did you mean: `field`")
```
### Retry
Carries the `delay` the client should wait before retrying, set with `WithRetryAfter`. It is `null` when the error is not retryable.
```go
return errors.NewTimeoutf(CodeSlow, "upstream timed out").
WithRetryAfter(5 * time.Second)
```
### Suggestions
`WithSuggestions` sets the error-wide `suggestions` list — hints about the error as a whole (e.g. "narrow the time range window"), as opposed to suggestions tied to a single detail. Prefer the builders in [pkg/errors/suggestions.go](/pkg/errors/suggestions.go) over hand-writing the strings so the phrasing stays consistent:
- `NewSuggestionsOnLevenshteinDistance(invalidInput, noun, validInputs)` — returns a ``did you mean: `x` `` correction (when a close typo match exists) followed by the valid-references list.
- `NewValidReferences(noun, values...)` — formats a capped list as ``valid <noun> are `a`, `b` `` (e.g. `"valid fields are"`, `"valid keys are"`). Returns `""` for an empty set.
- `NewSuggestionsFromFunc(produce)` — wraps a caller-computed correction string as a one-element ``did you mean: `x` `` slice (or nil when it returns `""`), for callers with their own matching strategy.
`noun` names the kind of value being suggested. Use one of the exported `Noun*` constants (`errors.NounFields`, `errors.NounKeys`, `errors.NounServices`, …) so the wording stays uniform across the codebase.
```go
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown field %q", field).
WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field, errors.NounFields, validFields)...)
```
## Show me some examples
### Using the error

View File

@@ -143,6 +143,10 @@ func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*autht
return provider.pkgAuthzService.List(ctx, orgID)
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
return provider.pkgAuthzService.Collect(ctx, orgID)
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
}
@@ -370,7 +374,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
for _, cb := range provider.onBeforeRoleDelete {
if err := cb(ctx, orgID, id); err != nil {
if err := cb(ctx, orgID, id, role.Name); err != nil {
return err
}
}

View File

@@ -2178,16 +2178,21 @@ export interface ErrorsResponseerroradditionalDTO {
/**
* @type string
*/
message?: string;
message: string;
/**
* @type array
*/
suggestions?: string[];
suggestions: string[];
}
export interface ErrorsResponseretryjsonDTO {
delay?: TimeDurationDTO;
}
export type ErrorsResponseretryjsonDTOAnyOf = {
delay: TimeDurationDTO;
};
/**
* @nullable
*/
export type ErrorsResponseretryjsonDTO = ErrorsResponseretryjsonDTOAnyOf | null;
export interface ErrorsJSONDTO {
/**
@@ -2197,24 +2202,24 @@ export interface ErrorsJSONDTO {
/**
* @type array
*/
errors?: ErrorsResponseerroradditionalDTO[];
errors: ErrorsResponseerroradditionalDTO[];
/**
* @type string
*/
message: string;
retry?: ErrorsResponseretryjsonDTO;
retry: ErrorsResponseretryjsonDTO | null;
/**
* @type array
*/
suggestions?: string[];
suggestions: string[];
/**
* @type string
*/
type?: string;
type: string;
/**
* @type string
* @type string,null
*/
url?: string;
url: string | null;
}
export interface AuthtypesOrgSessionContextDTO {
@@ -3427,12 +3432,14 @@ export interface Querybuildertypesv5FilterDTO {
expression?: string;
}
export type Querybuildertypesv5FunctionArgDTOValue = number | string;
export interface Querybuildertypesv5FunctionArgDTO {
/**
* @type string
*/
name?: string;
value?: unknown;
value?: Querybuildertypesv5FunctionArgDTOValue;
}
export enum Querybuildertypesv5FunctionNameDTO {
@@ -4265,26 +4272,21 @@ export interface DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDa
export enum DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQueryDTOKind {
'signoz/CompositeQuery' = 'signoz/CompositeQuery',
}
export enum Querybuildertypesv5QueryTypeDTO {
export type Querybuildertypesv5BuilderQuerySpecDTO =
| Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO
| Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregationDTO
| Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTO;
export enum Querybuildertypesv5QueryEnvelopeBuilderDTOType {
builder_query = 'builder_query',
builder_formula = 'builder_formula',
builder_trace_operator = 'builder_trace_operator',
clickhouse_sql = 'clickhouse_sql',
promql = 'promql',
}
export interface Querybuildertypesv5QueryEnvelopeBuilderTraceDTO {
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO;
type?: Querybuildertypesv5QueryTypeDTO;
}
export interface Querybuildertypesv5QueryEnvelopeBuilderLogDTO {
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregationDTO;
type?: Querybuildertypesv5QueryTypeDTO;
}
export interface Querybuildertypesv5QueryEnvelopeBuilderMetricDTO {
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTO;
type?: Querybuildertypesv5QueryTypeDTO;
export interface Querybuildertypesv5QueryEnvelopeBuilderDTO {
spec?: Querybuildertypesv5BuilderQuerySpecDTO;
/**
* @type string
* @enum builder_query
*/
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType;
}
export interface Querybuildertypesv5QueryBuilderFormulaDTO {
@@ -4319,9 +4321,16 @@ export interface Querybuildertypesv5QueryBuilderFormulaDTO {
order?: Querybuildertypesv5OrderByDTO[];
}
export enum Querybuildertypesv5QueryEnvelopeFormulaDTOType {
builder_formula = 'builder_formula',
}
export interface Querybuildertypesv5QueryEnvelopeFormulaDTO {
spec?: Querybuildertypesv5QueryBuilderFormulaDTO;
type?: Querybuildertypesv5QueryTypeDTO;
/**
* @type string
* @enum builder_formula
*/
type: Querybuildertypesv5QueryEnvelopeFormulaDTOType;
}
export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
@@ -4382,9 +4391,16 @@ export interface Querybuildertypesv5QueryBuilderTraceOperatorDTO {
stepInterval?: Querybuildertypesv5StepDTO;
}
export enum Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType {
builder_trace_operator = 'builder_trace_operator',
}
export interface Querybuildertypesv5QueryEnvelopeTraceOperatorDTO {
spec?: Querybuildertypesv5QueryBuilderTraceOperatorDTO;
type?: Querybuildertypesv5QueryTypeDTO;
/**
* @type string
* @enum builder_trace_operator
*/
type: Querybuildertypesv5QueryEnvelopeTraceOperatorDTOType;
}
export interface Querybuildertypesv5PromQueryDTO {
@@ -4411,9 +4427,16 @@ export interface Querybuildertypesv5PromQueryDTO {
step?: Querybuildertypesv5StepDTO;
}
export enum Querybuildertypesv5QueryEnvelopePromQLDTOType {
promql = 'promql',
}
export interface Querybuildertypesv5QueryEnvelopePromQLDTO {
spec?: Querybuildertypesv5PromQueryDTO;
type?: Querybuildertypesv5QueryTypeDTO;
/**
* @type string
* @enum promql
*/
type: Querybuildertypesv5QueryEnvelopePromQLDTOType;
}
export interface Querybuildertypesv5ClickHouseQueryDTO {
@@ -4435,40 +4458,24 @@ export interface Querybuildertypesv5ClickHouseQueryDTO {
query?: string;
}
export enum Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType {
clickhouse_sql = 'clickhouse_sql',
}
export interface Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO {
spec?: Querybuildertypesv5ClickHouseQueryDTO;
type?: Querybuildertypesv5QueryTypeDTO;
/**
* @type string
* @enum clickhouse_sql
*/
type: Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType;
}
export type Querybuildertypesv5QueryEnvelopeDTO =
| (Querybuildertypesv5QueryEnvelopeBuilderTraceDTO & {
spec?: unknown;
type?: Querybuildertypesv5QueryTypeDTO;
})
| (Querybuildertypesv5QueryEnvelopeBuilderLogDTO & {
spec?: unknown;
type?: Querybuildertypesv5QueryTypeDTO;
})
| (Querybuildertypesv5QueryEnvelopeBuilderMetricDTO & {
spec?: unknown;
type?: Querybuildertypesv5QueryTypeDTO;
})
| (Querybuildertypesv5QueryEnvelopeFormulaDTO & {
spec?: unknown;
type?: Querybuildertypesv5QueryTypeDTO;
})
| (Querybuildertypesv5QueryEnvelopeTraceOperatorDTO & {
spec?: unknown;
type?: Querybuildertypesv5QueryTypeDTO;
})
| (Querybuildertypesv5QueryEnvelopePromQLDTO & {
spec?: unknown;
type?: Querybuildertypesv5QueryTypeDTO;
})
| (Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO & {
spec?: unknown;
type?: Querybuildertypesv5QueryTypeDTO;
});
| Querybuildertypesv5QueryEnvelopeBuilderDTO
| Querybuildertypesv5QueryEnvelopeFormulaDTO
| Querybuildertypesv5QueryEnvelopeTraceOperatorDTO
| Querybuildertypesv5QueryEnvelopePromQLDTO
| Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO;
/**
* Composite query containing one or more query envelopes. Each query envelope specifies its type and corresponding spec.
@@ -6805,9 +6812,11 @@ export interface MetricsexplorertypesInspectMetricsRequestDTO {
start: number;
}
export type Querybuildertypesv5LabelDTOValue = string | number | boolean;
export interface Querybuildertypesv5LabelDTO {
key?: TelemetrytypesTelemetryFieldKeyDTO;
value?: unknown;
value?: Querybuildertypesv5LabelDTOValue;
}
export interface Querybuildertypesv5BucketDTO {
@@ -7417,9 +7426,20 @@ export enum Querybuildertypesv5VariableTypeDTO {
custom = 'custom',
text = 'text',
}
export type Querybuildertypesv5VariableItemDTOValueOneOfItem =
| string
| number
| boolean;
export type Querybuildertypesv5VariableItemDTOValue =
| string
| number
| boolean
| Querybuildertypesv5VariableItemDTOValueOneOfItem[];
export interface Querybuildertypesv5VariableItemDTO {
type?: Querybuildertypesv5VariableTypeDTO;
value?: unknown;
value?: Querybuildertypesv5VariableItemDTOValue;
}
export type Querybuildertypesv5QueryRangeRequestDTOVariables = {
@@ -7467,6 +7487,13 @@ export interface Querybuildertypesv5QueryRangeResponseDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export enum Querybuildertypesv5QueryTypeDTO {
builder_query = 'builder_query',
builder_formula = 'builder_formula',
builder_trace_operator = 'builder_trace_operator',
clickhouse_sql = 'clickhouse_sql',
promql = 'promql',
}
export interface RenderErrorResponseDTO {
error: ErrorsJSONDTO;
/**

View File

@@ -58,6 +58,7 @@
}
.metrics-table-container {
padding-bottom: 48px;
.ant-table {
margin-left: -16px;
margin-right: -16px;

View File

@@ -84,11 +84,12 @@ export function KeyboardHotkeysProvider({
}
const target = event.target as HTMLElement;
const isCodeMirrorEditor =
(target as HTMLElement).closest('.cm-editor') !== null;
const isCodeMirrorEditor = target.closest('.cm-editor') !== null;
const isMonacoEditor = target.closest('.monaco-editor') !== null;
if (
IGNORE_INPUTS.includes((target as HTMLElement).tagName.toLowerCase()) ||
isCodeMirrorEditor
IGNORE_INPUTS.includes(target.tagName.toLowerCase()) ||
isCodeMirrorEditor ||
isMonacoEditor
) {
return;
}

View File

@@ -26,7 +26,13 @@ describe('panelStatusFromError', () => {
code: 'invalid_query',
message: 'Query is invalid',
url: 'https://docs/err',
errors: [{ message: 'missing aggregation' }, { message: 'bad filter' }],
errors: [
{ message: 'missing aggregation', suggestions: [] },
{ message: 'bad filter', suggestions: [] },
],
retry: null,
suggestions: [],
type: '',
});
expect(panelStatusFromError(error)).toStrictEqual({
@@ -48,7 +54,15 @@ describe('panelStatusFromError', () => {
it('omits docsUrl when the API error has no url', () => {
const error = axiosErrorWith(
{ code: 'x', message: 'y', url: '', errors: [] },
{
code: 'x',
message: 'y',
url: '',
errors: [],
retry: null,
suggestions: [],
type: '',
},
StatusCodes.INTERNAL_SERVER_ERROR,
);

View File

@@ -1,11 +1,16 @@
import type {
DashboardtypesQueryDTO,
Querybuildertypesv5BuilderQuerySpecDTO,
Querybuildertypesv5ClickHouseQueryDTO,
Querybuildertypesv5CompositeQueryDTO,
Querybuildertypesv5PromQueryDTO,
Querybuildertypesv5QueryEnvelopeDTO,
Querybuildertypesv5QueryRangeRequestDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
Querybuildertypesv5QueryTypeDTO,
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType,
Querybuildertypesv5QueryEnvelopePromQLDTOType,
Querybuildertypesv5RequestTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -66,19 +71,26 @@ export function toQueryEnvelopes(
case 'signoz/CompositeQuery':
return (plugin.spec as Querybuildertypesv5CompositeQueryDTO).queries ?? [];
case 'signoz/BuilderQuery':
// plugin.spec is the (un-narrowed) plugin-spec union, so pick the builder
// spec out of it — mirroring the CompositeQuery case above.
return [
{
type: Querybuildertypesv5QueryTypeDTO.builder_query,
spec: plugin.spec,
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
spec: plugin.spec as Querybuildertypesv5BuilderQuerySpecDTO,
},
];
case 'signoz/PromQLQuery':
return [{ type: Querybuildertypesv5QueryTypeDTO.promql, spec: plugin.spec }];
return [
{
type: Querybuildertypesv5QueryEnvelopePromQLDTOType.promql,
spec: plugin.spec as Querybuildertypesv5PromQueryDTO,
},
];
case 'signoz/ClickHouseSQL':
return [
{
type: Querybuildertypesv5QueryTypeDTO.clickhouse_sql,
spec: plugin.spec,
type: Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType.clickhouse_sql,
spec: plugin.spec as Querybuildertypesv5ClickHouseQueryDTO,
},
];
case 'signoz/Formula':
@@ -134,14 +146,22 @@ function withBarStepInterval(
): Querybuildertypesv5QueryEnvelopeDTO[] {
const stepInterval = getBarStepIntervalSeconds(startMs, endMs);
return envelopes.map((envelope) => {
if (envelope.type !== Querybuildertypesv5QueryTypeDTO.builder_query) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
) {
return envelope;
}
const spec = envelope.spec as QuerySpecView;
if (spec.stepInterval) {
if (envelope.spec?.stepInterval) {
return envelope;
}
return { ...envelope, spec: { ...spec, stepInterval } };
return {
...envelope,
spec: {
...envelope.spec,
stepInterval,
} as Querybuildertypesv5BuilderQuerySpecDTO,
};
});
}
@@ -154,12 +174,19 @@ function withPagination(
{ offset, limit }: { offset: number; limit: number },
): Querybuildertypesv5QueryEnvelopeDTO[] {
return envelopes.map((envelope) => {
if (envelope.type !== Querybuildertypesv5QueryTypeDTO.builder_query) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
) {
return envelope;
}
return {
...envelope,
spec: { ...(envelope.spec as Record<string, unknown>), offset, limit },
spec: {
...envelope.spec,
offset,
limit,
} as Querybuildertypesv5BuilderQuerySpecDTO,
};
});
}
@@ -238,7 +265,8 @@ export function hasRunnableQueries(queries: DashboardtypesQueryDTO[]): boolean {
const metricsSpecs = envelopes
.filter(
(envelope) =>
envelope.type === Querybuildertypesv5QueryTypeDTO.builder_query,
envelope.type ===
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query,
)
.map((envelope) => envelope.spec as QuerySpecView)
.filter((spec) => spec.signal === 'metrics');

View File

@@ -7,7 +7,9 @@ import type {
import {
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpecDTOKind as BuilderQueryPluginKind,
DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5CompositeQueryDTOKind as CompositeQueryPluginKind,
Querybuildertypesv5QueryTypeDTO,
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType,
Querybuildertypesv5QueryEnvelopePromQLDTOType,
} from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
@@ -46,17 +48,24 @@ const toGeneratedEnvelopes = (
const isBuilderQueryEnvelope = (
envelope: Querybuildertypesv5QueryEnvelopeDTO,
): boolean => envelope.type === Querybuildertypesv5QueryTypeDTO.builder_query;
): boolean =>
envelope.type === Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query;
export function deriveQueryType(
envelopes: Querybuildertypesv5QueryEnvelopeDTO[],
): EQueryType {
if (envelopes.some((e) => e.type === Querybuildertypesv5QueryTypeDTO.promql)) {
if (
envelopes.some(
(e) => e.type === Querybuildertypesv5QueryEnvelopePromQLDTOType.promql,
)
) {
return EQueryType.PROM;
}
if (
envelopes.some(
(e) => e.type === Querybuildertypesv5QueryTypeDTO.clickhouse_sql,
(e) =>
e.type ===
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType.clickhouse_sql,
)
) {
return EQueryType.CLICKHOUSE;

View File

@@ -1,10 +1,12 @@
import type {
Querybuildertypesv5ColumnDescriptorDTO,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO,
Querybuildertypesv5QueryRangeRequestDTO,
Querybuildertypesv5ScalarDataDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Querybuildertypesv5QueryTypeDTO } from 'api/generated/services/sigNoz.schemas';
import {
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType,
} from 'api/generated/services/sigNoz.schemas';
import type { PanelTable, PanelTableColumn } from './types';
@@ -26,15 +28,15 @@ export function extractAggregationsPerQuery(
): AggregationsPerQuery {
const perQuery: AggregationsPerQuery = {};
(requestPayload?.compositeQuery?.queries ?? []).forEach((envelope) => {
if (envelope.type !== Querybuildertypesv5QueryTypeDTO.builder_query) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeBuilderDTOType.builder_query
) {
return;
}
const spec = envelope.spec as {
name?: string;
aggregations?: AggregationView[];
};
const spec = envelope.spec;
if (spec?.name && spec.aggregations) {
perQuery[spec.name] = spec.aggregations;
perQuery[spec.name] = spec.aggregations as AggregationView[];
}
});
return perQuery;
@@ -52,13 +54,14 @@ export function extractClickhouseQueryNames(
): Set<string> {
const names = new Set<string>();
(requestPayload?.compositeQuery?.queries ?? []).forEach((envelope) => {
if (envelope.type !== Querybuildertypesv5QueryTypeDTO.clickhouse_sql) {
if (
envelope.type !==
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTOType.clickhouse_sql
) {
return;
}
const spec = (envelope as Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO)
.spec;
if (spec?.name) {
names.add(spec.name);
if (envelope.spec?.name) {
names.add(envelope.spec.name);
}
});
return names;

2
go.mod
View File

@@ -180,7 +180,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/ClickHouse/ch-go v0.71.0
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/Yiling-J/theine-go v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b

View File

@@ -66,6 +66,9 @@ type AuthZ interface {
// Lists all the roles for the organization.
List(context.Context, valuer.UUID) ([]*authtypes.Role, error)
// Collect returns per-org role usage stats for the stats reporter.
Collect(context.Context, valuer.UUID) (map[string]any, error)
// Lists all the roles for the organization filtered by name
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*authtypes.Role, error)
@@ -92,7 +95,7 @@ type AuthZ interface {
}
// OnBeforeRoleDelete is a callback invoked before a role is deleted.
type OnBeforeRoleDelete func(context.Context, valuer.UUID, valuer.UUID) error
type OnBeforeRoleDelete func(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
type Handler interface {
Create(http.ResponseWriter, *http.Request)

View File

@@ -95,6 +95,15 @@ func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*autht
return provider.store.List(ctx, orgID)
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
roles, err := provider.List(ctx, orgID)
if err != nil {
return nil, err
}
return authtypes.NewStatsFromRoles(roles), nil
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
return provider.store.ListByOrgIDAndNames(ctx, orgID, names)
}

View File

@@ -75,6 +75,12 @@ func (b *base) Error() string {
return b.m
}
// Unwrap exposes the wrapped cause so stdlib errors.Is / errors.As can walk
// the chain — e.g. errors.Is(WrapCanceledf(context.Canceled, …), context.Canceled).
func (b *base) Unwrap() error {
return b.e
}
// New returns a base error. It requires type, code and message as input.
func New(t typ, code Code, message string) *base {
return &base{
@@ -337,6 +343,16 @@ func NewTimeoutf(code Code, format string, args ...any) *base {
return Newf(TypeTimeout, code, format, args...)
}
// WrapCanceledf is a wrapper around Wrapf with TypeCanceled.
func WrapCanceledf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeCanceled, code, format, args...)
}
// NewCanceledf is a wrapper around Newf with TypeCanceled.
func NewCanceledf(code Code, format string, args ...any) *base {
return Newf(TypeCanceled, code, format, args...)
}
// WrapUnauthenticatedf is a wrapper around Wrapf with TypeUnauthenticated.
func WrapUnauthenticatedf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeUnauthenticated, code, format, args...)

View File

@@ -1,6 +1,7 @@
package errors
import (
"context"
"errors" //nolint:depguard
"testing"
"time"
@@ -84,7 +85,7 @@ func TestWithSuggestiveAdditional(t *testing.T) {
assert.Equal(t, []responseerroradditional{
{Message: "field `filed` not found", Suggestions: []string{"did you mean: `field`"}},
}, j.Errors)
assert.Nil(t, j.Suggestions, "detail-scoped suggestions must not leak into the error-wide list")
assert.Empty(t, j.Suggestions, "detail-scoped suggestions must not leak into the error-wide list")
}
func TestWithRetryAfter(t *testing.T) {
@@ -106,7 +107,12 @@ func TestAsJSONBaseError(t *testing.T) {
assert.Equal(t, "bad_input", j.Code)
assert.Equal(t, "field foo is bad", j.Message)
assert.Equal(t, "https://docs/bad_input", j.Url)
assert.Equal(t, []responseerroradditional{{Message: "hint1"}, {Message: "hint2"}}, j.Errors)
// A detail with no suggestions carries an empty (non-nil) slice — the
// suggestions field is non-nullable, so it marshals to [] rather than null.
assert.Equal(t, []responseerroradditional{
{Message: "hint1", Suggestions: []string{}},
{Message: "hint2", Suggestions: []string{}},
}, j.Errors)
// InvalidInput auto-applies the after_fix policy via NewInvalidInputf — but
// New (bare constructor) does not. The retry block should reflect that.
@@ -157,9 +163,16 @@ func TestAsJSONRetryBlock(t *testing.T) {
})
}
func TestAsJSONOptionalFieldsOmittedWhenEmpty(t *testing.T) {
func TestAsJSONEmptyWhenNoneSet(t *testing.T) {
// errors and suggestions are non-nullable in the OpenAPI spec, so AsJSON
// leaves them as empty (non-nil) slices when the error carries none — they
// marshal to [] rather than null.
j := AsJSON(New(TypeInternal, MustNewCode("boom"), "boom"))
assert.Nil(t, j.Suggestions, "no suggestions set => Suggestions must be nil so json omitempty drops it")
assert.NotNil(t, j.Suggestions)
assert.Empty(t, j.Suggestions)
assert.NotNil(t, j.Errors)
assert.Empty(t, j.Errors)
}
func TestWithStacktrace(t *testing.T) {
@@ -173,3 +186,16 @@ func TestWithStacktrace(t *testing.T) {
assert.Equal(t, "test_code", code.String())
assert.Equal(t, "panic", message)
}
// Wrapped context sentinels must remain detectable via errors.Is so callers
// that branch on context.Canceled / context.DeadlineExceeded keep working
// after the error passes through one of the signoz Wrap* helpers.
func TestWrapPreservesContextSentinels(t *testing.T) {
canceled := WrapCanceledf(context.Canceled, MustNewCode("canceled"), "op canceled")
assert.True(t, Is(canceled, context.Canceled))
assert.False(t, Is(canceled, context.DeadlineExceeded))
deadline := WrapTimeoutf(context.DeadlineExceeded, MustNewCode("timeout"), "op timed out")
assert.True(t, Is(deadline, context.DeadlineExceeded))
assert.False(t, Is(deadline, context.Canceled))
}

View File

@@ -7,32 +7,29 @@ import (
)
type JSON struct {
Type string `json:"type,omitempty"`
Type string `json:"type" required:"true"`
Code string `json:"code" required:"true"`
Message string `json:"message" required:"true"`
Url string `json:"url,omitempty"`
Errors []responseerroradditional `json:"errors,omitempty"`
Retry *responseretryjson `json:"retry,omitempty"`
Suggestions []string `json:"suggestions,omitempty"`
Url string `json:"url" required:"true" nullable:"true"`
Errors []responseerroradditional `json:"errors" required:"true" nullable:"false"`
Retry *responseretryjson `json:"retry" required:"true" nullable:"true"`
Suggestions []string `json:"suggestions" required:"true" nullable:"false"`
}
type responseretryjson struct {
Delay time.Duration `json:"delay"`
Delay time.Duration `json:"delay" required:"true" nullable:"false"`
}
type responseerroradditional struct {
Message string `json:"message,omitempty"`
Suggestions []string `json:"suggestions,omitempty"`
Message string `json:"message" required:"true"`
Suggestions []string `json:"suggestions" required:"true" nullable:"false"`
}
func AsJSON(cause error) *JSON {
// See if this is an instance of the base error or not
t, c, m, _, u, a := Unwrapb(cause)
rea := make([]responseerroradditional, len(a))
for k, v := range a {
rea[k] = responseerroradditional{Message: v.message, Suggestions: v.suggestions}
}
rea := responseAdditionals(a)
var retry *responseretryjson
if r := retryOf(cause); r != nil {
@@ -46,7 +43,7 @@ func AsJSON(cause error) *JSON {
Url: u,
Errors: rea,
Retry: retry,
Suggestions: suggestionsOf(cause),
Suggestions: nonNilStrings(suggestionsOf(cause)),
}
}
@@ -54,10 +51,7 @@ func AsURLValues(cause error) url.Values {
// See if this is an instance of the base error or not
_, c, m, _, u, a := Unwrapb(cause)
rea := make([]responseerroradditional, len(a))
for k, v := range a {
rea[k] = responseerroradditional{Message: v.message, Suggestions: v.suggestions}
}
rea := responseAdditionals(a)
errors, err := json.Marshal(rea)
if err != nil {
@@ -75,3 +69,20 @@ func AsURLValues(cause error) url.Values {
"errors": {string(errors)},
}
}
func responseAdditionals(a []additional) []responseerroradditional {
rea := make([]responseerroradditional, len(a))
for k, v := range a {
rea[k] = responseerroradditional{Message: v.message, Suggestions: nonNilStrings(v.suggestions)}
}
return rea
}
func nonNilStrings(s []string) []string {
if s == nil {
return []string{}
}
return s
}

View File

@@ -5,6 +5,18 @@ import (
"strings"
)
// Nouns name the kind of value a suggestion refers to. Pass one to
// NewValidReferences / NewSuggestionsOnLevenshteinDistance to phrase the
// "valid <noun> are ..." list consistently across the codebase.
const (
NounFields = "fields"
NounKeys = "keys"
NounServices = "services"
NounQueryTypes = "query types"
NounSignals = "signals"
NounReferences = "references"
)
const (
typoSuggestionThreshold = 0.75
// maxValidReferences caps how many valid references are listed so
@@ -13,17 +25,18 @@ const (
maxValidReferences = 20
)
// SuggestionsOnLevenshteinDistance returns a "did you mean" correction (only
// NewSuggestionsOnLevenshteinDistance returns a "did you mean" correction (only
// when a close match at least typoSuggestionThreshold similar exists) followed
// by the valid-references list.
func SuggestionsOnLevenshteinDistance(invalidInput string, validInputs []string) []string {
// by the valid-references list. noun names the kind of value being suggested
// (e.g. "fields", "keys") and is used to phrase the valid-references list.
func NewSuggestionsOnLevenshteinDistance(invalidInput string, noun string, validInputs []string) []string {
suggestions := make([]string, 0, 2)
if match, ok := ClosestLevenshteinMatch(invalidInput, validInputs); ok {
suggestions = append(suggestions, didYouMean(match))
}
if refs := ValidReferences(validInputs...); refs != "" {
if refs := NewValidReferences(noun, validInputs...); refs != "" {
suggestions = append(suggestions, refs)
}
@@ -52,10 +65,10 @@ func ClosestLevenshteinMatch(input string, candidates []string) (string, bool) {
return "", false
}
// SuggestionsFromFunc formats the string produce returns as a one-element
// NewSuggestionsFromFunc formats the string produce returns as a one-element
// "did you mean: `x`" slice, or nil when it returns the empty string (so callers
// with their own matching strategy compose into a suggestions list cleanly).
func SuggestionsFromFunc(produce func() string) []string {
func NewSuggestionsFromFunc(produce func() string) []string {
s := produce()
if s == "" {
return nil
@@ -64,12 +77,12 @@ func SuggestionsFromFunc(produce func() string) []string {
return []string{didYouMean(s)}
}
// ValidReferences formats values as "valid references: `a`, `b`", capped at
// maxValidReferences with a "(+N more)" suffix. Each value is rendered as its
// own string, an Enum() element's StringValue(), or fmt.Sprint as a fallback.
// It returns "" when there are no values, so callers don't surface a bare
// "valid references: " with nothing after it.
func ValidReferences[T any](values ...T) string {
// NewValidReferences formats values as "valid <noun> are `a`, `b`" (e.g. noun
// "fields", "functions", "keys"), capped at maxValidReferences with a "(+N more)"
// suffix. Each value is rendered as its own string, an Enum() element's
// StringValue(), or fmt.Sprint as a fallback. It returns "" when there are no
// values, so callers don't surface a bare "valid <noun> are" with nothing after it.
func NewValidReferences[T any](noun string, values ...T) string {
if len(values) == 0 {
return ""
}
@@ -97,7 +110,7 @@ func ValidReferences[T any](values ...T) string {
quoted[i] = "`" + r + "`"
}
out := "valid references: " + strings.Join(quoted, ", ")
out := "valid " + noun + " are " + strings.Join(quoted, ", ")
if truncated > 0 {
out += fmt.Sprintf(" (+%d more)", truncated)
}

View File

@@ -6,26 +6,28 @@ import (
"github.com/stretchr/testify/assert"
)
func TestValidReferences(t *testing.T) {
// An empty set returns "" so callers don't surface a bare "valid references: ".
assert.Equal(t, "", ValidReferences[string]())
func TestNewValidReferences(t *testing.T) {
// An empty set returns "" so callers don't surface a bare "valid <noun> are".
assert.Equal(t, "", NewValidReferences[string](NounFields))
assert.Equal(t, "valid references: `a`, `b`", ValidReferences("a", "b"))
// The noun phrases the list, e.g. "valid fields are", "valid keys are".
assert.Equal(t, "valid fields are `a`, `b`", NewValidReferences(NounFields, "a", "b"))
assert.Equal(t, "valid keys are `a`, `b`", NewValidReferences(NounKeys, "a", "b"))
}
func TestSuggestionsOnLevenshteinDistance(t *testing.T) {
// No valid inputs => no suggestions at all (no bare "valid references: ").
assert.Empty(t, SuggestionsOnLevenshteinDistance("foo", nil))
func TestNewSuggestionsOnLevenshteinDistance(t *testing.T) {
// No valid inputs => no suggestions at all (no bare "valid <noun> are").
assert.Empty(t, NewSuggestionsOnLevenshteinDistance("foo", NounFields, nil))
// Close match => did-you-mean plus the valid-references list.
assert.Equal(t,
[]string{"did you mean: `name`", "valid references: `name`, `color`"},
SuggestionsOnLevenshteinDistance("nam", []string{"name", "color"}),
[]string{"did you mean: `name`", "valid fields are `name`, `color`"},
NewSuggestionsOnLevenshteinDistance("nam", NounFields, []string{"name", "color"}),
)
// No close match => valid-references list only.
assert.Equal(t,
[]string{"valid references: `name`, `color`"},
SuggestionsOnLevenshteinDistance("zzzzz", []string{"name", "color"}),
[]string{"valid fields are `name`, `color`"},
NewSuggestionsOnLevenshteinDistance("zzzzz", NounFields, []string{"name", "color"}),
)
}

View File

@@ -3,10 +3,10 @@ package binding
import (
"encoding/json"
"io"
"reflect"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/jsonschema"
)
const (
@@ -76,7 +76,7 @@ func (b *jsonBinding) BindBody(body io.Reader, obj any, opts ...BindBodyOption)
return errors.
NewInvalidInputf(errors.CodeInvalidInput, message, field).
WithSuggestions(errors.SuggestionsOnLevenshteinDistance(field, JSONFieldNames(obj))...)
WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field, errors.NounFields, jsonschema.JSONFieldNames(obj))...)
}
}
@@ -86,37 +86,6 @@ func (b *jsonBinding) BindBody(body io.Reader, obj any, opts ...BindBodyOption)
return nil
}
// JSONFieldNames returns the JSON field names of a struct (or pointer to one),
// skipping fields tagged "-" or without a json tag.
func JSONFieldNames(v any) []string {
var fields []string
t := reflect.TypeOf(v)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return fields
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
fieldName := strings.Split(jsonTag, ",")[0]
if fieldName != "" {
fields = append(fields, fieldName)
}
}
return fields
}
// extractUnknownField pulls fieldname out of a `json: unknown field "fieldname"`
// decoder message, or returns "" when the message has no quoted field.
func extractUnknownField(errMsg string) string {

View File

@@ -90,21 +90,21 @@ func TestJSONBinding_BindBody_UnknownFieldSuggestions(t *testing.T) {
body: `{"shape":"round"}`,
opts: []BindBodyOption{WithDisallowUnknownFields(true)},
message: `unknown field "shape"`,
suggestions: []string{"valid references: `name`, `color`"},
suggestions: []string{"valid fields are `name`, `color`"},
},
{
name: "WithContext",
body: `{"shape":"round"}`,
opts: []BindBodyOption{WithDisallowUnknownFields(true), WithUnknownFieldContext("widget spec")},
message: `unknown field "shape" in widget spec`,
suggestions: []string{"valid references: `name`, `color`"},
suggestions: []string{"valid fields are `name`, `color`"},
},
{
name: "NearMatch",
body: `{"nam":"x"}`,
opts: []BindBodyOption{WithDisallowUnknownFields(true)},
message: `unknown field "nam"`,
suggestions: []string{"did you mean: `name`", "valid references: `name`, `color`"},
suggestions: []string{"did you mean: `name`", "valid fields are `name`, `color`"},
},
}

View File

@@ -99,13 +99,13 @@ func TestError(t *testing.T) {
name: "AlreadyExists",
statusCode: http.StatusConflict,
err: errors.New(errors.TypeAlreadyExists, errors.MustNewCode("already_exists"), "already exists").WithUrl("https://already_exists"),
expected: []byte(`{"status":"error","error":{"type":"already-exists","code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
expected: []byte(`{"status":"error","error":{"type":"already-exists","code":"already_exists","message":"already exists","url":"https://already_exists","errors":[],"retry":null,"suggestions":[]}}`),
},
"/unauthenticated": {
name: "Unauthenticated",
statusCode: http.StatusUnauthorized,
err: errors.New(errors.TypeUnauthenticated, errors.MustNewCode("not_allowed"), "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"),
expected: []byte(`{"status":"error","error":{"type":"unauthenticated","code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
expected: []byte(`{"status":"error","error":{"type":"unauthenticated","code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1","suggestions":[]},{"message":"a2","suggestions":[]}],"retry":null,"suggestions":[]}}`),
},
}
@@ -177,8 +177,8 @@ func TestErrorRetryAfterHeader(t *testing.T) {
name: "BareErrorNoHeaderNoRetryBlock",
err: errors.New(errors.TypeInternal, errors.MustNewCode("boom"), "boom"),
wantRetryAfter: "",
wantBodyContains: `"code":"boom"`,
wantBodyNotContains: `"retry"`,
wantBodyContains: `"retry":null`,
wantBodyNotContains: `"delay"`,
},
}

38
pkg/jsonschema/fields.go Normal file
View File

@@ -0,0 +1,38 @@
// Package jsonschema holds small reflection helpers shared across packages.
package jsonschema
import (
"reflect"
"strings"
)
// JSONFieldNames returns the JSON field names of a struct (or pointer to one),
// skipping fields tagged "-" or without a json tag.
func JSONFieldNames(v any) []string {
var fields []string
t := reflect.TypeOf(v)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return fields
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
fieldName := strings.Split(jsonTag, ",")[0]
if fieldName != "" {
fields = append(fields, fieldName)
}
}
return fields
}

View File

@@ -44,3 +44,7 @@ type Handler interface {
Update(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
}
type Getter interface {
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
}

View File

@@ -0,0 +1,45 @@
package implauthdomain
import (
"context"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type getter struct {
store authtypes.AuthDomainStore
}
func NewGetter(store authtypes.AuthDomainStore) authdomain.Getter {
return &getter{store: store}
}
func (getter *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error {
domains, err := getter.store.ListByOrgID(ctx, orgID)
if err != nil {
return err
}
referencedBy := make([]string, 0)
for _, domain := range domains {
for _, mappedRole := range domain.AuthDomainConfig().RoleMapping.RoleNames() {
if mappedRole == roleName {
referencedBy = append(referencedBy, domain.StorableAuthDomain().Name)
break
}
}
}
if len(referencedBy) > 0 {
return errors.WithAdditionalf(
errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleHasAuthDomainMappings, "role is referenced by an SSO role mapping, remove it before deleting"),
"referenced by auth domain(s): %s", strings.Join(referencedBy, ", "),
)
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -12,13 +13,18 @@ import (
type module struct {
store authtypes.AuthDomainStore
authNs map[authtypes.AuthNProvider]authn.AuthN
authz authz.AuthZ
}
func NewModule(store authtypes.AuthDomainStore, authNs map[authtypes.AuthNProvider]authn.AuthN) authdomain.Module {
return &module{store: store, authNs: authNs}
func NewModule(store authtypes.AuthDomainStore, authNs map[authtypes.AuthNProvider]authn.AuthN, authz authz.AuthZ) authdomain.Module {
return &module{store: store, authNs: authNs, authz: authz}
}
func (module *module) Create(ctx context.Context, domain *authtypes.AuthDomain) error {
if err := module.validateRoleMapping(ctx, domain); err != nil {
return err
}
return module.store.Create(ctx, domain)
}
@@ -50,6 +56,10 @@ func (module *module) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*au
}
func (module *module) Update(ctx context.Context, domain *authtypes.AuthDomain) error {
if err := module.validateRoleMapping(ctx, domain); err != nil {
return err
}
return module.store.Update(ctx, domain)
}
@@ -74,3 +84,13 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return stats, nil
}
func (module *module) validateRoleMapping(ctx context.Context, domain *authtypes.AuthDomain) error {
roleNames := domain.AuthDomainConfig().RoleMapping.RoleNames()
if len(roleNames) == 0 {
return nil
}
_, err := module.authz.ListByOrgIDAndNames(ctx, domain.StorableAuthDomain().OrgID, roleNames)
return err
}

View File

@@ -18,7 +18,7 @@ func NewGetter(store serviceaccounttypes.Store) serviceaccount.Getter {
return &getter{store: store}
}
func (getter *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error {
func (getter *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, _ string) error {
serviceAccounts, err := getter.store.GetServiceAccountsByOrgIDAndRoleID(ctx, orgID, roleID)
if err != nil {
return err

View File

@@ -13,7 +13,7 @@ import (
type Getter interface {
// OnBeforeRoleDelete checks if any service accounts are assigned to the role and rejects deletion if so.
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
}
type Module interface {

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
@@ -29,9 +30,10 @@ type module struct {
authDomain authdomain.Module
tokenizer tokenizer.Tokenizer
orgGetter organization.Getter
authz authz.AuthZ
}
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter) session.Module {
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, authz authz.AuthZ) session.Module {
return &module{
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
authNs: authNs,
@@ -40,6 +42,7 @@ func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.A
authDomain: authDomain,
tokenizer: tokenizer,
orgGetter: orgGetter,
authz: authz,
}
}
@@ -143,15 +146,23 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
}
roleMapping := authDomain.AuthDomainConfig().RoleMapping
role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity)
signozManagedRole := authtypes.MustGetSigNozManagedRoleFromExistingRole(role)
roleAttributeExists := false
if roleMapping != nil && roleMapping.UseRoleAttribute && callbackIdentity.Role != "" {
_, err := module.authz.GetByOrgIDAndName(ctx, callbackIdentity.OrgID, authtypes.NormalizeRoleName(callbackIdentity.Role))
if err == nil {
roleAttributeExists = true
}
}
roleNames := roleMapping.NewRolesFromCallbackIdentity(callbackIdentity, roleAttributeExists)
newUser, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, callbackIdentity.OrgID, types.UserStatusActive)
if err != nil {
return "", err
}
newUser, err = module.userSetter.GetOrCreateUser(ctx, newUser, user.WithRoleNames([]string{signozManagedRole}))
newUser, err = module.userSetter.GetOrCreateUser(ctx, newUser, user.WithRoleNames(roleNames))
if err != nil {
return "", err
}

View File

@@ -239,7 +239,7 @@ func (module *getter) VerifyResetPasswordToken(ctx context.Context, token string
return nil
}
func (module *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error {
func (module *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, _ string) error {
users, err := module.GetUsersByOrgIDAndRoleID(ctx, orgID, roleID)
if err != nil {
return err

View File

@@ -96,7 +96,7 @@ type Getter interface {
GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*types.User, error)
// OnBeforeRoleDelete checks if any users are assigned to the role and rejects deletion if so.
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) error
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
// VerifyResetPasswordToken checks if a reset password token exists and is not expired.
VerifyResetPasswordToken(ctx context.Context, token string) error

View File

@@ -81,7 +81,7 @@ func CollisionHandledFinalExpr(
// - it is not a static field
// - the next best thing to do is see if there is a typo
// and suggest a correction
wrappedErr := errors.WithSuggestiveAdditionalf(fieldForErr, errors.SuggestionsOnLevenshteinDistance(field.Name, maps.Keys(keys)), "field `%s` not found", field.Name)
wrappedErr := errors.WithSuggestiveAdditionalf(fieldForErr, errors.NewSuggestionsOnLevenshteinDistance(field.Name, errors.NounKeys, maps.Keys(keys)), "field `%s` not found", field.Name)
return "", nil, wrappedErr
} else {
for _, key := range keysForField {

View File

@@ -300,7 +300,7 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
var suggestions []string
if len(v.invalid) == 1 {
inv := v.invalid[0]
suggestions = errors.SuggestionsFromFunc(func() string {
suggestions = errors.NewSuggestionsFromFunc(func() string {
match, ok := errors.ClosestLevenshteinMatch(inv, validKeys)
if !ok || strings.Contains(original, inv+"(") || strings.Contains(match, "(") {
return ""
@@ -309,7 +309,7 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
})
}
suggestions = append(suggestions, errors.ValidReferences(validKeys...))
suggestions = append(suggestions, errors.NewValidReferences(errors.NounReferences, validKeys...))
havingErr := errors.NewInvalidInputf(
errors.CodeInvalidInput,
"Invalid references in `Having` expression: [%s]",
@@ -339,7 +339,7 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
// multiple errors are surfaced as one additional detail each. If the parser
// produced no message (rare), the top-level message stands on its own.
if len(allSyntaxErrors) == 1 && len(msgs) == 1 {
suggestions := errors.SuggestionsFromFunc(func() string {
suggestions := errors.NewSuggestionsFromFunc(func() string {
return havingSuggestion(allSyntaxErrors[0], original)
})

View File

@@ -593,7 +593,7 @@ func TestRewriteForLogsAndTraces_InOperator(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [ghost]",
wantAdditional: []string{"Valid references are: [__result, __result0, count(), total]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `count()`, `total`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `count()`, `total`"},
},
{
name: "IN with end bracked missing",
@@ -655,7 +655,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [unknown_alias]",
wantAdditional: []string{"Valid references are: [__result, __result0, count(), total]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `count()`, `total`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `count()`, `total`"},
},
{
name: "typo in identifier suggests closest match",
@@ -666,7 +666,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [totol]",
wantAdditional: []string{"Valid references are: [__result, __result0, count(), total]"},
wantSuggestions: []string{"did you mean: `total > 100`", "valid references: `__result`, `__result0`, `count()`, `total`"},
wantSuggestions: []string{"did you mean: `total > 100`", "valid references are `__result`, `__result0`, `count()`, `total`"},
},
{
name: "expression not in column map",
@@ -677,7 +677,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [sum]",
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `count()`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `count()`"},
},
{
name: "one valid one invalid reference",
@@ -688,7 +688,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [ghost]",
wantAdditional: []string{"Valid references are: [__result, __result0, count(), total]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `count()`, `total`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `count()`, `total`"},
},
{
name: "__result ambiguous with multiple aggregations",
@@ -700,7 +700,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [__result]",
wantAdditional: []string{"Valid references are: [__result0, __result1, count(), sum(bytes)]"},
wantSuggestions: []string{"did you mean: `__result0 > 100`", "valid references: `__result0`, `__result1`, `count()`, `sum(bytes)`"},
wantSuggestions: []string{"did you mean: `__result0 > 100`", "valid references are `__result0`, `__result1`, `count()`, `sum(bytes)`"},
},
{
name: "out-of-range __result_N index",
@@ -711,7 +711,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [__result_9]",
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
wantSuggestions: []string{"did you mean: `__result > 100`", "valid references: `__result`, `__result0`, `count()`"},
wantSuggestions: []string{"did you mean: `__result > 100`", "valid references are `__result`, `__result0`, `count()`"},
},
{
name: "__result_1 out of range for single aggregation",
@@ -722,7 +722,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [__result_1]",
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
wantSuggestions: []string{"did you mean: `__result > 100`", "valid references: `__result`, `__result0`, `count()`"},
wantSuggestions: []string{"did you mean: `__result > 100`", "valid references are `__result`, `__result0`, `count()`"},
},
{
name: "cascaded function calls",
@@ -733,7 +733,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [sum]",
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `count()`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `count()`"},
},
{
name: "function call with multiple args not in column map",
@@ -744,7 +744,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [sum]",
wantAdditional: []string{"Valid references are: [__result, __result0, sum(a)]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `sum(a)`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `sum(a)`"},
},
{
name: "unquoted string value treated as unknown identifier",
@@ -755,7 +755,7 @@ func TestRewriteForLogsAndTraces_ErrorInvalidReferences(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [xyz]",
wantAdditional: []string{"Valid references are: [__result, __result0, sum(bytes)]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `sum(bytes)`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `sum(bytes)`"},
},
})
}
@@ -1030,7 +1030,7 @@ func TestRewriteForMetrics(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [wrong_metric]",
wantAdditional: []string{"Valid references are: [__result, __result0, sum(cpu_usage)]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `sum(cpu_usage)`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `sum(cpu_usage)`"},
},
// --- Error: string literal (not allowed in HAVING) ---
{
@@ -1077,7 +1077,7 @@ func TestRewriteForMetrics(t *testing.T) {
wantErr: true,
wantErrMsg: "Invalid references in `Having` expression: [count]",
wantAdditional: []string{"Valid references are: [__result, __result0, sum(cpu_usage)]"},
wantSuggestions: []string{"valid references: `__result`, `__result0`, `sum(cpu_usage)`"},
wantSuggestions: []string{"valid references are `__result`, `__result0`, `sum(cpu_usage)`"},
},
}

View File

@@ -128,6 +128,7 @@ func NewModules(
}
userSetter := impluser.NewSetter(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, userRoleStore, userGetter, onDeleteUser)
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
authDomainModule := implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs, authz)
return Modules{
OrgGetter: orgGetter,
@@ -142,8 +143,8 @@ func NewModules(
QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
RawDataExport: implrawdataexport.NewModule(querier),
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
AuthDomain: authDomainModule,
Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, authDomainModule, tokenizer, orgGetter, authz),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),

View File

@@ -215,6 +215,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewRecreateUserDashboardPreferenceFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateRecurrenceBoundsFactory(sqlstore),
sqlmigration.NewAddDashboardViewFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateSSORoleMappingNamesFactory(sqlstore),
)
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/meterreporter"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -349,10 +350,13 @@ func New(
// Initialize service account getter
serviceAccountGetter := implserviceaccount.NewGetter(implserviceaccount.NewStore(sqlstore))
authDomainGetter := implauthdomain.NewGetter(implauthdomain.NewStore(sqlstore))
// Build pre-delete callbacks from modules
onBeforeRoleDelete := []authz.OnBeforeRoleDelete{
userGetter.OnBeforeRoleDelete,
serviceAccountGetter.OnBeforeRoleDelete,
authDomainGetter.OnBeforeRoleDelete,
}
// Initialize authz
@@ -501,6 +505,7 @@ func New(
modules.LogsPipeline,
modules.InfraMonitoring,
querier,
authz,
}
// Initialize the stats aggregator (always-on, independent of whether reporting is enabled)

View File

@@ -0,0 +1,127 @@
package sqlmigration
import (
"context"
"encoding/json"
"log/slog"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type migrateSSORoleMappingNames struct {
sqlstore sqlstore.SQLStore
logger *slog.Logger
}
type authDomainRow struct {
bun.BaseModel `bun:"table:auth_domain"`
ID string `bun:"id"`
Data string `bun:"data"`
}
var legacyRoleToManagedRoleName = map[string]string{
"ADMIN": "signoz-admin",
"EDITOR": "signoz-editor",
"VIEWER": "signoz-viewer",
}
type ssoRoleMapping struct {
DefaultRole string `json:"defaultRole"`
GroupMappings map[string]string `json:"groupMappings"`
UseRoleAttribute bool `json:"useRoleAttribute"`
}
func NewMigrateSSORoleMappingNamesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("migrate_sso_role_mapping_names"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &migrateSSORoleMappingNames{sqlstore: sqlstore, logger: ps.Logger}, nil
},
)
}
func (migration *migrateSSORoleMappingNames) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *migrateSSORoleMappingNames) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
rows := make([]*authDomainRow, 0)
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
return err
}
for _, row := range rows {
config := make(map[string]json.RawMessage)
if err := json.Unmarshal([]byte(row.Data), &config); err != nil {
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable data", slog.String("auth_domain_id", row.ID), errors.Attr(err))
continue
}
roleMappingRaw, ok := config["roleMapping"]
if !ok || string(roleMappingRaw) == "null" {
continue
}
var roleMapping ssoRoleMapping
if err := json.Unmarshal(roleMappingRaw, &roleMapping); err != nil {
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable role mapping", slog.String("auth_domain_id", row.ID), errors.Attr(err))
continue
}
changed := false
if managed, ok := legacyRoleToManagedRoleName[strings.ToUpper(roleMapping.DefaultRole)]; ok {
roleMapping.DefaultRole = managed
changed = true
}
for group, role := range roleMapping.GroupMappings {
if managed, ok := legacyRoleToManagedRoleName[strings.ToUpper(role)]; ok {
roleMapping.GroupMappings[group] = managed
changed = true
}
}
if !changed {
continue
}
newRoleMapping, err := json.Marshal(roleMapping)
if err != nil {
return err
}
config["roleMapping"] = newRoleMapping
newData, err := json.Marshal(config)
if err != nil {
return err
}
if _, err := tx.NewUpdate().
Model((*authDomainRow)(nil)).
Set("data = ?", string(newData)).
Where("id = ?", row.ID).
Exec(ctx); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *migrateSSORoleMappingNames) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -109,7 +109,7 @@ func (m *fieldMapper) ColumnExpressionFor(
field.FieldContext = telemetrytypes.FieldContextLog
fieldExpression, _ = m.FieldFor(ctx, tsStart, tsEnd, field)
} else {
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.SuggestionsOnLevenshteinDistance(field.Name, maps.Keys(keys))...)
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field.Name, errors.NounKeys, maps.Keys(keys))...)
return "", wrappedErr
}
} else {

View File

@@ -263,7 +263,7 @@ func (m *fieldMapper) ColumnExpressionFor(
// - it is not a static field
// - the next best thing to do is see if there is a typo
// and suggest a correction
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.SuggestionsOnLevenshteinDistance(field.Name, maps.Keys(keys))...)
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field.Name, errors.NounKeys, maps.Keys(keys))...)
return "", wrappedErr
}
} else if len(keysForField) == 1 {

View File

@@ -91,7 +91,7 @@ func (m *fieldMapper) ColumnExpressionFor(
// - it is not a static field
// - the next best thing to do is see if there is a typo
// and suggest a correction
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.SuggestionsOnLevenshteinDistance(field.Name, maps.Keys(keys))...)
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field.Name, errors.NounKeys, maps.Keys(keys))...)
return "", wrappedErr
}
} else if len(keysForField) == 1 {

View File

@@ -3,13 +3,76 @@ package clickhousetelemetrystore
import (
"context"
chproto "github.com/ClickHouse/ch-go/proto"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"go.opentelemetry.io/otel/metric"
)
var (
ErrCodeSyntaxError = errors.MustNewCode("syntax_error")
ErrCodeUnknownTable = errors.MustNewCode("unknown_table")
ErrCodeUnknownDatabase = errors.MustNewCode("unknown_database")
ErrCodeUnknownIdentifier = errors.MustNewCode("unknown_identifier")
ErrCodeIllegalArgument = errors.MustNewCode("illegal_argument")
ErrCodeQueryCanceled = errors.MustNewCode("query_canceled")
ErrCodeQueryTimeout = errors.MustNewCode("query_timeout")
ErrCodeExecutionFailed = errors.MustNewCode("execution_failed")
)
// Codes absent from this map fall through to the raw driver error in castError.
var clickHouseExceptionWrappers = map[chproto.Error]func(cause error, ex *clickhouse.Exception) error{
chproto.ErrSyntaxError: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeSyntaxError, "SQL syntax error: %s", ex.Message)
},
chproto.ErrUnknownTable: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapNotFoundf(cause, ErrCodeUnknownTable, "unknown table: %s", ex.Message)
},
chproto.ErrUnknownDatabase: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapNotFoundf(cause, ErrCodeUnknownDatabase, "unknown database: %s", ex.Message)
},
chproto.ErrUnknownIdentifier: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeUnknownIdentifier, "unknown identifier: %s", ex.Message)
},
chproto.ErrUnknownFunction: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeUnknownIdentifier, "unknown function: %s", ex.Message)
},
chproto.ErrUnknownAggregateFunction: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeUnknownIdentifier, "unknown aggregate function: %s", ex.Message)
},
chproto.ErrUnknownType: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeUnknownIdentifier, "unknown type: %s", ex.Message)
},
chproto.ErrUnknownStorage: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeUnknownIdentifier, "unknown storage engine: %s", ex.Message)
},
chproto.ErrIllegalColumn: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeUnknownIdentifier, "illegal column: %s", ex.Message)
},
chproto.ErrUnknownElementInAst: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeSyntaxError, "unknown element in SQL AST: %s", ex.Message)
},
chproto.ErrUnknownTypeOfQuery: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeSyntaxError, "unknown query type: %s", ex.Message)
},
chproto.ErrIllegalTypeOfArgument: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeIllegalArgument, "illegal argument type: %s", ex.Message)
},
chproto.ErrNumberOfArgumentsDoesntMatch: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeIllegalArgument, "wrong number of arguments: %s", ex.Message)
},
chproto.ErrTooManyArgumentsForFunction: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeIllegalArgument, "too many arguments to function: %s", ex.Message)
},
chproto.ErrTooLessArgumentsForFunction: func(cause error, ex *clickhouse.Exception) error {
return errors.WrapInvalidInputf(cause, ErrCodeIllegalArgument, "too few arguments to function: %s", ex.Message)
},
}
type provider struct {
settings factory.ScopedProviderSettings
clickHouseConn clickhouse.Conn
@@ -103,7 +166,7 @@ func (p *provider) Query(ctx context.Context, query string, args ...interface{})
if err != nil {
event.Err = err
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return nil, err
return nil, castError(err)
}
return &rowsWithHooks{
@@ -120,10 +183,15 @@ func (p *provider) QueryRow(ctx context.Context, query string, args ...interface
ctx = telemetrystore.WrapBeforeQuery(p.hooks, ctx, event)
row := p.clickHouseConn.QueryRow(ctx, query, args...)
if row == nil {
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return nil
}
event.Err = row.Err()
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return row
return &rowWithCastError{Row: row}
}
func (p *provider) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
@@ -135,7 +203,7 @@ func (p *provider) Select(ctx context.Context, dest interface{}, query string, a
event.Err = err
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return err
return castError(err)
}
func (p *provider) Exec(ctx context.Context, query string, args ...interface{}) error {
@@ -147,20 +215,19 @@ func (p *provider) Exec(ctx context.Context, query string, args ...interface{})
event.Err = err
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return err
return castError(err)
}
func (p *provider) AsyncInsert(ctx context.Context, query string, wait bool, args ...interface{}) error {
event := telemetrystore.NewQueryEvent(query, args)
ctx = telemetrystore.WrapBeforeQuery(p.hooks, ctx, event)
// TODO: migrate to WithAsync() — https://github.com/SigNoz/engineering-pod/issues/5093
err := p.clickHouseConn.AsyncInsert(ctx, query, wait, args...) //nolint:staticcheck
err := p.clickHouseConn.Exec(clickhouse.Context(ctx, clickhouse.WithAsync(wait)), query, args...)
event.Err = err
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return err
return castError(err)
}
func (p *provider) PrepareBatch(ctx context.Context, query string, opts ...driver.PrepareBatchOption) (driver.Batch, error) {
@@ -172,7 +239,10 @@ func (p *provider) PrepareBatch(ctx context.Context, query string, opts ...drive
event.Err = err
telemetrystore.WrapAfterQuery(p.hooks, ctx, event)
return batch, err
if batch == nil {
return nil, castError(err)
}
return &batchWithCastError{Batch: batch}, castError(err)
}
func (p *provider) ServerVersion() (*driver.ServerVersion, error) {
@@ -182,3 +252,27 @@ func (p *provider) ServerVersion() (*driver.ServerVersion, error) {
func (p *provider) Contributors() []string {
return p.clickHouseConn.Contributors()
}
func castError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, context.Canceled) {
return errors.WrapCanceledf(err, ErrCodeQueryCanceled, "query canceled")
}
if errors.Is(err, context.DeadlineExceeded) {
return errors.WrapTimeoutf(err, ErrCodeQueryTimeout, "query timed out")
}
var ex *clickhouse.Exception
if !errors.As(err, &ex) {
return err
}
if wrap, ok := clickHouseExceptionWrappers[chproto.Error(ex.Code)]; ok {
return wrap(err, ex)
}
return err
}

View File

@@ -25,13 +25,95 @@ func (r *rowsWithHooks) Close() error {
// mark as closed and run the onClose hook
r.closed = true
if err := r.Err(); err != nil {
if err := castError(r.Rows.Err()); err != nil {
r.event.Err = err
}
closeErr := r.Rows.Close()
closeErr := castError(r.Rows.Close())
if closeErr != nil {
r.event.Err = closeErr
}
r.onClose()
return closeErr
}
func (r *rowsWithHooks) Err() error {
return castError(r.Rows.Err())
}
func (r *rowsWithHooks) Scan(dest ...any) error {
return castError(r.Rows.Scan(dest...))
}
func (r *rowsWithHooks) ScanStruct(dest any) error {
return castError(r.Rows.ScanStruct(dest))
}
func (r *rowsWithHooks) Totals(dest ...any) error {
return castError(r.Rows.Totals(dest...))
}
// rowWithCastError wraps driver.Row so errors surfaced by Err/Scan/ScanStruct
// are normalized through castError, matching the rest of the provider's API.
type rowWithCastError struct {
driver.Row
}
func (r *rowWithCastError) Err() error {
return castError(r.Row.Err())
}
func (r *rowWithCastError) Scan(dest ...any) error {
return castError(r.Row.Scan(dest...))
}
func (r *rowWithCastError) ScanStruct(dest any) error {
return castError(r.Row.ScanStruct(dest))
}
// batchWithCastError wraps driver.Batch so error-returning methods (Append,
// Send, Close, etc.) are normalized through castError.
type batchWithCastError struct {
driver.Batch
}
func (b *batchWithCastError) Abort() error {
return castError(b.Batch.Abort())
}
func (b *batchWithCastError) Append(v ...any) error {
return castError(b.Batch.Append(v...))
}
func (b *batchWithCastError) AppendStruct(v any) error {
return castError(b.Batch.AppendStruct(v))
}
func (b *batchWithCastError) Flush() error {
return castError(b.Batch.Flush())
}
func (b *batchWithCastError) Send() error {
return castError(b.Batch.Send())
}
func (b *batchWithCastError) Close() error {
return castError(b.Batch.Close())
}
func (b *batchWithCastError) Column(i int) driver.BatchColumn {
return &batchColumnWithCastError{BatchColumn: b.Batch.Column(i)}
}
// batchColumnWithCastError wraps driver.BatchColumn so column-level appends
// also flow through castError.
type batchColumnWithCastError struct {
driver.BatchColumn
}
func (c *batchColumnWithCastError) Append(v any) error {
return castError(c.BatchColumn.Append(v))
}
func (c *batchColumnWithCastError) AppendRow(v any) error {
return castError(c.BatchColumn.AppendRow(v))
}

View File

@@ -368,7 +368,7 @@ func (m *defaultFieldMapper) ColumnExpressionFor(
// - it is not a static field
// - the next best thing to do is see if there is a typo
// and suggest a correction
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.SuggestionsOnLevenshteinDistance(field.Name, maps.Keys(keys))...)
wrappedErr := errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field.Name, errors.NounKeys, maps.Keys(keys))...)
return "", wrappedErr
}
} else if len(keysForField) == 1 {

View File

@@ -2,10 +2,6 @@ package authtypes
import (
"encoding/json"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
)
type AttributeMapping struct {
@@ -51,83 +47,95 @@ func (attr *AttributeMapping) UnmarshalJSON(data []byte) error {
}
type RoleMapping struct {
// Default role any new SSO users. Defaults to "VIEWER"
// Default role assigned to new SSO users when no group mapping applies.
DefaultRole string `json:"defaultRole"`
// Map of IDP group names to SigNoz roles. Key is group name, value is SigNoz role
// Map of IDP group name to SigNoz role name.
GroupMappings map[string]string `json:"groupMappings"`
// If true, use the role claim directly from IDP instead of group mappings
// If true, use the role claim directly from IDP instead of group mappings.
UseRoleAttribute bool `json:"useRoleAttribute"`
}
func (typ *RoleMapping) UnmarshalJSON(data []byte) error {
type Alias RoleMapping
func (roleMapping *RoleMapping) UnmarshalJSON(data []byte) error {
type alias RoleMapping
var temp Alias
var temp alias
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
if temp.DefaultRole != "" {
if _, err := types.NewRole(strings.ToUpper(temp.DefaultRole)); err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid default role %s", temp.DefaultRole)
}
}
temp.DefaultRole = NormalizeRoleName(temp.DefaultRole)
for group, role := range temp.GroupMappings {
if _, err := types.NewRole(strings.ToUpper(role)); err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid role %s for group %s", role, group)
}
temp.GroupMappings[group] = NormalizeRoleName(role)
}
*typ = RoleMapping(temp)
*roleMapping = RoleMapping(temp)
return nil
}
func (roleMapping *RoleMapping) NewRoleFromCallbackIdentity(callbackIdentity *CallbackIdentity) types.Role {
func (roleMapping *RoleMapping) NewRolesFromCallbackIdentity(callbackIdentity *CallbackIdentity, roleAttributeExists bool) []string {
if roleMapping == nil {
return types.RoleViewer
return []string{SigNozViewerRoleName}
}
if roleMapping.UseRoleAttribute && callbackIdentity.Role != "" {
if role, err := types.NewRole(strings.ToUpper(callbackIdentity.Role)); err == nil {
return role
}
if roleAttributeExists {
return []string{NormalizeRoleName(callbackIdentity.Role)}
}
if len(roleMapping.GroupMappings) > 0 && len(callbackIdentity.Groups) > 0 {
highestRole := types.RoleViewer
found := false
roleNames := make([]string, 0)
seen := make(map[string]struct{})
for _, group := range callbackIdentity.Groups {
if mappedRole, exists := roleMapping.GroupMappings[group]; exists {
found = true
if role, err := types.NewRole(strings.ToUpper(mappedRole)); err == nil {
if compareRoles(role, highestRole) > 0 {
highestRole = role
}
}
roleName, exists := roleMapping.GroupMappings[group]
if !exists {
continue
}
if _, duplicate := seen[roleName]; duplicate {
continue
}
seen[roleName] = struct{}{}
roleNames = append(roleNames, roleName)
}
if found {
return highestRole
if len(roleNames) > 0 {
return roleNames
}
}
return []string{roleMapping.DefaultRoleName()}
}
func (roleMapping *RoleMapping) DefaultRoleName() string {
if roleMapping.DefaultRole != "" {
return roleMapping.DefaultRole
}
return SigNozViewerRoleName
}
func (roleMapping *RoleMapping) RoleNames() []string {
if roleMapping == nil {
return nil
}
seen := make(map[string]struct{})
roleNames := make([]string, 0, len(roleMapping.GroupMappings)+1)
if roleMapping.DefaultRole != "" {
if role, err := types.NewRole(strings.ToUpper(roleMapping.DefaultRole)); err == nil {
return role
seen[roleMapping.DefaultRole] = struct{}{}
roleNames = append(roleNames, roleMapping.DefaultRole)
}
for _, roleName := range roleMapping.GroupMappings {
if roleName == "" {
continue
}
if _, duplicate := seen[roleName]; duplicate {
continue
}
seen[roleName] = struct{}{}
roleNames = append(roleNames, roleName)
}
return types.RoleViewer
}
func compareRoles(a, b types.Role) int {
order := map[types.Role]int{
types.RoleViewer: 0,
types.RoleEditor: 1,
types.RoleAdmin: 2,
}
return order[a] - order[b]
return roleNames
}

View File

@@ -25,6 +25,7 @@ var (
ErrCodeRoleUnsupported = errors.MustNewCode("role_unsupported")
ErrCodeRoleHasUserAssignees = errors.MustNewCode("role_has_user_assignees")
ErrCodeRoleHasServiceAccountAssignees = errors.MustNewCode("role_has_service_account_assignees")
ErrCodeRoleHasAuthDomainMappings = errors.MustNewCode("role_has_auth_domain_mappings")
)
var (
@@ -135,6 +136,20 @@ func NewManagedRoles(orgID valuer.UUID) []*Role {
}
func NewStatsFromRoles(roles []*Role) map[string]any {
stats := make(map[string]any)
for _, role := range roles {
key := "role." + role.Type.StringValue() + ".count"
if value, ok := stats[key]; ok {
stats[key] = value.(int64) + 1
} else {
stats[key] = int64(1)
}
}
stats["role.count"] = int64(len(roles))
return stats
}
func (role *Role) PatchMetadata(description string) error {
err := role.ErrIfManaged()
if err != nil {
@@ -303,6 +318,20 @@ func MustGetSigNozManagedRoleFromExistingRole(role types.Role) string {
return managedRole
}
func NormalizeRoleName(role string) string {
legacyRole, err := types.NewRole(strings.ToUpper(role))
if err != nil {
return role
}
managedRole, ok := ExistingRoleToSigNozManagedRoleMap[legacyRole]
if !ok {
return role
}
return managedRole
}
type RoleStore interface {
Create(context.Context, *Role) error
Get(context.Context, valuer.UUID, valuer.UUID) (*Role, error)

View File

@@ -123,5 +123,5 @@ func NewServiceID(provider CloudProviderType, service string) (ServiceID, error)
return ServiceID{}, errors.NewInvalidInputf(ErrCodeInvalidServiceID,
"invalid service id %q for %s cloud provider", service, provider.StringValue()).
WithSuggestions(errors.SuggestionsOnLevenshteinDistance(service, validServices)...)
WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(service, errors.NounServices, validServices)...)
}

View File

@@ -621,6 +621,25 @@ func (f FunctionArg) Copy() FunctionArg {
return f
}
var _ jsonschema.Preparer = FunctionArg{}
// PrepareJSONSchema types `value` as a number-or-string scalar instead of an
// untyped {}. The Go field stays `any`; this only shapes the generated schema.
func (FunctionArg) PrepareJSONSchema(s *jsonschema.Schema) error {
if _, ok := s.Properties["value"]; !ok {
return nil
}
value := jsonschema.Schema{}
value.OneOf = []jsonschema.SchemaOrBool{
jsonschema.Number.ToSchemaOrBool(),
jsonschema.String.ToSchemaOrBool(),
}
s.Properties["value"] = value.ToSchemaOrBool()
return nil
}
type Function struct {
// name of the function
Name FunctionName `json:"name"`

View File

@@ -48,8 +48,8 @@ type QueryBuilderJoin struct {
Type JoinType `json:"type"`
On string `json:"on"`
// primary aggregations: if empty ⇒ raw columns
// currently supported: []Aggregation, []MetricAggregation
// primary aggregations: if empty ⇒ raw columns. Untyped — joins are deferred
// (see the commented JoinAggregation below).
Aggregations []any `json:"aggregations,omitempty"`
// select columns to select
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitempty"`
@@ -64,6 +64,32 @@ type QueryBuilderJoin struct {
Functions []Function `json:"functions,omitempty"`
}
// JoinAggregation modelled a join aggregation as a trace/log/metric oneOf. Deferred:
// that oneOf has no discriminator (trace ≡ log, and a join carries no `signal`), so
// code generators can't map it. TODO: add a discriminator before re-enabling.
//
// type JoinAggregation struct {
// value any
// }
//
// var _ jsonschema.OneOfExposer = JoinAggregation{}
//
// func (JoinAggregation) JSONSchemaOneOf() []any {
// return []any{
// TraceAggregation{},
// LogAggregation{},
// MetricAggregation{},
// }
// }
//
// func (j JoinAggregation) MarshalJSON() ([]byte, error) {
// return json.Marshal(j.value)
// }
//
// func (j *JoinAggregation) UnmarshalJSON(data []byte) error {
// return json.Unmarshal(data, &j.value)
// }
// Copy creates a deep copy of QueryBuilderJoin.
func (q QueryBuilderJoin) Copy() QueryBuilderJoin {
c := q

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/govaluate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
signozjsonschema "github.com/SigNoz/signoz/pkg/jsonschema"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -21,71 +22,113 @@ type QueryEnvelope struct {
Spec any `json:"spec"`
}
// queryEnvelopeBuilderTrace is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=traces.
type queryEnvelopeBuilderTrace struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderQuery[TraceAggregation] `json:"spec" description:"The trace builder query specification."`
// builderQuerySpec is a signal-discriminated oneOf of the three
// QueryBuilderQuery[T]; schema-only (runtime dispatch is in QueryEnvelope.UnmarshalJSON).
type builderQuerySpec struct{}
var (
_ jsonschema.OneOfExposer = builderQuerySpec{}
_ jsonschema.Preparer = builderQuerySpec{}
)
func (builderQuerySpec) JSONSchemaOneOf() []any {
return []any{
QueryBuilderQuery[TraceAggregation]{},
QueryBuilderQuery[LogAggregation]{},
QueryBuilderQuery[MetricAggregation]{},
}
}
// queryEnvelopeBuilderLog is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=logs.
type queryEnvelopeBuilderLog struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderQuery[LogAggregation] `json:"spec" description:"The log builder query specification."`
func (builderQuerySpec) PrepareJSONSchema(s *jsonschema.Schema) error {
if s.ExtraProperties == nil {
s.ExtraProperties = map[string]any{}
}
s.ExtraProperties["x-signoz-discriminator"] = map[string]any{
"propertyName": "signal",
"mapping": map[string]string{
telemetrytypes.SignalTraces.StringValue(): "#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation",
telemetrytypes.SignalLogs.StringValue(): "#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregation",
telemetrytypes.SignalMetrics.StringValue(): "#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregation",
},
}
return nil
}
// queryEnvelopeBuilderMetric is the OpenAPI schema for a QueryEnvelope with type=builder_query and signal=metrics.
type queryEnvelopeBuilderMetric struct {
Type QueryType `json:"type" description:"The type of the query."`
Spec QueryBuilderQuery[MetricAggregation] `json:"spec" description:"The metric builder query specification."`
// queryEnvelopeBuilder is the OpenAPI schema for a builder_query QueryEnvelope
// (spec is the signal-discriminated builderQuerySpec). `type` is required:"true"
// on every variant so oapi-codegen renders the discriminator non-pointer.
type queryEnvelopeBuilder struct {
Type QueryType `json:"type" required:"true" description:"The type of the query."`
Spec builderQuerySpec `json:"spec" description:"The builder query specification."`
}
// queryEnvelopeFormula is the OpenAPI schema for a QueryEnvelope with type=builder_formula.
type queryEnvelopeFormula struct {
Type QueryType `json:"type" description:"The type of the query."`
Type QueryType `json:"type" required:"true" description:"The type of the query."`
Spec QueryBuilderFormula `json:"spec" description:"The formula specification."`
}
// queryEnvelopeJoin is the OpenAPI schema for a QueryEnvelope with type=builder_join.
// queryEnvelopeJoin (builder_join) is deferred: its aggregations are an
// undiscriminable oneOf (see JoinAggregation in join.go). Re-add to
// JSONSchemaOneOf and the discriminator mapping when joins are supported.
// type queryEnvelopeJoin struct {
// Type QueryType `json:"type" description:"The type of the query."`
// Type QueryType `json:"type" required:"true" description:"The type of the query."`
// Spec QueryBuilderJoin `json:"spec" description:"The join specification."`
// }
// queryEnvelopeTraceOperator is the OpenAPI schema for a QueryEnvelope with type=builder_trace_operator.
type queryEnvelopeTraceOperator struct {
Type QueryType `json:"type" description:"The type of the query."`
Type QueryType `json:"type" required:"true" description:"The type of the query."`
Spec QueryBuilderTraceOperator `json:"spec" description:"The trace operator specification."`
}
// queryEnvelopePromQL is the OpenAPI schema for a QueryEnvelope with type=promql.
type queryEnvelopePromQL struct {
Type QueryType `json:"type" description:"The type of the query."`
Type QueryType `json:"type" required:"true" description:"The type of the query."`
Spec PromQuery `json:"spec" description:"The PromQL query specification."`
}
// queryEnvelopeClickHouseSQL is the OpenAPI schema for a QueryEnvelope with type=clickhouse_sql.
type queryEnvelopeClickHouseSQL struct {
Type QueryType `json:"type" description:"The type of the query."`
Type QueryType `json:"type" required:"true" description:"The type of the query."`
Spec ClickHouseQuery `json:"spec" description:"The ClickHouse SQL query specification."`
}
var _ jsonschema.OneOfExposer = QueryEnvelope{}
// JSONSchemaOneOf returns the oneOf variants for the QueryEnvelope discriminated union.
// Each variant represents a different query type with its corresponding spec schema.
// JSONSchemaOneOf returns the variants of the QueryEnvelope discriminated union.
func (QueryEnvelope) JSONSchemaOneOf() []any {
return []any{
queryEnvelopeBuilderTrace{},
queryEnvelopeBuilderLog{},
queryEnvelopeBuilderMetric{},
queryEnvelopeBuilder{},
queryEnvelopeFormula{},
// queryEnvelopeJoin{},
// queryEnvelopeJoin{}, // deferred — see commented queryEnvelopeJoin above
queryEnvelopeTraceOperator{},
queryEnvelopePromQL{},
queryEnvelopeClickHouseSQL{},
}
}
var _ jsonschema.Preparer = QueryEnvelope{}
// PrepareJSONSchema marks the envelope as a `type`-discriminated union;
// signoz.attachDiscriminators promotes it and strips the base properties.
func (QueryEnvelope) PrepareJSONSchema(s *jsonschema.Schema) error {
if s.ExtraProperties == nil {
s.ExtraProperties = map[string]any{}
}
s.ExtraProperties["x-signoz-discriminator"] = map[string]any{
"propertyName": "type",
"mapping": map[string]string{
QueryTypeBuilder.StringValue(): "#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder",
QueryTypeFormula.StringValue(): "#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula",
QueryTypeTraceOperator.StringValue(): "#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator",
QueryTypePromQL.StringValue(): "#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL",
QueryTypeClickHouseSQL.StringValue(): "#/components/schemas/Querybuildertypesv5QueryEnvelopeClickHouseSQL",
},
}
return nil
}
// implement custom json unmarshaler for the QueryEnvelope.
func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
var shadow struct {
@@ -152,7 +195,7 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
shadow.Type,
).WithAdditional(
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, builder_trace_operator, promql, clickhouse_sql",
).WithSuggestions(errors.ValidReferences(QueryType{}.Enum()...))
).WithSuggestions(errors.NewValidReferences(errors.NounQueryTypes, QueryType{}.Enum()...))
}
return nil
@@ -196,7 +239,7 @@ func UnmarshalBuilderQueryBySignal(data []byte) (any, error) {
errors.CodeInvalidInput,
"invalid signal %q",
header.Signal.StringValue(),
).WithSuggestions(errors.ValidReferences(telemetrytypes.Signal{}.Enum()...))
).WithSuggestions(errors.NewValidReferences(errors.NounSignals, telemetrytypes.Signal{}.Enum()...))
}
}
@@ -229,7 +272,7 @@ func (c *CompositeQuery) UnmarshalJSON(data []byte) error {
// Valid field names are derived from the struct itself so this stays in
// sync with the schema (and the generated OpenAPI spec) automatically.
fieldNames := binding.JSONFieldNames((*CompositeQuery)(nil))
fieldNames := signozjsonschema.JSONFieldNames((*CompositeQuery)(nil))
validFields := make(map[string]bool, len(fieldNames))
for _, f := range fieldNames {
validFields[f] = true
@@ -243,7 +286,7 @@ func (c *CompositeQuery) UnmarshalJSON(data []byte) error {
field,
).WithAdditional(
"Valid fields are: " + strings.Join(fieldNames, ", "),
).WithSuggestions(errors.SuggestionsOnLevenshteinDistance(field, fieldNames)...)
).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field, errors.NounFields, fieldNames)...)
return unknownFieldErr
}
}
@@ -276,6 +319,40 @@ type VariableItem struct {
Value any `json:"value"`
}
var _ jsonschema.Preparer = VariableItem{}
// PrepareJSONSchema types `value` as a scalar-or-scalar-list instead of an
// untyped {}. The Go field stays `any`; this only shapes the generated schema.
func (VariableItem) PrepareJSONSchema(s *jsonschema.Schema) error {
if _, ok := s.Properties["value"]; !ok {
return nil
}
item := jsonschema.Schema{}
item.OneOf = []jsonschema.SchemaOrBool{
jsonschema.String.ToSchemaOrBool(),
jsonschema.Number.ToSchemaOrBool(),
jsonschema.Boolean.ToSchemaOrBool(),
}
list := jsonschema.Schema{}
list.WithType(jsonschema.Array.Type())
items := jsonschema.Items{}
items.WithSchemaOrBool(item.ToSchemaOrBool())
list.WithItems(items)
value := jsonschema.Schema{}
value.OneOf = []jsonschema.SchemaOrBool{
jsonschema.String.ToSchemaOrBool(),
jsonschema.Number.ToSchemaOrBool(),
jsonschema.Boolean.ToSchemaOrBool(),
list.ToSchemaOrBool(),
}
s.Properties["value"] = value.ToSchemaOrBool()
return nil
}
type QueryRangeRequest struct {
// SchemaVersion is the version of the schema to use for the request payload.
SchemaVersion string `json:"schemaVersion"`
@@ -556,7 +633,7 @@ func (r *QueryRangeRequest) UnmarshalJSON(data []byte) error {
// Valid field names are derived from the struct itself so this stays in
// sync with the schema (and the generated OpenAPI spec) automatically.
fieldNames := binding.JSONFieldNames((*QueryRangeRequest)(nil))
fieldNames := signozjsonschema.JSONFieldNames((*QueryRangeRequest)(nil))
validFields := make(map[string]bool, len(fieldNames))
for _, f := range fieldNames {
validFields[f] = true
@@ -570,7 +647,7 @@ func (r *QueryRangeRequest) UnmarshalJSON(data []byte) error {
field,
).WithAdditional(
"Valid fields are: " + strings.Join(fieldNames, ", "),
).WithSuggestions(errors.SuggestionsOnLevenshteinDistance(field, fieldNames)...)
).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field, errors.NounFields, fieldNames)...)
return unknownFieldErr
}
}

View File

@@ -116,6 +116,26 @@ type Label struct {
Value any `json:"value"`
}
var _ jsonschema.Preparer = Label{}
// PrepareJSONSchema types `value` as a string/number/bool scalar instead of an
// untyped {}. The Go field stays `any`; this only shapes the generated schema.
func (Label) PrepareJSONSchema(s *jsonschema.Schema) error {
if _, ok := s.Properties["value"]; !ok {
return nil
}
value := jsonschema.Schema{}
value.OneOf = []jsonschema.SchemaOrBool{
jsonschema.String.ToSchemaOrBool(),
jsonschema.Number.ToSchemaOrBool(),
jsonschema.Boolean.ToSchemaOrBool(),
}
s.Properties["value"] = value.ToSchemaOrBool()
return nil
}
func GetUniqueSeriesKey(labels []*Label) string {
// Fast path for common cases
if len(labels) == 0 {

View File

@@ -518,7 +518,7 @@ func (q *QueryBuilderQuery[T]) validateOrderByForAggregation() error {
orderId,
).WithAdditional(
fmt.Sprintf("For aggregation queries, order by can only reference group by keys, aggregation aliases/expressions, or aggregation indices. Valid keys are: %s", strings.Join(validKeys, ", ")),
).WithSuggestions(errors.SuggestionsOnLevenshteinDistance(orderKey, validKeys)...)
).WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(orderKey, errors.NounKeys, validKeys)...)
}
}
@@ -712,7 +712,7 @@ func validateQueryEnvelope(envelope QueryEnvelope, opts ...ValidationOption) err
envelope.Type,
).WithAdditional(
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql, trace_operator",
).WithSuggestions(errors.ValidReferences(QueryType{}.Enum()...))
).WithSuggestions(errors.NewValidReferences(errors.NounQueryTypes, QueryType{}.Enum()...))
}
}