mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-26 18:10:27 +01:00
Compare commits
6 Commits
feat/dashb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19712c3579 | ||
|
|
e31683be11 | ||
|
|
1700ad06e6 | ||
|
|
ee3b45b80d | ||
|
|
4771e30c03 | ||
|
|
e933fa74c7 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
}
|
||||
|
||||
.metrics-table-container {
|
||||
padding-bottom: 48px;
|
||||
.ant-table {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
38
pkg/jsonschema/fields.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
45
pkg/modules/authdomain/implauthdomain/getter.go
Normal file
45
pkg/modules/authdomain/implauthdomain/getter.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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)`"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -215,6 +215,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewRecreateUserDashboardPreferenceFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateRecurrenceBoundsFactory(sqlstore),
|
||||
sqlmigration.NewAddDashboardViewFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateSSORoleMappingNamesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
127
pkg/sqlmigration/096_migrate_sso_role_mapping_names.go
Normal file
127
pkg/sqlmigration/096_migrate_sso_role_mapping_names.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)...)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()...))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user