mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-04 10:30:42 +01:00
Compare commits
8 Commits
proto/tree
...
feat/licen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d5c288dd6 | ||
|
|
35abee54b1 | ||
|
|
6a21345203 | ||
|
|
9f128ee00b | ||
|
|
ad06c14557 | ||
|
|
92734c0c2b | ||
|
|
4c752655f3 | ||
|
|
76211b3233 |
2
.github/workflows/goci.yaml
vendored
2
.github/workflows/goci.yaml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: check-semconv-generated-files
|
||||
run: make semconv-check
|
||||
run: go run ./scripts/semconv -check
|
||||
build:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
|
||||
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -62,7 +62,6 @@ jobs:
|
||||
- role
|
||||
- rootuser
|
||||
- savedview
|
||||
- semconvfamilies
|
||||
- serviceaccount
|
||||
- spanmapper
|
||||
- querier_json_body
|
||||
|
||||
4
Makefile
4
Makefile
@@ -237,10 +237,6 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
||||
semconv-generate: ## Regenerate semantic-convention families for Go and TypeScript
|
||||
@go run ./scripts/semconv
|
||||
|
||||
.PHONY: semconv-check
|
||||
semconv-check: ## Fail if the generated semantic-convention files are stale
|
||||
@go run ./scripts/semconv -check
|
||||
|
||||
.PHONY: gen-mocks
|
||||
gen-mocks:
|
||||
@echo ">> Generating mocks"
|
||||
|
||||
@@ -2559,7 +2559,6 @@ components:
|
||||
- factor-api-key
|
||||
- license
|
||||
- subscription
|
||||
- deployment-host
|
||||
- logs
|
||||
- traces
|
||||
- metrics
|
||||
@@ -5642,6 +5641,15 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
LicensetypesGettableLicense:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
LicensetypesPostableLicense:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
type: object
|
||||
LlmpricingruletypesGettablePricingRules:
|
||||
properties:
|
||||
items:
|
||||
@@ -8011,7 +8019,6 @@ components:
|
||||
- logs
|
||||
- metrics
|
||||
- meter
|
||||
- ai_observability
|
||||
type: string
|
||||
SavedviewtypesUpdatableSavedView:
|
||||
properties:
|
||||
@@ -8802,7 +8809,6 @@ components:
|
||||
- span
|
||||
- trace
|
||||
- resource
|
||||
- scope
|
||||
- attribute
|
||||
- body
|
||||
- ""
|
||||
@@ -15470,8 +15476,10 @@ paths:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key: []
|
||||
- tokenizer: []
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get features
|
||||
tags:
|
||||
- features
|
||||
@@ -23943,9 +23951,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- deployment-host:list
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- deployment-host:list
|
||||
- VIEWER
|
||||
summary: Get host info from Zeus.
|
||||
tags:
|
||||
- zeus
|
||||
@@ -23999,9 +24007,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- deployment-host:update
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- deployment-host:update
|
||||
- ADMIN
|
||||
summary: Put host in Zeus for a deployment.
|
||||
tags:
|
||||
- zeus
|
||||
@@ -24062,6 +24070,166 @@ paths:
|
||||
summary: Put profile in Zeus for a deployment.
|
||||
tags:
|
||||
- zeus
|
||||
/api/v3/licenses:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint validates the license key with upstream and activates
|
||||
the license for the organization.
|
||||
operationId: ActivateLicense
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LicensetypesPostableLicense'
|
||||
responses:
|
||||
"202":
|
||||
description: Accepted
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- license:create
|
||||
- tokenizer:
|
||||
- license:create
|
||||
summary: Activate a license.
|
||||
tags:
|
||||
- licenses
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint refreshes the active license of the organization
|
||||
from upstream.
|
||||
operationId: RefreshLicense
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- license:update
|
||||
- tokenizer:
|
||||
- license:update
|
||||
summary: Refresh the active license.
|
||||
tags:
|
||||
- licenses
|
||||
/api/v3/licenses/active:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets the active license of the organization.
|
||||
operationId: GetActiveLicense
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LicensetypesGettableLicense'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key: []
|
||||
- tokenizer: []
|
||||
summary: Get the active license.
|
||||
tags:
|
||||
- licenses
|
||||
/api/v3/metrics/dashboards:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# Tree-model prototype (proto/tree-model)
|
||||
|
||||
A local prototype of the proposed end-state architecture, built on
|
||||
`feat/semconv-phase2-signals` with byte-parity as the acceptance bar: every
|
||||
golden and every integration case must produce identical SQL. The prototype
|
||||
exists to let a reviewer judge the model empirically — how small the generic
|
||||
core really is, how large the per-signal residue really is, and where the
|
||||
line between them falls.
|
||||
|
||||
## What became generic
|
||||
|
||||
### The term compiler — `pkg/querybuilder/term.go`
|
||||
|
||||
`CompileTerm` is the flow of one filter term, written once:
|
||||
|
||||
1. resolve the evidence (`ResolveLogicalFields`, ambiguity warning),
|
||||
2. amend it with intrinsic storage (`TermSchema.AmendEvidence`),
|
||||
3. synthesize when it is empty (`TermSchema.Synthesize`),
|
||||
4. apply the resource-filter policy (`SkipResourcePolicy`),
|
||||
5. compile every field (`TermSchema.CompileField`), collecting warnings in
|
||||
a fixed order.
|
||||
|
||||
Six condition builders became delegates to it: traces, logs, metrics, audit,
|
||||
rule state history, and the resource filter. Each keeps a term-level
|
||||
intercept in front where one exists (`search()` on logs, the function-operator
|
||||
reject or skip), and implements the three `TermSchema` methods. The six
|
||||
hand-rolled copies of the flow are gone; a change to the flow — the order of
|
||||
warnings, the synthesized-exemption of the resource drop — now has one home.
|
||||
|
||||
`CompileFieldWithSharedOperators` is the canonical `CompileField`: the shared
|
||||
operator switch (`LogicalFamilyCondition`) over the merged or single value
|
||||
expression, with the default exists guard for positive operators. Traces uses
|
||||
it for every field; logs and metrics use it for families.
|
||||
|
||||
### The coerced column renderer — `pkg/querybuilder/column.go`
|
||||
|
||||
`RenderCoercedColumn` renders resolved fields for group-by, order, and
|
||||
aggregation arguments: every field exists-guarded and coerced in one
|
||||
`multiIf`, with the NULL group preserved. Traces and logs delegate their
|
||||
coerced modes to it through three leaf questions (`ColumnSchema`):
|
||||
|
||||
- `RawRead` — the uncoerced read of one field (logs overrides the legacy
|
||||
body path),
|
||||
- `Uncoerced` — the coercion exemption (traces time columns, logs legacy
|
||||
body reads),
|
||||
- `BareCandidate` — the fields that cannot sit inside `multiIf` (arrays).
|
||||
|
||||
## What stayed per-signal, and why
|
||||
|
||||
- **The single-key operator switches.** Metrics coerces collisions with its
|
||||
own casts (`toFloat64OrNull`, labels-as-String), logs carries the body
|
||||
machinery and the body-column index forms, audit and rule state history
|
||||
have their own switches. Unifying them changes SQL, so byte-parity forbids
|
||||
it here. This is the real distance to "one operator switch": it exists for
|
||||
families today, and extending it to singles is a re-pinning exercise, not a
|
||||
refactor.
|
||||
- **The resource-filter compile.** Index hints are woven into every operator
|
||||
case with per-operator polarity rules; the whole field compile stays its
|
||||
own (`CompileField` overrides wholesale).
|
||||
- **The raw-select tails.** Traces and logs raw-select shapes differ in more
|
||||
dimensions than they share (guard tests, stringification, collision
|
||||
application), so `ColumnExpressionFor`'s Unspecified mode keeps its
|
||||
per-signal tails. A generic raw renderer would need more knobs than it
|
||||
removes lines.
|
||||
- **The column resolution orders.** Which candidates a column stage sees —
|
||||
the storage probe, the metadata lookups, the swap/append of resolved
|
||||
spellings — keeps its pinned per-signal order in the mappers. Moving it
|
||||
into a generic `Resolve` is the model-B step proper, and it needs the
|
||||
stage-aware synthesis asymmetry (filters synthesize by operand, columns
|
||||
do not) carried as data.
|
||||
- **Metadata.** Its merged-value semantics bind query parameters inside
|
||||
presence guards, which the arg-free `ExistsFor` contract cannot express.
|
||||
Untouched, as declared.
|
||||
|
||||
## The review round (applied)
|
||||
|
||||
- The rule-state-history operator forms are pinned in the commit BEFORE the
|
||||
port (it had no tests and diverges most: IN binds the whole list to one
|
||||
placeholder, exists renders two ways), so the port proves parity.
|
||||
- Audit refuses families in its delegate, before the resource drop, so the
|
||||
wiring tripwire stays loud instead of dropping a resource-context family
|
||||
silently.
|
||||
- The logs coerced-column schema value carries the body mode; the flag reads
|
||||
one time per call. RawRead's dummy-value parameter is a legacy-body shim
|
||||
and leaves with the legacy body.
|
||||
- The compile context is named `CompileScope`: "scope" alone collides with
|
||||
the span search scope, the vocabulary member scope, and the resolution
|
||||
scope.
|
||||
|
||||
## Findings a reviewer should weigh
|
||||
|
||||
1. The condition-side consolidation is real and cheap: six flows became one,
|
||||
with per-signal surface of exactly three methods each, and every golden
|
||||
stayed byte-identical without adjustment.
|
||||
2. The column side splits: coerced modes unify on three knobs; raw select
|
||||
does not pay for unification at today's shapes.
|
||||
3. The full tree model (candidates as data, one `Resolve`, generic raw
|
||||
rendering) requires shape reconciliation that byte-parity forbids —
|
||||
confirming it should ride a forcing feature (the ValueMap reader) with a
|
||||
re-pinning round, not a standalone refactor.
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
@@ -42,7 +42,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
LicensingHandler: licensing.NewHandler(signoz.Licensing),
|
||||
Signoz: signoz,
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
}, config)
|
||||
@@ -67,19 +67,14 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
// note: add ee override methods first
|
||||
|
||||
// routes available only in ee version
|
||||
router.HandleFunc("/api/v1/features", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||
|
||||
// base overrides
|
||||
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingHandler.Checkout)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
|
||||
|
||||
// v3
|
||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingHandler.Portal)).Methods(http.MethodPost)
|
||||
|
||||
// v4
|
||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||
|
||||
268
frontend/src/api/generated/services/licenses/index.ts
Normal file
268
frontend/src/api/generated/services/licenses/index.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
GetActiveLicense200,
|
||||
LicensetypesPostableLicenseDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* This endpoint validates the license key with upstream and activates the license for the organization.
|
||||
* @summary Activate a license.
|
||||
*/
|
||||
export const activateLicense = (
|
||||
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v3/licenses`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: licensetypesPostableLicenseDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getActivateLicenseMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof activateLicense>>,
|
||||
TError,
|
||||
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof activateLicense>>,
|
||||
TError,
|
||||
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['activateLicense'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof activateLicense>>,
|
||||
{ data?: BodyType<LicensetypesPostableLicenseDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return activateLicense(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ActivateLicenseMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof activateLicense>>
|
||||
>;
|
||||
export type ActivateLicenseMutationBody =
|
||||
| BodyType<LicensetypesPostableLicenseDTO>
|
||||
| undefined;
|
||||
export type ActivateLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Activate a license.
|
||||
*/
|
||||
export const useActivateLicense = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof activateLicense>>,
|
||||
TError,
|
||||
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof activateLicense>>,
|
||||
TError,
|
||||
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getActivateLicenseMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint refreshes the active license of the organization from upstream.
|
||||
* @summary Refresh the active license.
|
||||
*/
|
||||
export const refreshLicense = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v3/licenses`,
|
||||
method: 'PUT',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRefreshLicenseMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['refreshLicense'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
void
|
||||
> = () => {
|
||||
return refreshLicense();
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type RefreshLicenseMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof refreshLicense>>
|
||||
>;
|
||||
|
||||
export type RefreshLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Refresh the active license.
|
||||
*/
|
||||
export const useRefreshLicense = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof refreshLicense>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getRefreshLicenseMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint gets the active license of the organization.
|
||||
* @summary Get the active license.
|
||||
*/
|
||||
export const getActiveLicense = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<GetActiveLicense200>({
|
||||
url: `/api/v3/licenses/active`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetActiveLicenseQueryKey = () => {
|
||||
return [`/api/v3/licenses/active`] as const;
|
||||
};
|
||||
|
||||
export const getGetActiveLicenseQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getActiveLicense>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getActiveLicense>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetActiveLicenseQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getActiveLicense>>> = ({
|
||||
signal,
|
||||
}) => getActiveLicense(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getActiveLicense>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetActiveLicenseQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getActiveLicense>>
|
||||
>;
|
||||
export type GetActiveLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get the active license.
|
||||
*/
|
||||
|
||||
export function useGetActiveLicense<
|
||||
TData = Awaited<ReturnType<typeof getActiveLicense>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getActiveLicense>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetActiveLicenseQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the active license.
|
||||
*/
|
||||
export const invalidateGetActiveLicense = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetActiveLicenseQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
@@ -2175,7 +2175,6 @@ export enum CoretypesKindDTO {
|
||||
'factor-api-key' = 'factor-api-key',
|
||||
license = 'license',
|
||||
subscription = 'subscription',
|
||||
'deployment-host' = 'deployment-host',
|
||||
logs = 'logs',
|
||||
traces = 'traces',
|
||||
metrics = 'metrics',
|
||||
@@ -3493,7 +3492,6 @@ export enum TelemetrytypesFieldContextDTO {
|
||||
span = 'span',
|
||||
trace = 'trace',
|
||||
resource = 'resource',
|
||||
scope = 'scope',
|
||||
attribute = 'attribute',
|
||||
body = 'body',
|
||||
'' = '',
|
||||
@@ -7173,6 +7171,21 @@ export interface InframonitoringtypesVolumesDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type LicensetypesGettableLicenseDTOAnyOf = { [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type LicensetypesGettableLicenseDTO =
|
||||
LicensetypesGettableLicenseDTOAnyOf | null;
|
||||
|
||||
export interface LicensetypesPostableLicenseDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
@@ -9023,7 +9036,6 @@ export enum SavedviewtypesSourceDTO {
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
ai_observability = 'ai_observability',
|
||||
}
|
||||
export interface SavedviewtypesSavedViewSpecDTO {
|
||||
display?: SavedviewtypesDisplayDTO;
|
||||
@@ -12402,6 +12414,14 @@ export type GetHosts200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetActiveLicense200 = {
|
||||
data: LicensetypesGettableLicenseDTO | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMetricDashboardsV2Params = {
|
||||
/**
|
||||
* @type string
|
||||
|
||||
@@ -7,8 +7,9 @@ import axios from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
@@ -68,7 +69,9 @@ function ExplorerCard({
|
||||
setIsOpen(newOpen);
|
||||
};
|
||||
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
export const ExploreHeaderToolTip = {
|
||||
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
|
||||
text: 'More details on how to use query builder',
|
||||
@@ -7,3 +9,5 @@ export const SaveButtonText = {
|
||||
SAVE_AS_NEW_VIEW: 'Save as new view',
|
||||
SAVE_VIEW: 'Save view',
|
||||
};
|
||||
|
||||
export type QuerySearchParamNames = QueryParams.viewName | QueryParams.viewKey;
|
||||
|
||||
@@ -241,29 +241,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
currentQuery.builder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula
|
||||
filterConfigs={filterConfigs}
|
||||
query={query}
|
||||
formula={formula}
|
||||
index={index}
|
||||
isAdditionalFilterEnable={false}
|
||||
isQBV2
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula
|
||||
filterConfigs={filterConfigs}
|
||||
query={query}
|
||||
formula={formula}
|
||||
index={index}
|
||||
isAdditionalFilterEnable={false}
|
||||
isQBV2
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
@@ -291,7 +290,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentQuery.builder.queryFormulas?.map((formula) => (
|
||||
{currentQuery.builder.queryFormulas.map((formula) => (
|
||||
<div key={formula.queryName} className="formula-name">
|
||||
{formula.queryName}
|
||||
</div>
|
||||
|
||||
@@ -212,32 +212,6 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
expect(handleRunQueryMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not crash when builder.queryFormulas/queryTraceOperator are missing (partial/legacy query)', () => {
|
||||
const currentQueryBase = baseQBContext.currentQuery as Query;
|
||||
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
...baseQBContext,
|
||||
currentQuery: {
|
||||
...currentQueryBase,
|
||||
builder: {
|
||||
queryData: currentQueryBase.builder.queryData,
|
||||
queryFormulas: undefined as unknown as [],
|
||||
queryTraceOperator: undefined as unknown as [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />),
|
||||
).not.toThrow();
|
||||
|
||||
// query list still renders from queryData, formulas block is skipped
|
||||
expect(document.querySelector('.query-names-section')).toBeInTheDocument();
|
||||
expect(
|
||||
document.querySelector('.qb-formulas-container'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('fx button is disabled when functions already exist', () => {
|
||||
const currentQueryBase = baseQBContext.currentQuery as Query;
|
||||
const supersetQueryBase = baseQBContext.supersetQuery as Query;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDeny,
|
||||
@@ -110,7 +110,10 @@ describe('ServiceAccountDrawer — permissions', () => {
|
||||
it('shows PermissionDeniedCallout in Keys tab when list-keys permission is denied', async () => {
|
||||
server.use(setupAuthzDeny(APIKeyListPermission));
|
||||
|
||||
renderDrawer({ account: 'sa-1', tab: 'keys' });
|
||||
renderDrawer();
|
||||
await screen.findByDisplayValue('CI Bot');
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: /keys/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list:factor-api-key/)).toBeInTheDocument();
|
||||
|
||||
@@ -1,82 +1,32 @@
|
||||
// Code generated by scripts/semconv. DO NOT EDIT.
|
||||
|
||||
// An empty contexts/signals/applyToMetrics array places no constraint on
|
||||
// that axis.
|
||||
export type SemconvMember = {
|
||||
readonly name: string;
|
||||
readonly contexts: readonly string[];
|
||||
readonly signals: readonly string[];
|
||||
readonly applyToMetrics: readonly string[];
|
||||
};
|
||||
|
||||
export type SemconvFamily = {
|
||||
readonly current: string;
|
||||
readonly old: readonly string[];
|
||||
readonly kind: 'attribute' | 'metric';
|
||||
readonly members: readonly SemconvMember[];
|
||||
readonly contexts: readonly string[];
|
||||
readonly signals: readonly string[];
|
||||
readonly applyToMetrics: readonly string[];
|
||||
readonly valueMap: Readonly<Record<string, string>>;
|
||||
};
|
||||
|
||||
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
|
||||
{
|
||||
current: 'container.cpu.usage',
|
||||
kind: 'metric',
|
||||
members: [
|
||||
{
|
||||
name: 'container.cpu.utilization',
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
},
|
||||
],
|
||||
current: 'db.system.name',
|
||||
old: ['db.system'],
|
||||
kind: 'attribute',
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
valueMap: {},
|
||||
},
|
||||
{
|
||||
current: 'deployment.environment.name',
|
||||
old: ['deployment.environment'],
|
||||
kind: 'attribute',
|
||||
members: [
|
||||
{
|
||||
name: 'deployment.environment',
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
},
|
||||
],
|
||||
contexts: ['attribute', 'resource'],
|
||||
signals: ['logs', 'metrics', 'traces'],
|
||||
valueMap: {},
|
||||
},
|
||||
{
|
||||
current: 'k8s.node.cpu.usage',
|
||||
kind: 'metric',
|
||||
members: [
|
||||
{
|
||||
name: 'k8s.node.cpu.utilization',
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
},
|
||||
],
|
||||
contexts: [],
|
||||
signals: [],
|
||||
valueMap: {},
|
||||
},
|
||||
{
|
||||
current: 'k8s.pod.cpu.usage',
|
||||
kind: 'metric',
|
||||
members: [
|
||||
{
|
||||
name: 'k8s.pod.cpu.utilization',
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
},
|
||||
],
|
||||
contexts: [],
|
||||
signals: [],
|
||||
applyToMetrics: [],
|
||||
valueMap: {},
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -10,7 +10,6 @@ const fieldContextToSuggestionMap: Record<
|
||||
[TelemetrytypesFieldContextDTO.attribute]: 'attribute',
|
||||
// no maps for the following values on suggestion context
|
||||
[TelemetrytypesFieldContextDTO.trace]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.scope]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.body]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.metric]: undefined,
|
||||
[TelemetrytypesFieldContextDTO.log]: undefined,
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { useSaveView } from 'hooks/saveViews/useSaveView';
|
||||
@@ -287,7 +287,8 @@ function ExplorerOptions({
|
||||
|
||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||
|
||||
const extraData = viewsData?.data?.data?.find(
|
||||
(view) => view.id === viewKey,
|
||||
|
||||
@@ -53,24 +53,17 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
|
||||
};
|
||||
|
||||
export const getSelectedQueryOptions = (
|
||||
queries:
|
||||
| Array<
|
||||
| IBuilderQuery
|
||||
| IBuilderTraceOperator
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery
|
||||
>
|
||||
| undefined
|
||||
| null,
|
||||
): SelectProps['options'] => {
|
||||
if (!queries) {
|
||||
return [];
|
||||
}
|
||||
return queries
|
||||
queries: Array<
|
||||
| IBuilderQuery
|
||||
| IBuilderTraceOperator
|
||||
| IBuilderFormula
|
||||
| IClickHouseQuery
|
||||
| IPromQLQuery
|
||||
>,
|
||||
): SelectProps['options'] =>
|
||||
queries
|
||||
.filter((query) => !query.disabled)
|
||||
.map((query) => ({
|
||||
label: 'queryName' in query ? query.queryName : query.name,
|
||||
value: 'queryName' in query ? query.queryName : query.name,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.explorer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-2) var(--spacing-0);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
.trace-explorer-header {
|
||||
.trace-explorer-run-query {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
margin: 8px 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-outlined-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
border-right: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer-header.single-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.traces-explorer-views {
|
||||
padding: 8px;
|
||||
padding-bottom: 60px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-search-view-container {
|
||||
padding: 8px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background: var(--l2-background) !important;
|
||||
height: 34px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer-list-view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.trace-explorer-traces-view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.trace-explorer-table-view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.trace-explorer-time-series-view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.trace-explorer-page {
|
||||
display: flex;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
|
||||
.filter {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
border-right: 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background-color: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer {
|
||||
width: 100%;
|
||||
background: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
.trace-explorer.filters-expanded {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
@@ -1,373 +1,13 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import {
|
||||
getExportQueryData,
|
||||
getQueryByPanelType,
|
||||
} from 'container/TracesExplorer/explorerUtils';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
ICurrentQueryData,
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import {
|
||||
tracesAddFilterAction,
|
||||
tracesChangeViewAction,
|
||||
tracesRunQueryAction,
|
||||
tracesSaveViewAction,
|
||||
} from 'pages/TracesExplorer/aiActions';
|
||||
import { Warning } from 'types/api';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import {
|
||||
explorerViewToPanelType,
|
||||
getExplorerViewFromUrl,
|
||||
} from 'utils/explorerUtils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TOOLBAR_VIEWS } from './constants';
|
||||
import ListView from './ListView/ListView';
|
||||
import { defaultSelectedColumns } from './ListView/configs';
|
||||
import QuerySection from './QuerySection/QuerySection';
|
||||
import TableView from './TableView/TableView';
|
||||
import TimeSeriesView from './TimeSeriesView/TimeSeriesView';
|
||||
import TracesView from './TracesView/TracesView';
|
||||
|
||||
import './Explorer.styles.scss';
|
||||
import styles from './Explorer.module.scss';
|
||||
|
||||
// Shell for the AI Observability Explorer tab. Owns the
|
||||
// /ai-observability/explorer route and is intentionally empty for now: the
|
||||
// query builder + results surface land in a follow-up.
|
||||
function Explorer(): JSX.Element {
|
||||
const {
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
handleRunQuery,
|
||||
stagedQuery,
|
||||
handleSetConfig,
|
||||
currentQuery,
|
||||
handleSetQueryData,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
initialOptions: {
|
||||
selectColumns: defaultSelectedColumns,
|
||||
},
|
||||
});
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
const listQueryKeyRef = useRef<any>();
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingQueries) {
|
||||
setIsCancelled(false);
|
||||
}
|
||||
}, [isLoadingQueries]);
|
||||
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
if (listQueryKeyRef.current) {
|
||||
queryClient.cancelQueries(listQueryKeyRef.current);
|
||||
}
|
||||
setIsCancelled(true);
|
||||
// Reset loading state — the active view unmounts when cancelled, so no
|
||||
// child will call setIsLoadingQueries(false) otherwise.
|
||||
setIsLoadingQueries(false);
|
||||
}, [queryClient]);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||
);
|
||||
|
||||
const [warning, setWarning] = useState<Warning | undefined>();
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const getExportToDashboardLink = useGetExportToDashboardLink();
|
||||
|
||||
const handleChangeSelectedView = useCallback(
|
||||
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
|
||||
handleSetConfig(explorerViewToPanelType[view], DataSource.TRACES);
|
||||
|
||||
setSelectedView(view);
|
||||
|
||||
handleExplorerTabChange(
|
||||
explorerViewToPanelType[view],
|
||||
querySearchParameters,
|
||||
);
|
||||
},
|
||||
[handleExplorerTabChange, handleSetConfig],
|
||||
);
|
||||
|
||||
// ─── AI Assistant page actions (only when license feature is on) ───────────
|
||||
const aiActions = useMemo(
|
||||
() =>
|
||||
isAIAssistantEnabled
|
||||
? [
|
||||
tracesRunQueryAction({
|
||||
currentQuery,
|
||||
handleSetQueryData,
|
||||
redirectWithQueryBuilderData,
|
||||
}),
|
||||
tracesAddFilterAction({
|
||||
currentQuery,
|
||||
handleSetQueryData,
|
||||
redirectWithQueryBuilderData,
|
||||
}),
|
||||
tracesChangeViewAction({
|
||||
onChangeView: (view) => handleChangeSelectedView(view as ExplorerViews),
|
||||
}),
|
||||
tracesSaveViewAction({
|
||||
// POC stub — logs a save request; wire to real API when available
|
||||
onSaveView: async (name) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[AI Assistant] Save view requested:', name);
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
isAIAssistantEnabled,
|
||||
currentQuery,
|
||||
handleSetQueryData,
|
||||
redirectWithQueryBuilderData,
|
||||
handleChangeSelectedView,
|
||||
],
|
||||
);
|
||||
usePageActions('traces-explorer', aiActions);
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
getQueryByPanelType(
|
||||
stagedQuery || initialQueriesMap.traces,
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
),
|
||||
[stagedQuery, panelType],
|
||||
);
|
||||
|
||||
const handleExport = useCallback(
|
||||
(dashboard: ExportDashboard | null, isNewDashboard?: boolean): void => {
|
||||
if (!dashboard || !panelType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
|
||||
? panelType
|
||||
: PANEL_TYPES.TIME_SERIES;
|
||||
|
||||
const widgetId = v4();
|
||||
|
||||
const query = getExportQueryData(
|
||||
exportDefaultQuery,
|
||||
panelTypeParam,
|
||||
options,
|
||||
);
|
||||
|
||||
logEvent('Traces Explorer: Add to dashboard successful', {
|
||||
panelType,
|
||||
isNewDashboard,
|
||||
dashboardName: dashboard?.title,
|
||||
});
|
||||
|
||||
const dashboardEditView = getExportToDashboardLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
if (dashboardEditView) {
|
||||
safeNavigate(dashboardEditView);
|
||||
}
|
||||
},
|
||||
[
|
||||
exportDefaultQuery,
|
||||
panelType,
|
||||
safeNavigate,
|
||||
options,
|
||||
getExportToDashboardLink,
|
||||
],
|
||||
);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
logEvent('Traces Explorer: Page visited', {});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isFilterApplied = useMemo(() => {
|
||||
// if any of the non-disabled queries has filters applied, return true
|
||||
const result = stagedQuery?.builder?.queryData?.filter(
|
||||
(item) => !isEmpty(item.filters?.items) && !item.disabled,
|
||||
);
|
||||
return !!result?.length;
|
||||
}, [stagedQuery]);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div
|
||||
className="trace-explorer-page"
|
||||
data-testid="llm-observability-explorer"
|
||||
>
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
className={cx('trace-explorer', {
|
||||
'filters-expanded': isOpen,
|
||||
})}
|
||||
>
|
||||
<div className="trace-explorer-header">
|
||||
<Toolbar
|
||||
showAutoRefresh
|
||||
leftActions={
|
||||
<LeftToolbarActions
|
||||
showFilter={isOpen}
|
||||
handleFilterVisibilityChange={(): void => setOpen(!isOpen)}
|
||||
items={TOOLBAR_VIEWS}
|
||||
selectedView={selectedView}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
}
|
||||
warningElement={
|
||||
!isEmpty(warning) ? <WarningPopover warningData={warning} /> : <div />
|
||||
}
|
||||
rightActions={
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={(): void => {
|
||||
setIsCancelled(false);
|
||||
handleRunQuery();
|
||||
}}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||
<div className="query-section-container">
|
||||
<QuerySection />
|
||||
</div>
|
||||
</ExplorerCard>
|
||||
|
||||
<div className="traces-explorer-views">
|
||||
{isCancelled && (
|
||||
<QueryCancelledPlaceholder subText='Click "Run Query" to load traces.' />
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.LIST && (
|
||||
<div className="trace-explorer-list-view">
|
||||
<ListView
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TRACE && (
|
||||
<div className="trace-explorer-traces-view">
|
||||
<TracesView
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TIMESERIES && (
|
||||
<div className="trace-explorer-time-series-view">
|
||||
<TimeSeriesView
|
||||
dataSource={DataSource.TRACES}
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TABLE && (
|
||||
<div className="trace-explorer-table-view">
|
||||
<TableView
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
sourcepage={DataSource.TRACES}
|
||||
onExport={handleExport}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
<div className={styles.explorer} data-testid="llm-observability-explorer">
|
||||
<div className={styles.placeholder}>Explorer coming soon.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: calc(100vh - 240px);
|
||||
min-height: 400px;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
.trace-explorer-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.order-by-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.order-by-label {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 133.333% */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.order-by-select {
|
||||
width: 100px;
|
||||
|
||||
.ant-select-selector {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import {
|
||||
getTraceLink,
|
||||
transformSpanRows,
|
||||
} from 'container/TracesExplorer/ListView/utils';
|
||||
import {
|
||||
getFieldColumn,
|
||||
TracesTableRow,
|
||||
} from 'container/TracesExplorer/TracesTable/getFieldColumn';
|
||||
import TracesTable from 'container/TracesExplorer/TracesTable/TracesTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getDefaultPaginationConfig } from 'hooks/queryPagination/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { ArrowUp10, Minus } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
defaultSelectedColumns,
|
||||
PER_PAGE_OPTIONS,
|
||||
TIMESTAMP_FIELD,
|
||||
} from './configs';
|
||||
import './ListView.styles.scss';
|
||||
|
||||
import styles from './ListView.module.scss';
|
||||
|
||||
interface ListViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
function ListView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: ListViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType: panelTypeFromQueryBuilder } =
|
||||
useQueryBuilder();
|
||||
|
||||
const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST;
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
loading: timeRangeUpdateLoading,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
initialOptions: {
|
||||
selectColumns: defaultSelectedColumns,
|
||||
},
|
||||
});
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
const paginationConfig =
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
// Stable sorted-name signature for the queryKey.
|
||||
// - Drag updates selectColumns; raw queryKey would churn on reorder.
|
||||
// - Trace API fetches only listed columns → add/remove must refetch.
|
||||
// - Sorted-name signature: stable on reorder, changes on add/remove.
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
(options?.selectColumns ?? [])
|
||||
.map((c) => c.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[options?.selectColumns],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
orderBy,
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isFetching, isLoading, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: requestQuery,
|
||||
graphType: panelType,
|
||||
selectedTime: 'GLOBAL_TIME' as const,
|
||||
globalSelectedInterval: globalSelectedTime as CustomTimeType,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationConfig,
|
||||
selectColumns: options?.selectColumns,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled:
|
||||
// don't make api call while the time range state in redux is loading
|
||||
!timeRangeUpdateLoading &&
|
||||
!!stagedQuery &&
|
||||
panelType === PANEL_TYPES.LIST &&
|
||||
!!options?.selectColumns?.length,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
const queryTableDataResult = data?.payload?.data?.newResult?.data?.result;
|
||||
const queryTableData = useMemo(
|
||||
() => queryTableDataResult || [],
|
||||
[queryTableDataResult],
|
||||
);
|
||||
|
||||
const columns = useMemo<TableColumnDef<TracesTableRow>[]>(() => {
|
||||
const fields = [
|
||||
TIMESTAMP_FIELD,
|
||||
...(options?.selectColumns ?? []).filter(
|
||||
(field) => field.name !== TIMESTAMP_FIELD.name,
|
||||
),
|
||||
];
|
||||
return fields.map((field) => getFieldColumn(field));
|
||||
}, [options?.selectColumns]);
|
||||
|
||||
const rows = useMemo(
|
||||
() => transformSpanRows(queryTableData),
|
||||
[queryTableData],
|
||||
);
|
||||
|
||||
const handleColumnOrderChange = useCallback(
|
||||
(reordered: TableColumnDef<TracesTableRow>[]): void => {
|
||||
config?.addColumn?.onReorder(reordered.map((column) => column.id));
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
const handleOrderChange = useCallback((value: string) => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isFetching && !isError && rows.length !== 0) {
|
||||
void logEvent('Traces Explorer: Data present', {
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, rows, panelType]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className="trace-explorer-controls">
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={handleOrderChange}
|
||||
dataSource={DataSource.TRACES}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
selectedColumns={options?.selectColumns}
|
||||
/>
|
||||
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
totalCount={rows.length}
|
||||
config={config}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TracesTable
|
||||
data={rows}
|
||||
columns={columns}
|
||||
panelType="LIST"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onColumnRemove={config?.addColumn?.onRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ListView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(ListView);
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
export const defaultSelectedColumns: string[] = [
|
||||
'service.name',
|
||||
'name',
|
||||
'duration_nano',
|
||||
'http_method',
|
||||
'response_status_code',
|
||||
'timestamp',
|
||||
];
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
|
||||
// Pinned timestamp column
|
||||
export const TIMESTAMP_FIELD = {
|
||||
name: 'timestamp',
|
||||
fieldContext: 'span',
|
||||
} as TelemetryFieldKey;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function QuerySection(): JSX.Element {
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const isList = panelTypes === PANEL_TYPES.LIST;
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: false, isDisabled: false },
|
||||
limit: { isHidden: isList, isDisabled: true },
|
||||
having: { isHidden: isList, isDisabled: true },
|
||||
};
|
||||
|
||||
return config;
|
||||
}, [panelTypes]);
|
||||
|
||||
const renderOrderBy = useCallback(
|
||||
({ query, onChange }: OrderByFilterProps) => (
|
||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const queryComponents = useMemo((): QueryBuilderProps['queryComponents'] => {
|
||||
const shouldRenderCustomOrderBy =
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE;
|
||||
|
||||
return {
|
||||
...(shouldRenderCustomOrderBy ? { renderOrderBy } : {}),
|
||||
};
|
||||
}, [panelTypes, renderOrderBy]);
|
||||
|
||||
const isListViewPanel = useMemo(
|
||||
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
|
||||
[panelTypes],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={isListViewPanel}
|
||||
showTraceOperator
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
showOnlyWhereClause={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(QuerySection);
|
||||
@@ -1,7 +0,0 @@
|
||||
.traces-table-view-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Space } from 'antd';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ExportMenu from 'components/ExportMenu/ExportMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import './TableView.styles.scss';
|
||||
|
||||
function TableView({
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: {
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<any>;
|
||||
}): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: panelType || PANEL_TYPES.TABLE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TABLE,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
const queryTableData = useMemo(
|
||||
() =>
|
||||
data?.payload?.data?.newResult?.data?.result ||
|
||||
data?.payload.data.result ||
|
||||
[],
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
return (
|
||||
<Space.Compact block direction="vertical">
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
{!isError && data && (
|
||||
<div className="traces-table-view-header">
|
||||
<ExportMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
data={data}
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
fileName="traces-table"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isError && (
|
||||
<QueryTable
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
queryTableData={queryTableData as QueryDataV3[]}
|
||||
loading={isLoading}
|
||||
sticky
|
||||
/>
|
||||
)}
|
||||
</Space.Compact>
|
||||
);
|
||||
}
|
||||
|
||||
TableView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TableView);
|
||||
@@ -1,8 +0,0 @@
|
||||
.trace-explorer-time-series-view-container {
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import './TimeSeriesView.styles.scss';
|
||||
|
||||
function TimeSeriesViewContainer({
|
||||
dataSource = DataSource.TRACES,
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const isValidToConvertToMs = useMemo(() => {
|
||||
const isValid: boolean[] = [];
|
||||
|
||||
currentQuery.builder.queryData.forEach(
|
||||
({ aggregateAttribute, aggregateOperator }) => {
|
||||
const isExistDurationNanoAttribute =
|
||||
aggregateAttribute?.key === 'durationNano' ||
|
||||
aggregateAttribute?.key === 'duration_nano';
|
||||
|
||||
const isCountOperator =
|
||||
aggregateOperator === 'count' || aggregateOperator === 'count_distinct';
|
||||
|
||||
isValid.push(!isCountOperator && isExistDurationNanoAttribute);
|
||||
},
|
||||
);
|
||||
|
||||
return isValid.every(Boolean);
|
||||
}, [currentQuery]);
|
||||
|
||||
const defaultUnit = isValidToConvertToMs ? 'ms' : 'short';
|
||||
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit(defaultUnit);
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap[dataSource],
|
||||
graphType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource,
|
||||
},
|
||||
},
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
const responseData = useMemo(
|
||||
() => (isValidToConvertToMs ? convertDataValueToMs(data) : data),
|
||||
[data, isValidToConvertToMs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
return (
|
||||
<div className="trace-explorer-time-series-view-container">
|
||||
<TimeSeriesView
|
||||
isFilterApplied={isFilterApplied}
|
||||
isError={isError}
|
||||
error={error as APIError}
|
||||
isLoading={isLoading || isFetching}
|
||||
data={responseData}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onYAxisUnitChange={onUnitChange}
|
||||
dataSource={dataSource}
|
||||
setWarning={setWarning}
|
||||
allowExport
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimeSeriesViewProps {
|
||||
dataSource?: DataSource;
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<any>;
|
||||
}
|
||||
|
||||
TimeSeriesViewContainer.defaultProps = {
|
||||
dataSource: DataSource.TRACES,
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default TimeSeriesViewContainer;
|
||||
@@ -1,15 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
// Page chain isn't a flex column, so anchor the virtualized table against the viewport.
|
||||
height: calc(100vh - 240px);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.actionsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import { getTraceLink } from 'container/TracesExplorer/ListView/utils';
|
||||
import { TracesTableRow } from 'container/TracesExplorer/TracesTable/getFieldColumn';
|
||||
import TracesTable from 'container/TracesExplorer/TracesTable/TracesTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import { columns, PER_PAGE_OPTIONS } from './configs';
|
||||
import styles from './TracesView.module.scss';
|
||||
|
||||
interface TracesViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
function TracesView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TracesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces),
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
[
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: transformedQuery,
|
||||
graphType: panelType || PANEL_TYPES.TRACE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationQueryData,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TRACE,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
const responseData = data?.payload?.data?.newResult?.data?.result[0]?.list;
|
||||
|
||||
const rows = useMemo<TracesTableRow[]>(
|
||||
() =>
|
||||
(responseData ?? []).map((item) => {
|
||||
const row = item.data;
|
||||
return { ...row, id: row.trace_id };
|
||||
}) as TracesTableRow[],
|
||||
[responseData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isFetching && !isError && rows.length !== 0) {
|
||||
void logEvent('Traces Explorer: Data present', {
|
||||
panelType: 'TRACE',
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, rows.length]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.actionsContainer}>
|
||||
<Typography>
|
||||
This tab only shows Root Spans. More details
|
||||
<Typography.Link href={DOCLINKS.TRACES_DETAILS_LINK} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>
|
||||
</Typography>
|
||||
|
||||
<div className="trace-explorer-controls">
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
panelType={PANEL_TYPES.TRACE}
|
||||
/>
|
||||
|
||||
<TraceExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={rows.length}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TracesTable
|
||||
data={rows}
|
||||
columns={columns}
|
||||
respectColumnOrder
|
||||
panelType="TRACE"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TracesView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TracesView);
|
||||
@@ -1,25 +0,0 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import {
|
||||
getFieldColumn,
|
||||
TracesTableRow,
|
||||
} from 'container/TracesExplorer/TracesTable/getFieldColumn';
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
|
||||
const TRACE_FIELDS = [
|
||||
{ name: 'service.name', fieldContext: 'resource' },
|
||||
{ name: 'name' },
|
||||
{ name: 'duration_nano' },
|
||||
{ name: 'span_count' },
|
||||
{ name: 'trace_id' },
|
||||
] as TelemetryFieldKey[];
|
||||
|
||||
export const columns: TableColumnDef<TracesTableRow>[] = TRACE_FIELDS.map(
|
||||
(field) => ({
|
||||
...getFieldColumn(field),
|
||||
enableRemove: false,
|
||||
canBeHidden: false,
|
||||
}),
|
||||
);
|
||||
@@ -1,36 +0,0 @@
|
||||
export const TOOLBAR_VIEWS = {
|
||||
list: {
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
show: true,
|
||||
key: 'list',
|
||||
},
|
||||
timeseries: {
|
||||
name: 'timeseries',
|
||||
label: 'Timeseries',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'timeseries',
|
||||
},
|
||||
trace: {
|
||||
name: 'trace',
|
||||
label: 'Trace',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'trace',
|
||||
},
|
||||
table: {
|
||||
name: 'table',
|
||||
label: 'Table',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'table',
|
||||
},
|
||||
clickhouse: {
|
||||
name: 'clickhouse',
|
||||
label: 'Clickhouse',
|
||||
disabled: false,
|
||||
show: false,
|
||||
key: 'clickhouse',
|
||||
},
|
||||
};
|
||||
@@ -18,12 +18,6 @@ jest.mock('pages/DashboardPageV2/DashboardContainer', () => ({
|
||||
default: (): JSX.Element => <div data-testid="llm-overview-dashboard" />,
|
||||
}));
|
||||
|
||||
// Same data-router gap as the dashboard above: the Explorer toolbar calls useNavigationType.
|
||||
jest.mock('container/LLMObservability/Explorer/Explorer', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="llm-observability-explorer" />,
|
||||
}));
|
||||
|
||||
function setupList(items = mockRules): void {
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) =>
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
padding: 0px 8px;
|
||||
|
||||
.logs-frequency-chart {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -49,7 +50,7 @@ function BodyTitleRenderer({
|
||||
const { featureFlags } = useAppContext();
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
const { viewName } = useGetSavedViewParams();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const cleanedNodeKey = removeObjectFromString(nodeKey);
|
||||
const isBodyJsonQueryEnabled =
|
||||
|
||||
@@ -7,12 +7,13 @@ import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
||||
import cx from 'classnames';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import {
|
||||
@@ -140,7 +141,7 @@ export default function TableViewActions(
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
const { viewName } = useGetSavedViewParams();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
|
||||
|
||||
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
|
||||
@@ -88,7 +88,7 @@ jest.mock('react-router-dom', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('hooks/saveViews/useGetSavedViewParams');
|
||||
jest.mock('hooks/queryBuilder/useGetSearchQueryParam');
|
||||
|
||||
describe('TableViewActions', () => {
|
||||
const TEST_VALUE = 'test value';
|
||||
@@ -140,10 +140,8 @@ describe('TableViewActions', () => {
|
||||
}),
|
||||
} as any);
|
||||
|
||||
// Default mock for useGetSavedViewParams
|
||||
jest
|
||||
.mocked(useGetSavedViewParams)
|
||||
.mockReturnValue({ viewName: '', viewKey: '' });
|
||||
// Default mock for useGetSearchQueryParam
|
||||
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('should render without crashing', () => {
|
||||
@@ -251,9 +249,7 @@ describe('TableViewActions', () => {
|
||||
updateQueriesData: mockUpdateQueriesData,
|
||||
} as any);
|
||||
|
||||
jest
|
||||
.mocked(useGetSavedViewParams)
|
||||
.mockReturnValue({ viewName: '', viewKey: '' });
|
||||
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
|
||||
|
||||
render(
|
||||
<TableViewActions
|
||||
|
||||
@@ -3,9 +3,10 @@ import { useLocation } from 'react-router-dom';
|
||||
import { CircleMinus, CirclePlus, Layers, RefreshCw } from '@signozhq/icons';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
@@ -57,7 +58,7 @@ export function useLogAttributeActions({
|
||||
const { pathname } = useLocation();
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
const { featureFlags } = useAppContext();
|
||||
const { viewName } = useGetSavedViewParams();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const isBodyJsonQueryEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
min-height: 200px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-card-body {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
padding: 0 16px 16px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
.logs-frequency-chart-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import getChartData, { GetChartDataProps } from 'lib/getChartData';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
||||
import { useLogsExplorerChartConfig } from './useLogsExplorerChartConfig';
|
||||
import { getColorsForSeverityLabels } from './utils';
|
||||
|
||||
import './LogsExplorerChart.styles.scss';
|
||||
|
||||
// Axis and tooltip format separately; both need this or only one abbreviates.
|
||||
const Y_AXIS_UNIT = 'short';
|
||||
|
||||
function LogsExplorerChart({
|
||||
data,
|
||||
isLoading,
|
||||
@@ -41,6 +37,24 @@ function LogsExplorerChart({
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] =
|
||||
useCallback(
|
||||
(element, index, allLabels) => ({
|
||||
data: element,
|
||||
backgroundColor: isLogsExplorerViews
|
||||
? getColorsForSeverityLabels(allLabels[index], index)
|
||||
: colors[index % colors.length] || themeColors.red,
|
||||
borderColor: isLogsExplorerViews
|
||||
? getColorsForSeverityLabels(allLabels[index], index)
|
||||
: colors[index % colors.length] || themeColors.red,
|
||||
...(isLabelEnabled
|
||||
? {
|
||||
label: allLabels[index],
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
[isLabelEnabled, isLogsExplorerViews],
|
||||
);
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
@@ -72,47 +86,44 @@ function LogsExplorerChart({
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery, isShowingLiveLogs],
|
||||
);
|
||||
|
||||
// uPlot plots the series on a seconds-based x scale
|
||||
const { minTimeScale, maxTimeScale } = useMemo(
|
||||
const graphData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: data,
|
||||
},
|
||||
],
|
||||
createDataset: handleCreateDatasets,
|
||||
}),
|
||||
[data, handleCreateDatasets],
|
||||
);
|
||||
|
||||
// Convert nanosecond timestamps to milliseconds for Chart.js
|
||||
const { chartMinTime, chartMaxTime } = useMemo(
|
||||
() => ({
|
||||
minTimeScale: minTime ? Math.floor(minTime / 1e9) : undefined,
|
||||
maxTimeScale: maxTime ? Math.floor(maxTime / 1e9) : undefined,
|
||||
chartMinTime: minTime ? Math.floor(minTime / 1e6) : undefined,
|
||||
chartMaxTime: maxTime ? Math.floor(maxTime / 1e6) : undefined,
|
||||
}),
|
||||
[minTime, maxTime],
|
||||
);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
|
||||
const { config, chartData } = useLogsExplorerChartConfig({
|
||||
data,
|
||||
isLogsExplorerViews,
|
||||
isLabelEnabled,
|
||||
onDragSelect,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
yAxisUnit: Y_AXIS_UNIT,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={graphRef} className={`${className} logs-frequency-chart-container`}>
|
||||
<div className={`${className} logs-frequency-chart-container`}>
|
||||
{isLoading ? (
|
||||
<div className="logs-frequency-chart-loading">
|
||||
<Spinner size="default" height="100%" />
|
||||
</div>
|
||||
) : (
|
||||
<BarChart
|
||||
config={config}
|
||||
data={chartData}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
stack={isLogsExplorerViews ? StackMode.Normal : StackMode.None}
|
||||
showLegend={isLabelEnabled}
|
||||
legendConfig={{ position: LegendPosition.BOTTOM }}
|
||||
timezone={timezone}
|
||||
data-testid="logs-frequency-chart"
|
||||
yAxisUnit={Y_AXIS_UNIT}
|
||||
<Graph
|
||||
name="logsExplorerChart"
|
||||
data={graphData.data}
|
||||
isStacked={isLogsExplorerViews}
|
||||
type="bar"
|
||||
animate
|
||||
onDragSelect={onDragSelect}
|
||||
minTime={chartMinTime}
|
||||
maxTime={chartMaxTime}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { getColorsForSeverityLabels } from './utils';
|
||||
|
||||
export interface UseLogsExplorerChartConfigParams {
|
||||
data: QueryData[];
|
||||
isLogsExplorerViews?: boolean;
|
||||
isLabelEnabled?: boolean;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
yAxisUnit?: string;
|
||||
}
|
||||
|
||||
export interface UseLogsExplorerChartConfigResult {
|
||||
config: UPlotConfigBuilder;
|
||||
chartData: uPlot.AlignedData;
|
||||
}
|
||||
|
||||
export function useLogsExplorerChartConfig({
|
||||
data,
|
||||
isLogsExplorerViews = false,
|
||||
isLabelEnabled = true,
|
||||
onDragSelect,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
yAxisUnit,
|
||||
}: UseLogsExplorerChartConfigParams): UseLogsExplorerChartConfigResult {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
// getUPlotChartData / buildBaseConfig both consume the legacy query-range payload
|
||||
// shape, so the raw series list is wrapped instead of being plotted directly.
|
||||
const apiResponse = useMemo(
|
||||
() =>
|
||||
({
|
||||
data: { result: data, resultType: '' },
|
||||
}) as unknown as MetricRangePayloadProps,
|
||||
[data],
|
||||
);
|
||||
|
||||
const chartData = useMemo(() => getUPlotChartData(apiResponse), [apiResponse]);
|
||||
|
||||
const config = useMemo(() => {
|
||||
const builder = buildBaseConfig({
|
||||
id: 'logs-explorer-frequency-chart',
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
yAxisUnit,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
});
|
||||
|
||||
data.forEach((series, index) => {
|
||||
const label = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '',
|
||||
series.legend || '',
|
||||
);
|
||||
|
||||
const color = isLogsExplorerViews
|
||||
? getColorsForSeverityLabels(label, index)
|
||||
: colors[index % colors.length] || themeColors.red;
|
||||
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
// No group-by yields query name "A"; use ' ' not '' so uPlot does not default the label to "Value".
|
||||
label: isLabelEnabled && label.trim() ? label : ' ',
|
||||
lineColor: color,
|
||||
colorMapping: {},
|
||||
isDarkMode,
|
||||
});
|
||||
});
|
||||
|
||||
return builder;
|
||||
}, [
|
||||
data,
|
||||
isDarkMode,
|
||||
isLabelEnabled,
|
||||
isLogsExplorerViews,
|
||||
maxTimeScale,
|
||||
minTimeScale,
|
||||
onDragSelect,
|
||||
timezone,
|
||||
yAxisUnit,
|
||||
]);
|
||||
|
||||
return { config, chartData };
|
||||
}
|
||||
@@ -217,6 +217,13 @@
|
||||
padding: 0px 8px;
|
||||
|
||||
.logs-frequency-chart {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,6 @@ export const useGetCompositeQueryParam = (): Query | null => {
|
||||
decodeURIComponent(compositeQuery.replace(/\+/g, ' ')),
|
||||
);
|
||||
|
||||
// Add default values for optional fields if empty
|
||||
if (parsedCompositeQuery?.builder) {
|
||||
parsedCompositeQuery.builder.queryFormulas =
|
||||
parsedCompositeQuery.builder.queryFormulas ?? [];
|
||||
parsedCompositeQuery.builder.queryTraceOperator =
|
||||
parsedCompositeQuery.builder.queryTraceOperator ?? [];
|
||||
}
|
||||
|
||||
// Convert old format to new format for each query in builder.queryData
|
||||
if (parsedCompositeQuery?.builder?.queryData) {
|
||||
parsedCompositeQuery.builder.queryData =
|
||||
|
||||
15
frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts
Normal file
15
frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
import { QuerySearchParamNames } from 'components/ExplorerCard/constants';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
export const useGetSearchQueryParam = (
|
||||
searchParams: QuerySearchParamNames,
|
||||
): string | null => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
return useMemo(() => {
|
||||
const searchQuery = urlQuery.get(searchParams);
|
||||
|
||||
return searchQuery ? JSON.parse(searchQuery) : null;
|
||||
}, [urlQuery, searchParams]);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import { useGetSavedViewParams } from '../useGetSavedViewParams';
|
||||
|
||||
jest.mock('hooks/useUrlQuery');
|
||||
|
||||
const mockedUseUrlQuery = useUrlQuery as jest.Mock;
|
||||
|
||||
const setSearch = (search: string): void => {
|
||||
mockedUseUrlQuery.mockReturnValue(new URLSearchParams(search));
|
||||
};
|
||||
|
||||
describe('useGetSavedViewParams', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns empty strings when no params are present', () => {
|
||||
setSearch('');
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({ viewName: '', viewKey: '' });
|
||||
});
|
||||
|
||||
it('parses JSON-stringified values', () => {
|
||||
setSearch(
|
||||
`viewName=${encodeURIComponent(
|
||||
JSON.stringify('Hindsight'),
|
||||
)}&viewKey=${encodeURIComponent(JSON.stringify('abc-123'))}`,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
viewName: 'Hindsight',
|
||||
viewKey: 'abc-123',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to the raw string when a value is not valid JSON', () => {
|
||||
setSearch('viewName=Hindsight&viewKey=some-uuid-value');
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
viewName: 'Hindsight',
|
||||
viewKey: 'some-uuid-value',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw and keeps the raw string for non-string JSON', () => {
|
||||
setSearch('viewName=123');
|
||||
|
||||
const { result } = renderHook(() => useGetSavedViewParams());
|
||||
|
||||
expect(result.current).toStrictEqual({ viewName: '123', viewKey: '' });
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
interface SavedViewParams {
|
||||
viewName: string;
|
||||
viewKey: string;
|
||||
}
|
||||
|
||||
const parseViewParam = (value: string | null): string => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return typeof parsed === 'string' ? parsed : value;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetSavedViewParams = (): SavedViewParams => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
viewName: parseViewParam(urlQuery.get(QueryParams.viewName)),
|
||||
viewKey: parseViewParam(urlQuery.get(QueryParams.viewKey)),
|
||||
}),
|
||||
[urlQuery],
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/const
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { useGetSavedViewParams } from './saveViews/useGetSavedViewParams';
|
||||
import { useGetSearchQueryParam } from './queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from './queryBuilder/useQueryBuilder';
|
||||
|
||||
export interface ICurrentQueryData {
|
||||
@@ -31,7 +31,9 @@ export const useHandleExplorerTabChange = (): {
|
||||
updateQueriesData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const { viewName, viewKey } = useGetSavedViewParams();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||
|
||||
const getUpdateQuery = useCallback(
|
||||
(newPanelType: PANEL_TYPES): Query => {
|
||||
|
||||
@@ -163,23 +163,20 @@ export function QueryBuilderProvider({
|
||||
const prepareQueryBuilderData = useCallback(
|
||||
(query: Query): Query => {
|
||||
const builder: QueryBuilderData = {
|
||||
queryData:
|
||||
query.builder.queryData?.map((item) => ({
|
||||
...initialQueryBuilderFormValuesMap[
|
||||
initialDataSource || DataSource.METRICS
|
||||
],
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryFormulas:
|
||||
query.builder.queryFormulas?.map((item) => ({
|
||||
...initialFormulaBuilderFormValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryTraceOperator:
|
||||
query.builder.queryTraceOperator?.map((item) => ({
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryData: query.builder.queryData?.map((item) => ({
|
||||
...initialQueryBuilderFormValuesMap[
|
||||
initialDataSource || DataSource.METRICS
|
||||
],
|
||||
...item,
|
||||
})),
|
||||
queryFormulas: query.builder.queryFormulas?.map((item) => ({
|
||||
...initialFormulaBuilderFormValues,
|
||||
...item,
|
||||
})),
|
||||
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
...item,
|
||||
})),
|
||||
};
|
||||
|
||||
const setupedQueryData = builder.queryData.map((item) => {
|
||||
@@ -212,17 +209,15 @@ export function QueryBuilderProvider({
|
||||
return currentElement;
|
||||
});
|
||||
|
||||
const promql: IPromQLQuery[] =
|
||||
query.promql?.map((item) => ({
|
||||
...initialQueryPromQLData,
|
||||
...item,
|
||||
})) ?? [];
|
||||
const promql: IPromQLQuery[] = query.promql.map((item) => ({
|
||||
...initialQueryPromQLData,
|
||||
...item,
|
||||
}));
|
||||
|
||||
const clickHouse: IClickHouseQuery[] =
|
||||
query.clickhouse_sql?.map((item) => ({
|
||||
...initialClickHouseData,
|
||||
...item,
|
||||
})) ?? [];
|
||||
const clickHouse: IClickHouseQuery[] = query.clickhouse_sql.map((item) => ({
|
||||
...initialClickHouseData,
|
||||
...item,
|
||||
}));
|
||||
|
||||
const newQueryState: QueryState = {
|
||||
clickhouse_sql: clickHouse,
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"factor-api-key",
|
||||
"license",
|
||||
"subscription",
|
||||
"deployment-host",
|
||||
"logs",
|
||||
"traces",
|
||||
"metrics",
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addFlaggerRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/features", handler.New(provider.authzMiddleware.OpenAccess(provider.flaggerHandler.GetFeatures), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/features", handler.New(provider.authzMiddleware.ViewAccess(provider.flaggerHandler.GetFeatures), handler.OpenAPIDef{
|
||||
ID: "GetFeatures",
|
||||
Tags: []string{"features"},
|
||||
Summary: "Get features",
|
||||
@@ -21,7 +22,7 @@ func (provider *provider) addFlaggerRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes(nil),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
84
pkg/apiserver/signozapiserver/licensing.go
Normal file
84
pkg/apiserver/signozapiserver/licensing.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addLicensingRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v3/licenses", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.licensingHandler.Activate, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ActivateLicense",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "Activate a license.",
|
||||
Description: "This endpoint validates the license key with upstream and activates the license for the organization.",
|
||||
Request: new(licensetypes.PostableLicense),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusAccepted,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbCreate)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceLicense,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v3/licenses", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.licensingHandler.Refresh, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "RefreshLicense",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "Refresh the active license.",
|
||||
Description: "This endpoint refreshes the active license of the organization from upstream.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbUpdate)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceLicense,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v3/licenses/active", handler.New(provider.authzMiddleware.OpenAccess(provider.licensingHandler.GetActive), handler.OpenAPIDef{
|
||||
ID: "GetActiveLicense",
|
||||
Tags: []string{"licenses"},
|
||||
Summary: "Get the active license.",
|
||||
Description: "This endpoint gets the active license of the organization.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(licensetypes.GettableLicense),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes(nil),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/aiobservability"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
@@ -67,6 +68,7 @@ type provider struct {
|
||||
authzHandler authz.Handler
|
||||
rawDataExportHandler rawdataexport.Handler
|
||||
zeusHandler zeus.Handler
|
||||
licensingHandler licensing.Handler
|
||||
querierHandler querier.Handler
|
||||
serviceAccountHandler serviceaccount.Handler
|
||||
serviceAccountGetter serviceaccount.Getter
|
||||
@@ -105,6 +107,7 @@ func NewFactory(
|
||||
authzHandler authz.Handler,
|
||||
rawDataExportHandler rawdataexport.Handler,
|
||||
zeusHandler zeus.Handler,
|
||||
licensingHandler licensing.Handler,
|
||||
querierHandler querier.Handler,
|
||||
serviceAccountHandler serviceaccount.Handler,
|
||||
serviceAccountGetter serviceaccount.Getter,
|
||||
@@ -146,6 +149,7 @@ func NewFactory(
|
||||
authzHandler,
|
||||
rawDataExportHandler,
|
||||
zeusHandler,
|
||||
licensingHandler,
|
||||
querierHandler,
|
||||
serviceAccountHandler,
|
||||
serviceAccountGetter,
|
||||
@@ -189,6 +193,7 @@ func newProvider(
|
||||
authzHandler authz.Handler,
|
||||
rawDataExportHandler rawdataexport.Handler,
|
||||
zeusHandler zeus.Handler,
|
||||
licensingHandler licensing.Handler,
|
||||
querierHandler querier.Handler,
|
||||
serviceAccountHandler serviceaccount.Handler,
|
||||
serviceAccountGetter serviceaccount.Getter,
|
||||
@@ -231,6 +236,7 @@ func newProvider(
|
||||
authzHandler: authzHandler,
|
||||
rawDataExportHandler: rawDataExportHandler,
|
||||
zeusHandler: zeusHandler,
|
||||
licensingHandler: licensingHandler,
|
||||
querierHandler: querierHandler,
|
||||
serviceAccountHandler: serviceAccountHandler,
|
||||
serviceAccountGetter: serviceAccountGetter,
|
||||
@@ -332,6 +338,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addLicensingRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addZeusRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -29,7 +27,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.CheckResources(provider.zeusHandler.GetHosts, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.ViewAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
|
||||
ID: "GetHosts",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Get host info from Zeus.",
|
||||
@@ -41,17 +39,12 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDeploymentHost.Scope(coretypes.VerbList)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceDeploymentHost,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.CheckResources(provider.zeusHandler.PutHost, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{
|
||||
ID: "PutHost",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Put host in Zeus for a deployment.",
|
||||
@@ -63,14 +56,8 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDeploymentHost.Scope(coretypes.VerbUpdate)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceDeploymentHost,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
ID: coretypes.BodyJSONPath("name"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodPut).GetError(); err != nil {
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
Name: FeatureResolveSemconvFamilies,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether trace, log, and metric queries resolve a semantic-convention name to all the spellings of its family",
|
||||
Description: "Controls whether trace queries resolve a semantic-convention name to all the spellings of its family",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package httplicensing
|
||||
package licensing
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,21 +8,20 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type licensingAPI struct {
|
||||
licensing licensing.Licensing
|
||||
type handler struct {
|
||||
licensing Licensing
|
||||
}
|
||||
|
||||
func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
|
||||
return &licensingAPI{licensing: licensing}
|
||||
func NewHandler(licensing Licensing) Handler {
|
||||
return &handler{licensing: licensing}
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
|
||||
func (handler *handler) Activate(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -45,7 +44,7 @@ func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = api.licensing.Activate(r.Context(), orgID, req.Key)
|
||||
err = handler.licensing.Activate(r.Context(), orgID, req.Key)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -54,7 +53,7 @@ func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusAccepted, nil)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||
func (handler *handler) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -70,7 +69,7 @@ func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
license, err := api.licensing.GetActive(r.Context(), orgID)
|
||||
license, err := handler.licensing.GetActive(r.Context(), orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -80,7 +79,7 @@ func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusOK, gettableLicense)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||
func (handler *handler) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -96,7 +95,7 @@ func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = api.licensing.Refresh(r.Context(), orgID)
|
||||
err = handler.licensing.Refresh(r.Context(), orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -105,7 +104,7 @@ func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||
func (handler *handler) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -127,7 +126,7 @@ func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req)
|
||||
gettableSubscription, err := handler.licensing.Checkout(ctx, orgID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -136,7 +135,7 @@ func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusCreated, gettableSubscription)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
|
||||
func (handler *handler) Portal(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -158,7 +157,7 @@ func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
gettableSubscription, err := api.licensing.Portal(ctx, orgID, req)
|
||||
gettableSubscription, err := handler.licensing.Portal(ctx, orgID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -37,7 +37,7 @@ type Licensing interface {
|
||||
statsreporter.StatsCollector
|
||||
}
|
||||
|
||||
type API interface {
|
||||
type Handler interface {
|
||||
Activate(http.ResponseWriter, *http.Request)
|
||||
Refresh(http.ResponseWriter, *http.Request)
|
||||
GetActive(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package nooplicensing
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
)
|
||||
|
||||
type noopLicensingAPI struct{}
|
||||
|
||||
func NewLicenseAPI() licensing.API {
|
||||
return &noopLicensingAPI{}
|
||||
}
|
||||
|
||||
func (api *noopLicensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
|
||||
}
|
||||
|
||||
func (api *noopLicensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
|
||||
}
|
||||
|
||||
func (api *noopLicensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
|
||||
}
|
||||
|
||||
func (api *noopLicensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
|
||||
}
|
||||
|
||||
func (api *noopLicensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented"))
|
||||
}
|
||||
@@ -440,7 +440,6 @@ func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filte
|
||||
whereClauseSelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
|
||||
}
|
||||
|
||||
whereClauseSelectors = querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, m.fl, whereClauseSelectors)
|
||||
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, orgID, whereClauseSelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -448,9 +447,6 @@ func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filte
|
||||
|
||||
opts := querybuilder.FilterExprVisitorOpts{
|
||||
Context: ctx,
|
||||
OrgID: orgID,
|
||||
Flagger: m.fl,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Logger: m.logger,
|
||||
FieldMapper: m.fieldMapper,
|
||||
ConditionBuilder: m.condBuilder,
|
||||
|
||||
@@ -969,7 +969,6 @@ func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filte
|
||||
// whereClauseSelectors[idx].Source = query.Source
|
||||
}
|
||||
|
||||
whereClauseSelectors = querybuilder.ExpandKeySelectorsForFamilies(ctx, orgID, m.fl, whereClauseSelectors)
|
||||
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, orgID, whereClauseSelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -977,9 +976,6 @@ func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filte
|
||||
|
||||
opts := querybuilder.FilterExprVisitorOpts{
|
||||
Context: ctx,
|
||||
OrgID: orgID,
|
||||
Flagger: m.fl,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Logger: m.logger,
|
||||
FieldMapper: m.fieldMapper,
|
||||
ConditionBuilder: m.condBuilder,
|
||||
|
||||
@@ -22,46 +22,45 @@ func newConditionBuilder(fm qbtypes.FieldMapper) qbtypes.ConditionBuilder {
|
||||
}
|
||||
|
||||
// Rule state history has no resource sub-query, so options are unused.
|
||||
// ConditionFor rejects the logs-only function operators and hands the term to
|
||||
// the generic flow; rule state history fields have no family support.
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
logicalFields []*telemetrytypes.LogicalField,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
_ qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for rule state history.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
scope := querybuilder.CompileScope{OrgID: orgID, StartNs: startNs, EndNs: endNs}
|
||||
return querybuilder.CompileTerm(ctx, scope, c, querybuilder.SkipResourceNone, key, logicalFields, fieldKeys, options, operator, value, sb)
|
||||
}
|
||||
|
||||
// AmendEvidence: rule state history folds no intrinsic storage into the evidence.
|
||||
func (c *conditionBuilder) AmendEvidence(_ context.Context, _ querybuilder.CompileScope, _ *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField {
|
||||
return fields
|
||||
}
|
||||
|
||||
// Synthesize: rule state history synthesizes nothing — an unknown key is an error.
|
||||
func (c *conditionBuilder) Synthesize(_ context.Context, _ querybuilder.CompileScope, key *telemetrytypes.TelemetryFieldKey, _ qbtypes.FilterOperator, _ any, _ map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error) {
|
||||
return nil, nil, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
// CompileField: single keys only — a family is a wiring error for this signal.
|
||||
func (c *conditionBuilder) CompileField(ctx context.Context, scope querybuilder.CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error) {
|
||||
if logical.IsFamily() {
|
||||
return "", nil, errors.NewInternalf(errors.CodeInternal, "field %q resolved to a family, and this signal compiles single keys only", logical.Name)
|
||||
// Rule state history fields have no family support, so every logical field
|
||||
// is single-member and flattens losslessly to its physical key.
|
||||
resolved, warning := querybuilder.ResolveLogicalFields(key, querybuilder.MatchingLogicalFields(ctx, orgID, nil, key, fieldKeys))
|
||||
keys := querybuilder.SingleKeys(resolved)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
cond, err := c.conditionForKey(ctx, scope.OrgID, scope.StartNs, scope.EndNs, logical.Single(), operator, value, sb)
|
||||
return cond, nil, err
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package implrulestatehistory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The pins below fix the exact operator forms of this builder, including the
|
||||
// two shapes that exist nowhere else: IN binds the whole list to one
|
||||
// placeholder (sb.In without spreading), and the exists predicate renders
|
||||
// "true" for an intrinsic column but a JSONHas membership check for a label.
|
||||
func TestConditionForPinsOperatorForms(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fm := newFieldMapper()
|
||||
cb := newConditionBuilder(fm)
|
||||
|
||||
intrinsic := &telemetrytypes.TelemetryFieldKey{Name: "state"}
|
||||
label := &telemetrytypes.TelemetryFieldKey{Name: "deployment"}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
key *telemetrytypes.TelemetryFieldKey
|
||||
operator qbtypes.FilterOperator
|
||||
value any
|
||||
expectedSQL string
|
||||
expectedArgs []any
|
||||
}{
|
||||
{name: "intrinsic equal", key: intrinsic, operator: qbtypes.FilterOperatorEqual, value: "firing", expectedSQL: "WHERE state = ?", expectedArgs: []any{"firing"}},
|
||||
{name: "intrinsic not equal", key: intrinsic, operator: qbtypes.FilterOperatorNotEqual, value: "firing", expectedSQL: "WHERE state <> ?", expectedArgs: []any{"firing"}},
|
||||
{name: "label equal", key: label, operator: qbtypes.FilterOperatorEqual, value: "production", expectedSQL: "WHERE JSONExtractString(labels, 'deployment') = ?", expectedArgs: []any{"production"}},
|
||||
{name: "greater than", key: &telemetrytypes.TelemetryFieldKey{Name: "unix_milli"}, operator: qbtypes.FilterOperatorGreaterThan, value: int64(123), expectedSQL: "WHERE unix_milli > ?", expectedArgs: []any{int64(123)}},
|
||||
{name: "like", key: intrinsic, operator: qbtypes.FilterOperatorLike, value: "fir", expectedSQL: "WHERE state LIKE ?", expectedArgs: []any{"fir"}},
|
||||
{name: "contains", key: intrinsic, operator: qbtypes.FilterOperatorContains, value: "fir", expectedSQL: "WHERE LOWER(state) LIKE LOWER(?)", expectedArgs: []any{"%fir%"}},
|
||||
{name: "regexp", key: intrinsic, operator: qbtypes.FilterOperatorRegexp, value: "^f", expectedSQL: "WHERE match(state, ?)", expectedArgs: []any{"^f"}},
|
||||
{name: "between", key: &telemetrytypes.TelemetryFieldKey{Name: "unix_milli"}, operator: qbtypes.FilterOperatorBetween, value: []any{int64(1), int64(2)}, expectedSQL: "WHERE unix_milli BETWEEN ? AND ?", expectedArgs: []any{int64(1), int64(2)}},
|
||||
{name: "in binds the list to one placeholder", key: intrinsic, operator: qbtypes.FilterOperatorIn, value: []any{"firing", "inactive"}, expectedSQL: "WHERE state IN (?)", expectedArgs: []any{[]any{"firing", "inactive"}}},
|
||||
{name: "not in binds the list to one placeholder", key: intrinsic, operator: qbtypes.FilterOperatorNotIn, value: []any{"firing"}, expectedSQL: "WHERE state NOT IN (?)", expectedArgs: []any{[]any{"firing"}}},
|
||||
{name: "intrinsic exists is constant true", key: intrinsic, operator: qbtypes.FilterOperatorExists, value: nil, expectedSQL: "WHERE true", expectedArgs: nil},
|
||||
{name: "label exists is a membership check", key: label, operator: qbtypes.FilterOperatorExists, value: nil, expectedSQL: "WHERE JSONHas(labels, ?)", expectedArgs: []any{"deployment"}},
|
||||
{name: "label not exists negates the membership check", key: label, operator: qbtypes.FilterOperatorNotExists, value: nil, expectedSQL: "WHERE not JSONHas(labels, ?)", expectedArgs: []any{"deployment"}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{tc.key.Name: {tc.key}}
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
conds, _, err := cb.ConditionFor(ctx, valuer.UUID{}, 0, 0, tc.key,
|
||||
querybuilder.MatchingLogicalFields(ctx, valuer.UUID{}, nil, telemetrytypes.SignalUnspecified, nil, tc.key, fieldKeys),
|
||||
fieldKeys, qbtypes.ConditionBuilderOptions{}, tc.operator, tc.value, sb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, conds, 1)
|
||||
sb.Where(conds...)
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
assert.Equal(t, tc.expectedSQL, sql)
|
||||
assert.Equal(t, tc.expectedArgs, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An unknown key is an error, and a function operator is rejected before
|
||||
// resolution.
|
||||
func TestConditionForRejections(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cb := newConditionBuilder(newFieldMapper())
|
||||
key := &telemetrytypes.TelemetryFieldKey{Name: "missing"}
|
||||
|
||||
_, _, err := cb.ConditionFor(ctx, valuer.UUID{}, 0, 0, key, nil, nil, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorEqual, "x", sqlbuilder.NewSelectBuilder())
|
||||
assert.ErrorContains(t, err, "not found")
|
||||
|
||||
_, _, err = cb.ConditionFor(ctx, valuer.UUID{}, 0, 0, key, nil, nil, qbtypes.ConditionBuilderOptions{}, qbtypes.FilterOperatorHasToken, "x", sqlbuilder.NewSelectBuilder())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func (m *fieldMapper) ExistsFor(ctx context.Context, _ valuer.UUID, _, _ uint64,
|
||||
return "true", nil
|
||||
}
|
||||
|
||||
func (m *fieldMapper) ColumnExpressionFor(ctx context.Context, orgID valuer.UUID, tsStart, tsEnd uint64, field *telemetrytypes.TelemetryFieldKey, _ []*telemetrytypes.LogicalField, _ telemetrytypes.FieldDataType, _ map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
func (m *fieldMapper) ColumnExpressionFor(ctx context.Context, orgID valuer.UUID, tsStart, tsEnd uint64, field *telemetrytypes.TelemetryFieldKey, _ telemetrytypes.FieldDataType, _ map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
colName, err := m.FieldFor(ctx, orgID, tsStart, tsEnd, field)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -2,7 +2,6 @@ package prometheus
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
)
|
||||
@@ -24,11 +23,5 @@ func NewEngine(logger *slog.Logger, cfg Config) *Engine {
|
||||
Timeout: cfg.Timeout,
|
||||
ActiveQueryTracker: activeQueryTracker,
|
||||
LookbackDelta: cfg.LookbackDelta,
|
||||
// The engine calls this for subqueries that do not set a step, such as
|
||||
// `metric[5m:]`, and segfaults if it is nil. 1m matches the default
|
||||
// global evaluation_interval that Prometheus wires here.
|
||||
NoStepSubqueryIntervalFn: func(int64) int64 {
|
||||
return time.Minute.Milliseconds()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNoStepSubqueryDoesNotPanic(t *testing.T) {
|
||||
engine := NewEngine(slog.New(slog.DiscardHandler), Config{Timeout: time.Minute})
|
||||
queryable := storage.QueryableFunc(func(int64, int64) (storage.Querier, error) {
|
||||
return storage.NoopQuerier(), nil
|
||||
})
|
||||
|
||||
qry, err := engine.NewRangeQuery(
|
||||
context.Background(),
|
||||
queryable,
|
||||
nil,
|
||||
"max_over_time(some_metric[5m:])",
|
||||
time.Now().Add(-time.Hour),
|
||||
time.Now(),
|
||||
time.Minute,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer qry.Close()
|
||||
|
||||
res := qry.Exec(context.Background())
|
||||
require.NoError(t, res.Err)
|
||||
}
|
||||
@@ -82,12 +82,6 @@ func (q *builderQuery[T]) Fingerprint() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// AI trace aggregations qualify and rank traces on whole-window per-trace
|
||||
// values, which do not decompose into cacheable time buckets.
|
||||
if q.queryType == qbtypes.QueryTypeBuilderAI {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Create a deterministic fingerprint for builder queries
|
||||
// This needs to include all fields that affect the query results
|
||||
parts := []string{q.queryType.StringValue()}
|
||||
|
||||
@@ -117,7 +117,8 @@ func TestBuilderQueryFingerprintQueryType(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.True(t, strings.HasPrefix(regular.Fingerprint(), qbtypes.QueryTypeBuilder.StringValue()+"&"))
|
||||
assert.Empty(t, ai.Fingerprint())
|
||||
assert.True(t, strings.HasPrefix(ai.Fingerprint(), qbtypes.QueryTypeBuilderAI.StringValue()+"&"))
|
||||
assert.NotEqual(t, regular.Fingerprint(), ai.Fingerprint())
|
||||
}
|
||||
|
||||
func TestMakeBucketsOrder(t *testing.T) {
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
@@ -415,16 +415,6 @@ func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.Quer
|
||||
// resolved: never-seen metrics and dormant metrics (seen but no data in
|
||||
// the query window).
|
||||
// - err: Internal when a metadata fetch fails.
|
||||
//
|
||||
// familyMetricNames returns the storage names of the metric-name family when
|
||||
// the resolve_semconv_families flag is on for the org, else just the name.
|
||||
// Metric metadata must resolve through every spelling the statement builder
|
||||
// unions, or a query on the old name dies as a missing metric before the
|
||||
// union runs.
|
||||
func (q *querier) familyMetricNames(ctx context.Context, orgID valuer.UUID, metricName string) []string {
|
||||
return querybuilder.FamilyMetricNames(ctx, orgID, q.fl, metricName)
|
||||
}
|
||||
|
||||
func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, metricWarnings []string, err error) {
|
||||
metricNames := make([]string, 0)
|
||||
for idx := range queries {
|
||||
@@ -437,7 +427,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
for _, agg := range spec.Aggregations {
|
||||
if agg.MetricName != "" {
|
||||
metricNames = append(metricNames, q.familyMetricNames(ctx, orgID, agg.MetricName)...)
|
||||
metricNames = append(metricNames, agg.MetricName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,19 +456,13 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
presentAggregations := make([]qbtypes.MetricAggregation, 0, len(spec.Aggregations))
|
||||
for i := range spec.Aggregations {
|
||||
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Temporality == metrictypes.Unknown {
|
||||
for _, member := range q.familyMetricNames(ctx, orgID, spec.Aggregations[i].MetricName) {
|
||||
if temp, ok := metricTemporality[member]; ok && temp != metrictypes.Unknown {
|
||||
spec.Aggregations[i].Temporality = temp
|
||||
break
|
||||
}
|
||||
if temp, ok := metricTemporality[spec.Aggregations[i].MetricName]; ok && temp != metrictypes.Unknown {
|
||||
spec.Aggregations[i].Temporality = temp
|
||||
}
|
||||
}
|
||||
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
|
||||
for _, member := range q.familyMetricNames(ctx, orgID, spec.Aggregations[i].MetricName) {
|
||||
if foundMetricType, ok := metricTypes[member]; ok && foundMetricType != metrictypes.UnspecifiedType {
|
||||
spec.Aggregations[i].Type = foundMetricType
|
||||
break
|
||||
}
|
||||
if foundMetricType, ok := metricTypes[spec.Aggregations[i].MetricName]; ok && foundMetricType != metrictypes.UnspecifiedType {
|
||||
spec.Aggregations[i].Type = foundMetricType
|
||||
}
|
||||
}
|
||||
if spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
|
||||
@@ -486,7 +470,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
continue
|
||||
}
|
||||
// Type is resolved now; validate aggregation compatibility against it.
|
||||
if err := spec.Aggregations[i].ValidateForTypeAndTemporality(); err != nil {
|
||||
if err := spec.Aggregations[i].ValidateForType(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if reducedMetricsSet[spec.Aggregations[i].MetricName] {
|
||||
|
||||
@@ -52,10 +52,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
|
||||
chErrors "github.com/SigNoz/signoz/pkg/query-service/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/metrics"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/semconv"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -3202,14 +3202,7 @@ func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, orgID v
|
||||
query = query + fmt.Sprintf(" LIMIT %d;", req.Limit)
|
||||
}
|
||||
names := []string{req.AggregateAttribute}
|
||||
current := semconv.Current(semconv.KindMetric, telemetrytypes.FieldKeySelector{
|
||||
Name: req.AggregateAttribute,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
FieldContext: telemetrytypes.FieldContextMetric,
|
||||
})
|
||||
if current != req.AggregateAttribute {
|
||||
names = append(names, current)
|
||||
}
|
||||
names = append(names, metrics.GetTransitionedMetric(req.AggregateAttribute))
|
||||
|
||||
rows, err = r.db.Query(ctx, query, req.FilterAttributeKey, names, req.FilterAttributeKey, fmt.Sprintf("%%%s%%", req.SearchText), common.PastDayRoundOff())
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ type APIHandler struct {
|
||||
// Websocket connection upgrader
|
||||
Upgrader *websocket.Upgrader
|
||||
|
||||
LicensingAPI licensing.API
|
||||
LicensingHandler licensing.Handler
|
||||
|
||||
QueryParserAPI *queryparser.API
|
||||
|
||||
@@ -139,7 +139,7 @@ type APIHandlerOpts struct {
|
||||
// Flux Interval
|
||||
FluxInterval time.Duration
|
||||
|
||||
LicensingAPI licensing.API
|
||||
LicensingHandler licensing.Handler
|
||||
|
||||
QueryParserAPI *queryparser.API
|
||||
|
||||
@@ -176,7 +176,7 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
querier: querier,
|
||||
querierV2: querierv2,
|
||||
LicensingAPI: opts.LicensingAPI,
|
||||
LicensingHandler: opts.LicensingHandler,
|
||||
Signoz: opts.Signoz,
|
||||
QueryParserAPI: opts.QueryParserAPI,
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v2/traces/fields", am.EditAccess(aH.updateTraceField)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/features", am.OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/features", am.ViewAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/health", am.OpenAccess(aH.getHealth)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/listErrors", am.ViewAccess(aH.listErrors)).Methods(http.MethodPost)
|
||||
@@ -457,13 +457,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
router.HandleFunc("/api/v1/register", am.OpenAccess(aH.registerUser)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v3/licenses", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
|
||||
render.Success(rw, http.StatusOK, []any{})
|
||||
})).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
|
||||
aH.LicensingAPI.Activate(rw, req)
|
||||
})).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/span_percentile", am.ViewAccess(aH.Signoz.Handlers.SpanPercentile.GetSpanPercentileDetails)).Methods(http.MethodPost)
|
||||
|
||||
// Query Filter Analyzer api used to extract metric names and grouping columns from a query
|
||||
@@ -1497,7 +1490,7 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
aH.HandleError(w, err, http.StatusUnauthorized)
|
||||
aH.HandleError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/cache/cachetest"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/soheilhy/cmux"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
@@ -84,7 +84,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
IntegrationsController: integrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
LicensingAPI: nooplicensing.NewLicenseAPI(),
|
||||
LicensingHandler: licensing.NewHandler(signoz.Licensing),
|
||||
Signoz: signoz,
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
}, config)
|
||||
|
||||
14
pkg/query-service/metrics/transition.go
Normal file
14
pkg/query-service/metrics/transition.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package metrics
|
||||
|
||||
var MetricsUnderTransition = map[string]string{
|
||||
"k8s.pod.cpu.utilization": "k8s.pod.cpu.usage",
|
||||
"k8s.node.cpu.utilization": "k8s.node.cpu.usage",
|
||||
"container.cpu.utilization": "container.cpu.usage",
|
||||
}
|
||||
|
||||
func GetTransitionedMetric(metric string) string {
|
||||
if transitionedMetric, ok := MetricsUnderTransition[metric]; ok {
|
||||
return transitionedMetric
|
||||
}
|
||||
return metric
|
||||
}
|
||||
@@ -10,9 +10,8 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/metrics"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/semconv"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// ValidateAndCastValue validates and casts the value of a key to the corresponding data type of the key
|
||||
@@ -235,12 +234,12 @@ func ClickHouseFormattedValue(v interface{}) string {
|
||||
|
||||
func ClickHouseFormattedMetricNames(v interface{}) string {
|
||||
if name, ok := v.(string); ok {
|
||||
current := semconv.Current(semconv.KindMetric, telemetrytypes.FieldKeySelector{
|
||||
Name: name,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
FieldContext: telemetrytypes.FieldContextMetric,
|
||||
})
|
||||
return ClickHouseFormattedValue([]interface{}{current})
|
||||
transitionedMetrics := metrics.GetTransitionedMetric(name)
|
||||
if transitionedMetrics != name {
|
||||
return ClickHouseFormattedValue([]interface{}{transitionedMetrics})
|
||||
} else {
|
||||
return ClickHouseFormattedValue([]interface{}{name})
|
||||
}
|
||||
}
|
||||
|
||||
return ClickHouseFormattedValue(v)
|
||||
|
||||
@@ -483,22 +483,3 @@ func TestGetEpochNanoSecs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Only the canonical dotted spelling of a metric-name family redirects on the
|
||||
// legacy path; a normalized spelling keeps reading its own series.
|
||||
func TestClickHouseFormattedMetricNames(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{name: "k8s.pod.cpu.utilization", expected: "['k8s.pod.cpu.usage']"},
|
||||
{name: "k8s.pod.cpu.usage", expected: "['k8s.pod.cpu.usage']"},
|
||||
{name: "k8s_pod_cpu_utilization", expected: "['k8s_pod_cpu_utilization']"},
|
||||
{name: "http.server.duration", expected: "['http.server.duration']"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ClickHouseFormattedMetricNames(c.name); got != c.expected {
|
||||
t.Errorf("ClickHouseFormattedMetricNames(%q) = %q, want %q", c.name, got, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ type aggExprRewriter struct {
|
||||
fieldMapper qbtypes.FieldMapper
|
||||
conditionBuilder qbtypes.ConditionBuilder
|
||||
flagger flagger.Flagger
|
||||
signal telemetrytypes.Signal
|
||||
}
|
||||
|
||||
var _ qbtypes.AggExprRewriter = (*aggExprRewriter)(nil)
|
||||
@@ -33,7 +32,6 @@ func NewAggExprRewriter(
|
||||
fieldMapper qbtypes.FieldMapper,
|
||||
conditionBuilder qbtypes.ConditionBuilder,
|
||||
fl flagger.Flagger,
|
||||
signal telemetrytypes.Signal,
|
||||
) *aggExprRewriter {
|
||||
set := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querybuilder/agg_rewrite")
|
||||
|
||||
@@ -43,7 +41,6 @@ func NewAggExprRewriter(
|
||||
fieldMapper: fieldMapper,
|
||||
conditionBuilder: conditionBuilder,
|
||||
flagger: fl,
|
||||
signal: signal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +89,6 @@ func (r *aggExprRewriter) Rewrite(
|
||||
r.fieldMapper,
|
||||
r.conditionBuilder,
|
||||
r.flagger,
|
||||
r.signal,
|
||||
)
|
||||
// Rewrite the first select item (our expression)
|
||||
if err := sel.SelectItems[0].Accept(visitor); err != nil {
|
||||
@@ -147,7 +143,6 @@ type exprVisitor struct {
|
||||
fieldMapper qbtypes.FieldMapper
|
||||
conditionBuilder qbtypes.ConditionBuilder
|
||||
flagger flagger.Flagger
|
||||
signal telemetrytypes.Signal
|
||||
Modified bool
|
||||
chArgs []any
|
||||
isRate bool
|
||||
@@ -164,7 +159,6 @@ func newExprVisitor(
|
||||
fieldMapper qbtypes.FieldMapper,
|
||||
conditionBuilder qbtypes.ConditionBuilder,
|
||||
fl flagger.Flagger,
|
||||
signal telemetrytypes.Signal,
|
||||
) *exprVisitor {
|
||||
return &exprVisitor{
|
||||
ctx: ctx,
|
||||
@@ -177,7 +171,6 @@ func newExprVisitor(
|
||||
fieldMapper: fieldMapper,
|
||||
conditionBuilder: conditionBuilder,
|
||||
flagger: fl,
|
||||
signal: signal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +213,6 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
FilterExprVisitorOpts{
|
||||
Context: v.ctx,
|
||||
OrgID: v.orgID,
|
||||
Flagger: v.flagger,
|
||||
Signal: v.signal,
|
||||
Logger: v.logger,
|
||||
FieldKeys: v.fieldKeys,
|
||||
FieldMapper: v.fieldMapper,
|
||||
@@ -253,7 +244,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
for i := 0; i < len(args)-1; i++ {
|
||||
origVal := chparser.Format(args[i])
|
||||
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(origVal)
|
||||
expr, err := v.fieldMapper.ColumnExpressionFor(v.ctx, v.orgID, v.startNs, v.endNs, &fieldKey, MatchingLogicalFields(v.ctx, v.orgID, v.flagger, v.signal, nil, &fieldKey, v.fieldKeys), dataType, v.fieldKeys)
|
||||
expr, err := v.fieldMapper.ColumnExpressionFor(v.ctx, v.orgID, v.startNs, v.endNs, &fieldKey, dataType, v.fieldKeys)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to get table field name for %q", origVal)
|
||||
}
|
||||
@@ -270,7 +261,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
for i, arg := range args {
|
||||
orig := chparser.Format(arg)
|
||||
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(orig)
|
||||
expr, err := v.fieldMapper.ColumnExpressionFor(v.ctx, v.orgID, v.startNs, v.endNs, &fieldKey, MatchingLogicalFields(v.ctx, v.orgID, v.flagger, v.signal, nil, &fieldKey, v.fieldKeys), dataType, v.fieldKeys)
|
||||
expr, err := v.fieldMapper.ColumnExpressionFor(v.ctx, v.orgID, v.startNs, v.endNs, &fieldKey, dataType, v.fieldKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// ColumnSchema is the per-signal surface of the generic coerced-column
|
||||
// renderer (group-by, order, and aggregation arguments). The three methods
|
||||
// are leaf questions about one resolved field; the composition — the guarded,
|
||||
// coerced multiIf with the NULL group — is RenderCoercedColumn and is written
|
||||
// once. Raw select keeps its per-signal shapes in the mappers: its tails
|
||||
// differ by storage in more ways than they share.
|
||||
type ColumnSchema interface {
|
||||
// RawRead returns the uncoerced value read of one field. The canonical
|
||||
// answer is LogicalValueExpr (the merged read for a family, the member's
|
||||
// own read otherwise); logs overrides it for the legacy body path.
|
||||
RawRead(ctx context.Context, scope CompileScope, logical *telemetrytypes.LogicalField, dummyValue any) (string, error)
|
||||
|
||||
// Uncoerced reports a field whose native type must survive the stage
|
||||
// coercion: a time column would collapse to seconds (traces), and a
|
||||
// legacy body read carries its own typing.
|
||||
Uncoerced(ctx context.Context, scope CompileScope, logical *telemetrytypes.LogicalField) (bool, error)
|
||||
|
||||
// BareCandidate reports a field that cannot sit inside Nullable/multiIf
|
||||
// and renders as its bare read when it is the only candidate (arrays).
|
||||
BareCandidate(logical *telemetrytypes.LogicalField) bool
|
||||
}
|
||||
|
||||
// RenderCoercedColumn renders resolved fields as one column expression for
|
||||
// the coerced stages: every field exists-guarded and coerced to the target
|
||||
// type in a single multiIf, so rows holding none of the candidates keep the
|
||||
// NULL group of a single key.
|
||||
func RenderCoercedColumn(
|
||||
ctx context.Context,
|
||||
scope CompileScope,
|
||||
schema ColumnSchema,
|
||||
fm qbtypes.FieldMapper,
|
||||
fields []*telemetrytypes.LogicalField,
|
||||
target telemetrytypes.FieldDataType,
|
||||
) (string, error) {
|
||||
if len(fields) == 1 && schema.BareCandidate(fields[0]) {
|
||||
return schema.RawRead(ctx, scope, fields[0], "")
|
||||
}
|
||||
|
||||
var dummyValue any = ""
|
||||
if target == telemetrytypes.FieldDataTypeFloat64 {
|
||||
dummyValue = 0.0
|
||||
}
|
||||
stmts := make([]string, 0, len(fields)*2)
|
||||
for _, logical := range fields {
|
||||
guard, err := LogicalExistsExpr(ctx, scope.OrgID, scope.StartNs, scope.EndNs, fm, logical, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
read, err := schema.RawRead(ctx, scope, logical, dummyValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
uncoerced, err := schema.Uncoerced(ctx, scope, logical)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !uncoerced {
|
||||
read, _ = DataTypeCollisionHandledFieldName(logical.Single(), dummyValue, read, qbtypes.FilterOperatorUnknown)
|
||||
}
|
||||
stmts = append(stmts, guard, read)
|
||||
}
|
||||
return fmt.Sprintf("multiIf(%s, NULL)", strings.Join(stmts, ", ")), nil
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
// LogicalFamilyCondition compiles one condition for a family logical field
|
||||
// from the mapper's primitives. Family members are map-backed attribute or
|
||||
// resource keys by construction, so the compiler has no column-specific
|
||||
// branches; a signal keeps its own single-member paths and hands only
|
||||
// families here.
|
||||
func LogicalFamilyCondition(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
startNs, endNs uint64,
|
||||
fm qbtypes.FieldMapper,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
if operator.IsStringSearchOperator() {
|
||||
value = FormatValueForContains(value)
|
||||
}
|
||||
|
||||
fieldExpression, err := LogicalValueExpr(ctx, orgID, startNs, endNs, fm, logical)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Coercion switches only on the data type, which every member shares, so
|
||||
// the first member stands in for the field.
|
||||
fieldExpression, value = DataTypeCollisionHandledFieldName(logical.Single(), value, fieldExpression, operator)
|
||||
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorEqual:
|
||||
return sb.E(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorNotEqual:
|
||||
return sb.NE(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorGreaterThan:
|
||||
return sb.G(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorGreaterThanOrEq:
|
||||
return sb.GE(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorLessThan:
|
||||
return sb.LT(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorLessThanOrEq:
|
||||
return sb.LE(fieldExpression, value), nil
|
||||
|
||||
case qbtypes.FilterOperatorLike:
|
||||
return sb.Like(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorNotLike:
|
||||
return sb.NotLike(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorILike:
|
||||
return sb.ILike(fieldExpression, value), nil
|
||||
case qbtypes.FilterOperatorNotILike:
|
||||
return sb.NotILike(fieldExpression, value), nil
|
||||
|
||||
case qbtypes.FilterOperatorContains:
|
||||
return sb.ILike(fieldExpression, fmt.Sprintf("%%%s%%", value)), nil
|
||||
case qbtypes.FilterOperatorNotContains:
|
||||
return sb.NotILike(fieldExpression, fmt.Sprintf("%%%s%%", value)), nil
|
||||
|
||||
case qbtypes.FilterOperatorRegexp:
|
||||
return fmt.Sprintf(`match(%s, %s)`, sqlbuilder.Escape(fieldExpression), sb.Var(value)), nil
|
||||
case qbtypes.FilterOperatorNotRegexp:
|
||||
return fmt.Sprintf(`NOT match(%s, %s)`, sqlbuilder.Escape(fieldExpression), sb.Var(value)), nil
|
||||
|
||||
case qbtypes.FilterOperatorBetween:
|
||||
values, ok := value.([]any)
|
||||
if !ok || len(values) != 2 {
|
||||
return "", qbtypes.ErrBetweenValues
|
||||
}
|
||||
return sb.Between(fieldExpression, values[0], values[1]), nil
|
||||
case qbtypes.FilterOperatorNotBetween:
|
||||
values, ok := value.([]any)
|
||||
if !ok || len(values) != 2 {
|
||||
return "", qbtypes.ErrBetweenValues
|
||||
}
|
||||
return sb.NotBetween(fieldExpression, values[0], values[1]), nil
|
||||
|
||||
// `=`+OR / `!=`+AND instead of IN / NOT IN, to make use of the index
|
||||
case qbtypes.FilterOperatorIn:
|
||||
values, ok := value.([]any)
|
||||
if !ok {
|
||||
return "", qbtypes.ErrInValues
|
||||
}
|
||||
conditions := make([]string, 0, len(values))
|
||||
for _, item := range values {
|
||||
cond, err := LogicalFamilyCondition(ctx, orgID, startNs, endNs, fm, logical, qbtypes.FilterOperatorEqual, item, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conditions = append(conditions, cond)
|
||||
}
|
||||
return sb.Or(conditions...), nil
|
||||
case qbtypes.FilterOperatorNotIn:
|
||||
values, ok := value.([]any)
|
||||
if !ok {
|
||||
return "", qbtypes.ErrInValues
|
||||
}
|
||||
conditions := make([]string, 0, len(values))
|
||||
for _, item := range values {
|
||||
cond, err := LogicalFamilyCondition(ctx, orgID, startNs, endNs, fm, logical, qbtypes.FilterOperatorNotEqual, item, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conditions = append(conditions, cond)
|
||||
}
|
||||
return sb.And(conditions...), nil
|
||||
|
||||
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
|
||||
pred, err := LogicalExistsExpr(ctx, orgID, startNs, endNs, fm, logical, operator == qbtypes.FilterOperatorExists)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sqlbuilder.Escape(pred), nil
|
||||
}
|
||||
return "", qbtypes.ErrUnsupportedOperator
|
||||
}
|
||||
@@ -4,66 +4,57 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/semconv"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// SemconvFamiliesEnabled evaluates the resolve_semconv_families flag for the
|
||||
// semconvFamiliesEnabled evaluates the resolve_semconv_families flag for the
|
||||
// org. A nil flagger means off, so a caller without family support stays
|
||||
// literal by default.
|
||||
func SemconvFamiliesEnabled(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger) bool {
|
||||
func semconvFamiliesEnabled(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger) bool {
|
||||
if fl == nil {
|
||||
return false
|
||||
}
|
||||
return fl.BooleanOrEmpty(ctx, flagger.FeatureResolveSemconvFamilies, featuretypes.NewFlaggerEvaluationContext(orgID))
|
||||
}
|
||||
|
||||
// ExpandKeySelectorsForFamilies adds selectors for the other spellings of
|
||||
// each semantic-convention family that a selector names. The metadata fetched
|
||||
// for a query then contains each spelling that MatchingLogicalFields can
|
||||
// group. This function is the prefetch of the resolution layer: statement
|
||||
// builders call it after they derive the selectors, and the metadata store
|
||||
// stays family-blind (autocomplete responses keep the literal spelling that
|
||||
// the user typed). It does nothing when the resolve_semconv_families flag is
|
||||
// off for the org. Fuzzy (search-style) selectors never expand.
|
||||
//
|
||||
// Every call site pairs this prefetch with MatchingLogicalFields at query
|
||||
// time. A site that forgets either half degrades soft: the metadata lacks the
|
||||
// sibling (or the grouping never runs), and the name stays literal — never a
|
||||
// wrong merge.
|
||||
// ExpandKeySelectorsForFamilies adds selectors for the other members of each
|
||||
// semantic-convention family that a selector names. The metadata fetched for
|
||||
// a query then contains each spelling that MatchingLogicalFields can group.
|
||||
// This function is the prefetch of the resolution layer: statement builders
|
||||
// call it after they derive the selectors, and the metadata store stays
|
||||
// family-blind (autocomplete responses keep the literal spelling that the
|
||||
// user typed). It does nothing when the resolve_semconv_families flag is off
|
||||
// for the org. Only trace selectors expand today, because that matches the
|
||||
// family support. Fuzzy (search-style) selectors never expand.
|
||||
func ExpandKeySelectorsForFamilies(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, selectors []*telemetrytypes.FieldKeySelector) []*telemetrytypes.FieldKeySelector {
|
||||
if !SemconvFamiliesEnabled(ctx, orgID, fl) {
|
||||
if !semconvFamiliesEnabled(ctx, orgID, fl) {
|
||||
return selectors
|
||||
}
|
||||
|
||||
// Two selectors may share a name under different contexts, signals, or
|
||||
// data types, and each needs its own sibling selectors, so the dedupe key
|
||||
// is the full identity.
|
||||
identity := func(selector *telemetrytypes.FieldKeySelector, name string) string {
|
||||
return selector.Signal.StringValue() + ";" + selector.FieldContext.StringValue() + ";" + selector.FieldDataType.StringValue() + ";" + name
|
||||
}
|
||||
out := selectors
|
||||
seen := make(map[string]bool, len(selectors))
|
||||
for _, selector := range selectors {
|
||||
seen[identity(selector, selector.Name)] = true
|
||||
seen[selector.Name] = true
|
||||
}
|
||||
|
||||
for _, selector := range selectors {
|
||||
if selector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeFuzzy {
|
||||
if selector.Signal != telemetrytypes.SignalTraces ||
|
||||
selector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeFuzzy {
|
||||
continue
|
||||
}
|
||||
members := familySpellings(telemetrytypes.FieldKeySelector{
|
||||
Name: selector.Name,
|
||||
Signal: selector.Signal,
|
||||
FieldContext: selector.FieldContext,
|
||||
MetricContext: selector.MetricContext,
|
||||
members := semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
|
||||
Name: selector.Name,
|
||||
Signal: selector.Signal,
|
||||
FieldContext: selector.FieldContext,
|
||||
})
|
||||
for _, member := range members {
|
||||
if seen[identity(selector, member)] {
|
||||
if seen[member] {
|
||||
continue
|
||||
}
|
||||
seen[identity(selector, member)] = true
|
||||
seen[member] = true
|
||||
expanded := *selector
|
||||
expanded.Name = member
|
||||
out = append(out, &expanded)
|
||||
|
||||
@@ -75,29 +75,15 @@ func WrapAsLogicalFields(requestedName string, keys []*telemetrytypes.TelemetryF
|
||||
}
|
||||
|
||||
// SingleKeys flattens logical fields to their single members. It is the
|
||||
// adapter for signals whose condition builders compile per physical key. A
|
||||
// family in the input is a wiring error — the caller's signal has no family
|
||||
// compiler, and flattening would silently read one spelling — so it returns
|
||||
// an error instead.
|
||||
func SingleKeys(fields []*telemetrytypes.LogicalField) ([]*telemetrytypes.TelemetryFieldKey, error) {
|
||||
// adapter for signals whose fields are single-member by construction (every
|
||||
// signal without family support); their condition builders keep compiling per
|
||||
// physical key.
|
||||
func SingleKeys(fields []*telemetrytypes.LogicalField) []*telemetrytypes.TelemetryFieldKey {
|
||||
keys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if field.IsFamily() {
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "field %q resolved to a family, and this signal compiles single keys only", field.Name)
|
||||
}
|
||||
keys = append(keys, field.Single())
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// ReadsOtherSpelling reports whether the resolved logical field reads a
|
||||
// spelling different from the requested name: a family merges several
|
||||
// spellings, and a cross-spelling single member reads the one stored sibling
|
||||
// of the requested name. Mappers route such fields through the resolved
|
||||
// members instead of the requested-name flow, so a column reads the same
|
||||
// rows the filter matches.
|
||||
func ReadsOtherSpelling(logical *telemetrytypes.LogicalField, requested string) bool {
|
||||
return logical.IsFamily() || logical.Single().Name != requested
|
||||
return keys
|
||||
}
|
||||
|
||||
// NewKeyNotFoundError builds the error a condition builder returns when a filter term
|
||||
@@ -121,8 +107,8 @@ func SynthesizeKeys(field *telemetrytypes.TelemetryFieldKey, value any) []*telem
|
||||
fieldContext = telemetrytypes.FieldContextAttribute
|
||||
}
|
||||
fieldDataType := field.FieldDataType
|
||||
// Resource and scope values are strings; pin the type so operand coercion applies.
|
||||
if (fieldContext == telemetrytypes.FieldContextResource || fieldContext == telemetrytypes.FieldContextScope) &&
|
||||
// Resource values are strings; pin the type so operand coercion applies.
|
||||
if fieldContext == telemetrytypes.FieldContextResource &&
|
||||
fieldDataType == telemetrytypes.FieldDataTypeUnspecified {
|
||||
fieldDataType = telemetrytypes.FieldDataTypeString
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/semconv"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// Legacy metric storage spellings. The normalized era wrote label and metric
|
||||
// names with underscores, and the span-metrics pipeline additionally wrote
|
||||
// resource attributes with a resource_ prefix. This is a compatibility shim
|
||||
// over aging data, not a property of the metrics signal: delete it when the
|
||||
// span-metrics pipeline revisit lands and the normalized-era data ages out.
|
||||
// The vocabulary itself (pkg/semconv) knows only canonical dotted names.
|
||||
|
||||
const legacyResourcePrefix = "resource_"
|
||||
|
||||
// MetricLabelSpellings returns the storage spellings that may hold
|
||||
// selector.Name in metric labels: every admitted family member expanded into
|
||||
// its dotted, normalized, and resource_-prefixed layouts, ordered
|
||||
// member-major with the requested shape's layout first. A name outside an
|
||||
// enabled family — or one the selector leaves ambiguous — is returned
|
||||
// unchanged.
|
||||
func MetricLabelSpellings(selector telemetrytypes.FieldKeySelector) []string {
|
||||
lookupSelector := selector
|
||||
lookupSelector.Name = strings.TrimPrefix(selector.Name, legacyResourcePrefix)
|
||||
|
||||
members, style, ok := metricVocabulary(semconv.KindAttribute, lookupSelector)
|
||||
if !ok {
|
||||
return []string{selector.Name}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(members)*4)
|
||||
resourceFirst := selector.FieldContext != telemetrytypes.FieldContextAttribute ||
|
||||
strings.HasPrefix(selector.Name, legacyResourcePrefix)
|
||||
for _, member := range members {
|
||||
variants := []string{member, normalizedMetricSpelling(member)}
|
||||
if style == legacySpellingNormalized {
|
||||
variants[0], variants[1] = variants[1], variants[0]
|
||||
}
|
||||
|
||||
if resourceFirst {
|
||||
for _, variant := range variants {
|
||||
result = appendUniqueSpelling(result, legacyResourcePrefix+variant)
|
||||
}
|
||||
}
|
||||
for _, variant := range variants {
|
||||
result = appendUniqueSpelling(result, variant)
|
||||
}
|
||||
if !resourceFirst {
|
||||
for _, variant := range variants {
|
||||
result = appendUniqueSpelling(result, legacyResourcePrefix+variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MetricNameSpellings returns the storage names of a metric-name family in
|
||||
// the requested layout: both layouts are valid metric identities and must not
|
||||
// be mixed in one query.
|
||||
func MetricNameSpellings(name string) []string {
|
||||
selector := telemetrytypes.FieldKeySelector{
|
||||
Name: name,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
FieldContext: telemetrytypes.FieldContextMetric,
|
||||
}
|
||||
members, style, ok := metricVocabulary(semconv.KindMetric, selector)
|
||||
if !ok {
|
||||
return []string{name}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(members))
|
||||
for _, member := range members {
|
||||
if style == legacySpellingNormalized {
|
||||
member = normalizedMetricSpelling(member)
|
||||
}
|
||||
result = appendUniqueSpelling(result, member)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type legacySpelling int
|
||||
|
||||
const (
|
||||
legacySpellingDotted legacySpelling = iota
|
||||
legacySpellingNormalized
|
||||
)
|
||||
|
||||
// metricVocabulary resolves a metric spelling to its family members: first as
|
||||
// the canonical dotted name, then by comparing the normalized layout of every
|
||||
// vocabulary spelling. The ambiguity rule of the vocabulary applies to both
|
||||
// layouts: a name that admits several families stays unresolved.
|
||||
func metricVocabulary(kind semconv.Kind, selector telemetrytypes.FieldKeySelector) ([]string, legacySpelling, bool) {
|
||||
if members := semconv.Members(kind, selector); len(members) > 1 {
|
||||
return members, legacySpellingDotted, true
|
||||
}
|
||||
|
||||
dotted, ok := denormalizedName(kind, selector)
|
||||
if !ok {
|
||||
return nil, legacySpellingDotted, false
|
||||
}
|
||||
dottedSelector := selector
|
||||
dottedSelector.Name = dotted
|
||||
members := semconv.Members(kind, dottedSelector)
|
||||
if len(members) <= 1 {
|
||||
return nil, legacySpellingDotted, false
|
||||
}
|
||||
return members, legacySpellingNormalized, true
|
||||
}
|
||||
|
||||
// normalizedVocabulary indexes every vocabulary spelling by kind and
|
||||
// normalized layout: one candidate per family — the first of its spellings
|
||||
// with that layout — with cross-family duplicates kept, so the ambiguity
|
||||
// count below sees every family that carries the layout.
|
||||
var normalizedVocabulary = buildNormalizedVocabulary()
|
||||
|
||||
func buildNormalizedVocabulary() map[semconv.Kind]map[string][]string {
|
||||
index := make(map[semconv.Kind]map[string][]string)
|
||||
for family := range semconv.All() {
|
||||
if index[family.Kind()] == nil {
|
||||
index[family.Kind()] = make(map[string][]string)
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
for _, name := range append([]string{family.Current()}, family.Old()...) {
|
||||
normalized := normalizedMetricSpelling(name)
|
||||
if seen[normalized] {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = true
|
||||
index[family.Kind()][normalized] = append(index[family.Kind()][normalized], name)
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// denormalizedName maps a normalized spelling back to its unique canonical
|
||||
// vocabulary name. The reverse mapping is lossy in general (a dot and an
|
||||
// underscore normalize identically), so only an unambiguous match resolves.
|
||||
func denormalizedName(kind semconv.Kind, selector telemetrytypes.FieldKeySelector) (string, bool) {
|
||||
found, foundName := 0, ""
|
||||
for _, name := range normalizedVocabulary[kind][selector.Name] {
|
||||
probe := selector
|
||||
probe.Name = name
|
||||
if len(semconv.Members(kind, probe)) > 1 {
|
||||
found++
|
||||
foundName = name
|
||||
}
|
||||
}
|
||||
if found != 1 {
|
||||
return "", false
|
||||
}
|
||||
return foundName, true
|
||||
}
|
||||
|
||||
func normalizedMetricSpelling(name string) string {
|
||||
return strings.ReplaceAll(name, ".", "_")
|
||||
}
|
||||
|
||||
func appendUniqueSpelling(values []string, value string) []string {
|
||||
for _, existing := range values {
|
||||
if existing == value {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, value)
|
||||
}
|
||||
|
||||
// FamilyMetricNames returns the storage names a metric query must read: the
|
||||
// requested name plus the other spellings of its metric-name family when the
|
||||
// resolve_semconv_families flag is on for the org.
|
||||
func FamilyMetricNames(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, metricName string) []string {
|
||||
if !SemconvFamiliesEnabled(ctx, orgID, fl) {
|
||||
return []string{metricName}
|
||||
}
|
||||
return MetricNameSpellings(metricName)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMetricLabelSpellingsExpandsLegacyLayouts(t *testing.T) {
|
||||
selector := telemetrytypes.FieldKeySelector{
|
||||
Name: "deployment.environment",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"resource_deployment.environment.name", "resource_deployment_environment_name",
|
||||
"deployment.environment.name", "deployment_environment_name",
|
||||
"resource_deployment.environment", "resource_deployment_environment",
|
||||
"deployment.environment", "deployment_environment",
|
||||
}, MetricLabelSpellings(selector), "members expand into dotted, normalized, and resource_-prefixed layouts")
|
||||
}
|
||||
|
||||
func TestMetricLabelSpellingsPreservesRequestedLayout(t *testing.T) {
|
||||
selector := telemetrytypes.FieldKeySelector{
|
||||
Name: "resource_deployment_environment",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"resource_deployment_environment_name", "resource_deployment.environment.name",
|
||||
"deployment_environment_name", "deployment.environment.name",
|
||||
"resource_deployment_environment", "resource_deployment.environment",
|
||||
"deployment_environment", "deployment.environment",
|
||||
}, MetricLabelSpellings(selector), "a normalized request lists normalized layouts first and keeps resource_ variants")
|
||||
}
|
||||
|
||||
func TestMetricLabelSpellingsStaysLiteralOutsideTheVocabulary(t *testing.T) {
|
||||
selector := telemetrytypes.FieldKeySelector{
|
||||
Name: "http.route",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"http.route"}, MetricLabelSpellings(selector))
|
||||
}
|
||||
|
||||
func TestMetricNameSpellingsPreservesLayout(t *testing.T) {
|
||||
assert.Equal(t, []string{"k8s.pod.cpu.usage", "k8s.pod.cpu.utilization"}, MetricNameSpellings("k8s.pod.cpu.utilization"))
|
||||
assert.Equal(t, []string{"k8s_pod_cpu_usage", "k8s_pod_cpu_utilization"}, MetricNameSpellings("k8s_pod_cpu_utilization"))
|
||||
assert.Equal(t, []string{"k8s.pod.cpu.usage", "k8s.pod.cpu.utilization"}, MetricNameSpellings("k8s.pod.cpu.usage"))
|
||||
assert.Equal(t, []string{"http.server.duration"}, MetricNameSpellings("http.server.duration"))
|
||||
}
|
||||
@@ -52,13 +52,8 @@ func LogicalValueExpr(
|
||||
return "COALESCE(" + strings.Join(values, ", ") + ", '')", nil
|
||||
}
|
||||
|
||||
// Numeric and boolean maps read their zero value for an absent key, so the
|
||||
// tail keeps single-key semantics for rows without any member — the same
|
||||
// contract as the '' tail above.
|
||||
tail := "0"
|
||||
if logical.FieldDataType == telemetrytypes.FieldDataTypeBool {
|
||||
tail = "false"
|
||||
}
|
||||
// Numeric and boolean maps return zero for an absent key. If a family of
|
||||
// either type is enabled, this tail must become zero too.
|
||||
branches := make([]string, 0, len(logical.Members)*2)
|
||||
for i, member := range logical.Members {
|
||||
guard, err := fm.ExistsFor(ctx, orgID, tsStart, tsEnd, member, true)
|
||||
@@ -67,7 +62,7 @@ func LogicalValueExpr(
|
||||
}
|
||||
branches = append(branches, guard, memberExprs[i])
|
||||
}
|
||||
return "multiIf(" + strings.Join(branches, ", ") + ", " + tail + ")", nil
|
||||
return "multiIf(" + strings.Join(branches, ", ") + ", NULL)", nil
|
||||
}
|
||||
|
||||
// LogicalExistsExpr returns the existence predicate for a resolved logical
|
||||
|
||||
@@ -31,7 +31,7 @@ func (stubFieldMapper) ColumnFor(context.Context, valuer.UUID, uint64, uint64, *
|
||||
return nil, qbtypes.ErrColumnNotFound
|
||||
}
|
||||
|
||||
func (stubFieldMapper) ColumnExpressionFor(context.Context, valuer.UUID, uint64, uint64, *telemetrytypes.TelemetryFieldKey, []*telemetrytypes.LogicalField, telemetrytypes.FieldDataType, map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
func (stubFieldMapper) ColumnExpressionFor(context.Context, valuer.UUID, uint64, uint64, *telemetrytypes.TelemetryFieldKey, telemetrytypes.FieldDataType, map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||
return "", qbtypes.ErrColumnNotFound
|
||||
}
|
||||
|
||||
@@ -72,23 +72,7 @@ func TestLogicalValueExprNumericFamilyGuardsEveryMember(t *testing.T) {
|
||||
}
|
||||
expr, err := LogicalValueExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, logical)
|
||||
require.NoError(t, err)
|
||||
// The 0 tail mirrors the '' tail of the string branch: numeric maps read 0
|
||||
// for an absent key, so keyless rows keep single-key semantics.
|
||||
assert.Equal(t, "multiIf(has(current), value(current), has(old), value(old), 0)", expr)
|
||||
}
|
||||
|
||||
func TestLogicalValueExprBoolFamilyReadsFalseForKeylessRows(t *testing.T) {
|
||||
logical := &telemetrytypes.LogicalField{
|
||||
Name: "flag",
|
||||
FieldDataType: telemetrytypes.FieldDataTypeBool,
|
||||
Members: []*telemetrytypes.TelemetryFieldKey{
|
||||
{Name: "current", FieldDataType: telemetrytypes.FieldDataTypeBool},
|
||||
{Name: "old", FieldDataType: telemetrytypes.FieldDataTypeBool},
|
||||
},
|
||||
}
|
||||
expr, err := LogicalValueExpr(context.Background(), valuer.UUID{}, 0, 0, stubFieldMapper{}, logical)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "multiIf(has(current), value(current), has(old), value(old), false)", expr)
|
||||
assert.Equal(t, "multiIf(has(current), value(current), has(old), value(old), NULL)", expr)
|
||||
}
|
||||
|
||||
func TestLogicalExistsExprSingleMemberDelegatesToExistsFor(t *testing.T) {
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestFamiliesOffByDefault(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, flaggertest.New(t), telemetrytypes.SignalUnspecified, nil, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, flaggertest.New(t), &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
require.Len(t, fields, 1)
|
||||
assert.False(t, fields[0].IsFamily())
|
||||
assert.Equal(t, []string{"deployment.environment.name"}, memberNames(fields[0]))
|
||||
@@ -76,7 +76,7 @@ func TestMatchingLogicalFieldsGroupsFamilyMembers(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, requested := range []string{"deployment.environment.name", "deployment.environment"} {
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), telemetrytypes.SignalUnspecified, nil, &telemetrytypes.TelemetryFieldKey{Name: requested}, fieldKeys)
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{Name: requested}, fieldKeys)
|
||||
require.Len(t, fields, 1, "a family is one logical field, requested via %s", requested)
|
||||
logical := fields[0]
|
||||
assert.Equal(t, requested, logical.Name, "response identity is the requested spelling")
|
||||
@@ -106,7 +106,7 @@ func TestMatchingLogicalFieldsOrdersMembersByFamilyRank(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), telemetrytypes.SignalUnspecified, nil, &telemetrytypes.TelemetryFieldKey{
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{
|
||||
Name: "deployment.environment.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
}, fieldKeys)
|
||||
@@ -115,8 +115,9 @@ func TestMatchingLogicalFieldsOrdersMembersByFamilyRank(t *testing.T) {
|
||||
assert.Equal(t, []string{"resource.deployment.environment.name", "deployment.environment"}, memberNames(fields[0]))
|
||||
}
|
||||
|
||||
// Log entries group into families exactly like trace entries.
|
||||
func TestMatchingLogicalFieldsGroupsLogEntries(t *testing.T) {
|
||||
// Non-trace signals have no family support: the requested spelling stays
|
||||
// literal, and a family member name never pulls in its siblings.
|
||||
func TestMatchingLogicalFieldsKeepsLogsLiteral(t *testing.T) {
|
||||
logsKey := func(name string) *telemetrytypes.TelemetryFieldKey {
|
||||
return &telemetrytypes.TelemetryFieldKey{
|
||||
Name: name,
|
||||
@@ -130,61 +131,10 @@ func TestMatchingLogicalFieldsGroupsLogEntries(t *testing.T) {
|
||||
"deployment.environment": {logsKey("deployment.environment")},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), telemetrytypes.SignalLogs, nil, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
require.Len(t, fields, 1)
|
||||
assert.True(t, fields[0].IsFamily())
|
||||
assert.Equal(t, []string{"deployment.environment.name", "deployment.environment"}, memberNames(fields[0]))
|
||||
}
|
||||
|
||||
// Metric entries group across the stored label spellings of the family, in
|
||||
// member-major order: every spelling of the current name precedes the first
|
||||
// spelling of the old one.
|
||||
func TestMatchingLogicalFieldsGroupsMetricSpellings(t *testing.T) {
|
||||
metricsKey := func(name string) *telemetrytypes.TelemetryFieldKey {
|
||||
return &telemetrytypes.TelemetryFieldKey{
|
||||
Name: name,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}
|
||||
}
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {metricsKey("deployment.environment.name")},
|
||||
"deployment_environment_name": {metricsKey("deployment_environment_name")},
|
||||
"deployment.environment": {metricsKey("deployment.environment")},
|
||||
"deployment_environment": {metricsKey("deployment_environment")},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), telemetrytypes.SignalMetrics, nil, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment"}, fieldKeys)
|
||||
require.Len(t, fields, 1)
|
||||
assert.True(t, fields[0].IsFamily())
|
||||
assert.Equal(t, []string{
|
||||
"deployment.environment.name", "deployment_environment_name",
|
||||
"deployment.environment", "deployment_environment",
|
||||
}, memberNames(fields[0]))
|
||||
}
|
||||
|
||||
// A non-string entry never joins a family: the merged expression has no
|
||||
// common ClickHouse type across storages yet.
|
||||
func TestMatchingLogicalFieldsKeepsNumberEntriesSingle(t *testing.T) {
|
||||
numberKey := func(name string) *telemetrytypes.TelemetryFieldKey {
|
||||
return &telemetrytypes.TelemetryFieldKey{
|
||||
Name: name,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
||||
}
|
||||
}
|
||||
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"deployment.environment.name": {numberKey("deployment.environment.name")},
|
||||
"deployment.environment": {numberKey("deployment.environment")},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), telemetrytypes.SignalMetrics, nil, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment"}, fieldKeys)
|
||||
require.Len(t, fields, 2)
|
||||
for _, logical := range fields {
|
||||
assert.False(t, logical.IsFamily())
|
||||
}
|
||||
assert.False(t, fields[0].IsFamily())
|
||||
assert.Equal(t, []string{"deployment.environment.name"}, memberNames(fields[0]))
|
||||
}
|
||||
|
||||
// A family and a genuine same-name collision stack cleanly: the family stays
|
||||
@@ -215,7 +165,7 @@ func TestResolveLogicalFieldsKeepsFamilyThroughAmbiguity(t *testing.T) {
|
||||
}
|
||||
|
||||
requested := &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), telemetrytypes.SignalUnspecified, nil, requested, fieldKeys)
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), requested, fieldKeys)
|
||||
require.Len(t, fields, 2, "resource family + attribute collision")
|
||||
|
||||
resolved, warning := ResolveLogicalFields(requested, fields)
|
||||
@@ -243,7 +193,7 @@ func TestMatchingLogicalFieldsNeverMergesAcrossDataTypes(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), telemetrytypes.SignalUnspecified, nil, &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
fields := MatchingLogicalFields(context.Background(), valuer.UUID{}, familiesOn(t), &telemetrytypes.TelemetryFieldKey{Name: "deployment.environment.name"}, fieldKeys)
|
||||
require.Len(t, fields, 2)
|
||||
for _, logical := range fields {
|
||||
assert.False(t, logical.IsFamily())
|
||||
@@ -268,15 +218,11 @@ func TestExpandKeySelectorsForFamilies(t *testing.T) {
|
||||
"service.name",
|
||||
"deployment.environment.name",
|
||||
"deployment.environment",
|
||||
"deployment.environment",
|
||||
}, names, "each selector identity gets its own sibling; non-family names stay untouched")
|
||||
}, names, "one sibling selector for the trace family member; logs and non-family names untouched")
|
||||
|
||||
tracesSibling := expanded[len(expanded)-2]
|
||||
assert.Equal(t, telemetrytypes.SignalTraces, tracesSibling.Signal)
|
||||
assert.Equal(t, telemetrytypes.FieldSelectorMatchTypeExact, tracesSibling.SelectorMatchType)
|
||||
logsSibling := expanded[len(expanded)-1]
|
||||
assert.Equal(t, telemetrytypes.SignalLogs, logsSibling.Signal,
|
||||
"a same-named selector under another signal must not steal the sibling")
|
||||
sibling := expanded[len(expanded)-1]
|
||||
assert.Equal(t, telemetrytypes.SignalTraces, sibling.Signal)
|
||||
assert.Equal(t, telemetrytypes.FieldSelectorMatchTypeExact, sibling.SelectorMatchType)
|
||||
}
|
||||
|
||||
func TestExpandKeySelectorsForFamiliesDeduplicatesAndSkipsFuzzy(t *testing.T) {
|
||||
|
||||
@@ -56,17 +56,6 @@ func QueryStringToKeysSelectors(query string) []*telemetrytypes.FieldKeySelector
|
||||
FieldDataType: key.FieldDataType,
|
||||
})
|
||||
}
|
||||
|
||||
// todo(tushar): consider reverting changes done to this method in below PR to avoid scope specific checks
|
||||
// https://github.com/SigNoz/signoz/issues/11374
|
||||
if key.FieldContext == telemetrytypes.FieldContextScope {
|
||||
keys = append(keys, &telemetrytypes.FieldKeySelector{
|
||||
Name: key.FieldContext.StringValue() + "." + key.Name,
|
||||
Signal: key.Signal,
|
||||
FieldContext: telemetrytypes.FieldContextUnspecified, // this allows 'scope.' prefix for keys with other context as well
|
||||
FieldDataType: key.FieldDataType,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,44 +72,6 @@ func TestQueryToKeys(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: `scope.version = '1.0.0'`,
|
||||
expectedKeys: []telemetrytypes.FieldKeySelector{
|
||||
{
|
||||
Name: "version",
|
||||
Signal: telemetrytypes.SignalUnspecified,
|
||||
FieldContext: telemetrytypes.FieldContextScope,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
||||
},
|
||||
{
|
||||
Name: "scope.version",
|
||||
Signal: telemetrytypes.SignalUnspecified,
|
||||
FieldContext: telemetrytypes.FieldContextUnspecified,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// A scope attribute whose own name carries a `scope.` prefix. `scope.prefixed`
|
||||
// normalizes to {prefixed, scope}; the second selector re-adds the prefix so the
|
||||
// metadata fetch can target the attribute's exact key `scope.prefixed` rather than
|
||||
// relying on the broad `%prefixed%` match.
|
||||
query: `scope.prefixed = 'x'`,
|
||||
expectedKeys: []telemetrytypes.FieldKeySelector{
|
||||
{
|
||||
Name: "prefixed",
|
||||
Signal: telemetrytypes.SignalUnspecified,
|
||||
FieldContext: telemetrytypes.FieldContextScope,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
||||
},
|
||||
{
|
||||
Name: "scope.prefixed",
|
||||
Signal: telemetrytypes.SignalUnspecified,
|
||||
FieldContext: telemetrytypes.FieldContextUnspecified,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// CompileScope carries the compile-time context of one query as one value: the org
|
||||
// and the time range every physical read (evolution selection, probes) needs.
|
||||
// The generic term and column compilers thread it instead of loose
|
||||
// positional parameters, so a dropped axis is a compile error.
|
||||
type CompileScope struct {
|
||||
OrgID valuer.UUID
|
||||
StartNs uint64
|
||||
EndNs uint64
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
// TermSchema is the per-signal surface of the generic term compiler. The
|
||||
// methods answer what exists and how one resolved field compiles; the flow —
|
||||
// evidence, synthesis, the resource-filter policy, the per-field loop — is
|
||||
// CompileTerm and is written once. A signal implements exactly three
|
||||
// methods; term-level intercepts (search(), function rejects) stay in the
|
||||
// signal's ConditionFor delegate, in front of the flow.
|
||||
type TermSchema interface {
|
||||
// AmendEvidence folds intrinsic storage into non-empty resolved evidence
|
||||
// (traces prepends the span column for a bare name). Most signals return
|
||||
// the fields unchanged.
|
||||
AmendEvidence(ctx context.Context, scope CompileScope, key *telemetrytypes.TelemetryFieldKey, fields []*telemetrytypes.LogicalField) []*telemetrytypes.LogicalField
|
||||
|
||||
// Synthesize returns the logical fields for a name resolution found no
|
||||
// evidence for, with any warnings (not-found advisories). The error is
|
||||
// terminal (unknown key). A (nil, nil, nil) return skips the term: the
|
||||
// signal contributes no condition for the name (resource filter).
|
||||
Synthesize(ctx context.Context, scope CompileScope, key *telemetrytypes.TelemetryFieldKey, operator qbtypes.FilterOperator, value any, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.LogicalField, []string, error)
|
||||
|
||||
// CompileField compiles one resolved field into a condition. This is the
|
||||
// signal's storage residue — body JSON, index hints, its own operator
|
||||
// forms — and the exists-guard policy, which differs per storage.
|
||||
// CompileFieldWithSharedOperators is the canonical implementation for
|
||||
// map-backed fields.
|
||||
CompileField(ctx context.Context, scope CompileScope, logical *telemetrytypes.LogicalField, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, []string, error)
|
||||
}
|
||||
|
||||
// SkipResourcePolicy states how ConditionBuilderOptions.SkipResourceFilter
|
||||
// applies to a signal's resolved fields.
|
||||
type SkipResourcePolicy int
|
||||
|
||||
const (
|
||||
// SkipResourceNone: the option does not apply (metrics, rule state history).
|
||||
SkipResourceNone SkipResourcePolicy = iota
|
||||
// SkipResourceDrop: a resource sub-query covers resource fields, so drop
|
||||
// them from the evidence; when none remain the term is already covered.
|
||||
// Synthesized fields are exempt: the sub-query skips unknown keys.
|
||||
SkipResourceDrop
|
||||
// SkipResourceOnly: the signal stores resource attributes only, so keep
|
||||
// resource fields and omit everything else (the resource fingerprint
|
||||
// filter).
|
||||
SkipResourceOnly
|
||||
)
|
||||
|
||||
// CompileTerm is the generic flow of one filter term: resolve the evidence,
|
||||
// amend it, synthesize when it is empty, apply the resource-filter policy,
|
||||
// and compile every field through the signal's CompileField. Warnings keep
|
||||
// their order: ambiguity first, synthesis advisories second, per-field
|
||||
// advisories last.
|
||||
func CompileTerm(
|
||||
ctx context.Context,
|
||||
scope CompileScope,
|
||||
schema TermSchema,
|
||||
policy SkipResourcePolicy,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
evidence []*telemetrytypes.LogicalField,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
fields, warning := ResolveLogicalFields(key, evidence)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
fields = schema.AmendEvidence(ctx, scope, key, fields)
|
||||
|
||||
synthesized := false
|
||||
if len(fields) == 0 {
|
||||
synthesizedFields, synthWarnings, err := schema.Synthesize(ctx, scope, key, operator, value, fieldKeys)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
warnings = append(warnings, synthWarnings...)
|
||||
if len(synthesizedFields) == 0 {
|
||||
return nil, warnings, nil
|
||||
}
|
||||
fields = synthesizedFields
|
||||
synthesized = true
|
||||
}
|
||||
|
||||
switch policy {
|
||||
case SkipResourceDrop:
|
||||
if options.SkipResourceFilter && !synthesized {
|
||||
filtered := make([]*telemetrytypes.LogicalField, 0, len(fields))
|
||||
for _, logical := range fields {
|
||||
if logical.FieldContext != telemetrytypes.FieldContextResource {
|
||||
filtered = append(filtered, logical)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, warnings, nil
|
||||
}
|
||||
fields = filtered
|
||||
}
|
||||
case SkipResourceOnly:
|
||||
filtered := make([]*telemetrytypes.LogicalField, 0, len(fields))
|
||||
for _, logical := range fields {
|
||||
if logical.FieldContext == telemetrytypes.FieldContextResource {
|
||||
filtered = append(filtered, logical)
|
||||
}
|
||||
}
|
||||
fields = filtered
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(fields))
|
||||
for _, logical := range fields {
|
||||
cond, fieldWarnings, err := schema.CompileField(ctx, scope, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
warnings = append(warnings, fieldWarnings...)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
// CompileFieldWithSharedOperators is the canonical CompileField for
|
||||
// map-backed fields: the shared operator switch over the merged (family) or
|
||||
// single value expression, with the default exists guard AND-ed for positive
|
||||
// operators when guard is true. The keyless-row contract lives here: the
|
||||
// guard keeps an empty-string equality from matching rows without the key,
|
||||
// and its absence on negative operators keeps them set-complement over all
|
||||
// rows.
|
||||
func CompileFieldWithSharedOperators(
|
||||
ctx context.Context,
|
||||
scope CompileScope,
|
||||
fm qbtypes.FieldMapper,
|
||||
logical *telemetrytypes.LogicalField,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
guard bool,
|
||||
) (string, error) {
|
||||
condition, err := LogicalFamilyCondition(ctx, scope.OrgID, scope.StartNs, scope.EndNs, fm, logical, operator, value, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if guard && operator.AddDefaultExistsFilter() {
|
||||
existsCondition, err := LogicalFamilyCondition(ctx, scope.OrgID, scope.StartNs, scope.EndNs, fm, logical, qbtypes.FilterOperatorExists, nil, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sb.And(condition, existsCondition), nil
|
||||
}
|
||||
return condition, nil
|
||||
}
|
||||
@@ -42,8 +42,6 @@ type filterExpressionVisitor struct {
|
||||
skipResourceFilter bool
|
||||
skipFullTextFilter bool
|
||||
variables map[string]qbtypes.VariableItem
|
||||
signal telemetrytypes.Signal
|
||||
metricContext *telemetrytypes.MetricContext
|
||||
|
||||
keysWithWarnings map[string]bool
|
||||
startNs uint64
|
||||
@@ -69,11 +67,6 @@ type FilterExprVisitorOpts struct {
|
||||
Variables map[string]qbtypes.VariableItem
|
||||
StartNs uint64
|
||||
EndNs uint64
|
||||
// Signal is the signal the statement builder compiles for; family
|
||||
// resolution uses it for keys that do not carry their own. MetricContext
|
||||
// carries the queried metric name so metric-scoped families resolve.
|
||||
Signal telemetrytypes.Signal
|
||||
MetricContext *telemetrytypes.MetricContext
|
||||
}
|
||||
|
||||
// newFilterExpressionVisitor creates a new filterExpressionVisitor.
|
||||
@@ -90,8 +83,6 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
|
||||
skipResourceFilter: opts.SkipResourceFilter,
|
||||
skipFullTextFilter: opts.SkipFullTextFilter,
|
||||
variables: opts.Variables,
|
||||
signal: opts.Signal,
|
||||
metricContext: opts.MetricContext,
|
||||
keysWithWarnings: make(map[string]bool),
|
||||
startNs: opts.StartNs,
|
||||
endNs: opts.EndNs,
|
||||
@@ -395,7 +386,7 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
// VisitComparison handles all comparison operators.
|
||||
func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
key := v.Visit(ctx.Key()).(*telemetrytypes.TelemetryFieldKey)
|
||||
matching := MatchingLogicalFields(v.context, v.orgID, v.fl, v.signal, v.metricContext, key, v.fieldKeys)
|
||||
matching := MatchingLogicalFields(v.context, v.orgID, v.fl, key, v.fieldKeys)
|
||||
|
||||
// Handle EXISTS specially
|
||||
if ctx.EXISTS() != nil {
|
||||
@@ -746,7 +737,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
conds, ok := v.buildConditions(key, MatchingLogicalFields(v.context, v.orgID, v.fl, v.signal, v.metricContext, key, v.fieldKeys), operator, value)
|
||||
conds, ok := v.buildConditions(key, MatchingLogicalFields(v.context, v.orgID, v.fl, key, v.fieldKeys), operator, value)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
@@ -939,7 +930,7 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
|
||||
// buildConditions invokes the condition builder for a filter term, folding its
|
||||
// warnings/errors into visitor state; returns false if an error was recorded.
|
||||
func (v *filterExpressionVisitor) buildConditions(key *telemetrytypes.TelemetryFieldKey, matching []*telemetrytypes.LogicalField, op qbtypes.FilterOperator, value any) ([]string, bool) {
|
||||
conds, warns, err := v.conditionBuilder.ConditionFor(v.context, v.orgID, v.startNs, v.endNs, key, matching, v.fieldKeys, qbtypes.ConditionBuilderOptions{SkipResourceFilter: v.skipResourceFilter}, op, value, v.builder)
|
||||
conds, warns, err := v.conditionBuilder.ConditionFor(v.context, v.orgID, v.startNs, v.endNs, key, v.fieldKeys, qbtypes.ConditionBuilderOptions{SkipResourceFilter: v.skipResourceFilter}, op, value, v.builder)
|
||||
if err != nil {
|
||||
_, _, _, _, errURL, _ := errors.Unwrapb(err)
|
||||
assignIfEmpty(&v.mainErrorURL, errURL)
|
||||
@@ -996,57 +987,44 @@ func assignIfEmpty(s *string, value string) {
|
||||
}
|
||||
|
||||
// familyMemberNames returns the physical spellings to look up for the
|
||||
// referenced key: the semantic-convention family spellings (current-first)
|
||||
// when the resolve_semconv_families flag is on for the org, else just the
|
||||
// requested name. The key's own signal wins over the caller's; metric lookups
|
||||
// additionally expand each member into its stored label spellings.
|
||||
func familyMemberNames(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, signal telemetrytypes.Signal, metricCtx *telemetrytypes.MetricContext, field *telemetrytypes.TelemetryFieldKey) []string {
|
||||
if !SemconvFamiliesEnabled(ctx, orgID, fl) {
|
||||
// referenced key: the semantic-convention family members (current-first) when
|
||||
// the resolve_semconv_families flag is on for the org and the key can resolve
|
||||
// to traces, else just the requested name. Only trace field mappers understand
|
||||
// families today; logs and metrics keep the requested spelling until theirs
|
||||
// land.
|
||||
func familyMemberNames(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, field *telemetrytypes.TelemetryFieldKey) []string {
|
||||
if !semconvFamiliesEnabled(ctx, orgID, fl) {
|
||||
return []string{field.Name}
|
||||
}
|
||||
if field.Signal != telemetrytypes.SignalUnspecified {
|
||||
signal = field.Signal
|
||||
if field.Signal != telemetrytypes.SignalUnspecified && field.Signal != telemetrytypes.SignalTraces {
|
||||
return []string{field.Name}
|
||||
}
|
||||
return familySpellings(telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: signal,
|
||||
FieldContext: field.FieldContext,
|
||||
MetricContext: metricCtx,
|
||||
return semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: field.FieldContext,
|
||||
})
|
||||
}
|
||||
|
||||
// familySpellings returns the storage spellings for the selector: the plain
|
||||
// vocabulary members, expanded through the legacy metric layouts for the
|
||||
// metrics signal.
|
||||
func familySpellings(selector telemetrytypes.FieldKeySelector) []string {
|
||||
if selector.Signal == telemetrytypes.SignalMetrics {
|
||||
return MetricLabelSpellings(selector)
|
||||
}
|
||||
return semconv.Members(semconv.KindAttribute, selector)
|
||||
}
|
||||
|
||||
// MatchingLogicalFields resolves the referenced key against the metadata map
|
||||
// into logical fields, honoring any context/data type the user specified.
|
||||
//
|
||||
// Physical keys that are members of one semantic-convention family group into
|
||||
// one logical field per (signal, context, data type) identity, members
|
||||
// ordered current-first. Every other matching key becomes its own
|
||||
// single-member logical field. Ambiguity is the length of the returned slice:
|
||||
// one family is one element and is never ambiguous with itself, but the slice
|
||||
// can hold several logical fields — including several family fields, one per
|
||||
// identity, when the family exists under more than one context or data type.
|
||||
// Members alias the metadata map entries; nothing is copied or mutated.
|
||||
//
|
||||
// signal is the signal the caller compiles for and metricCtx the queried
|
||||
// metric, both used only for family resolution; a key that carries its own
|
||||
// signal wins over the caller's.
|
||||
// Physical keys that are members of one semantic-convention family (traces
|
||||
// only today) group into one logical field per (signal, context, data type)
|
||||
// identity, members ordered current-first. Every other matching key becomes
|
||||
// its own single-member logical field. Ambiguity is the length of the
|
||||
// returned slice: one family is one element and is never ambiguous with
|
||||
// itself, but the slice can hold several logical fields — including several
|
||||
// family fields, one per identity, when the family exists under more than
|
||||
// one context or data type. Members alias the metadata map entries; nothing
|
||||
// is copied or mutated.
|
||||
//
|
||||
// Family grouping only happens when the resolve_semconv_families flag is on
|
||||
// for the org. A nil flagger means off: every match then stays a
|
||||
// single-member logical field.
|
||||
func MatchingLogicalFields(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, signal telemetrytypes.Signal, metricCtx *telemetrytypes.MetricContext, field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.LogicalField {
|
||||
members := familyMemberNames(ctx, orgID, fl, signal, metricCtx, field)
|
||||
matches := collectMemberMatches(field, members, metricCtx, fieldKeys)
|
||||
func MatchingLogicalFields(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.LogicalField {
|
||||
members := familyMemberNames(ctx, orgID, fl, field)
|
||||
matches := collectMemberMatches(field, members, fieldKeys)
|
||||
return groupIntoLogicalFields(field.Name, len(members) > 1, matches)
|
||||
}
|
||||
|
||||
@@ -1072,29 +1050,32 @@ func matchesRequestedIdentity(field, item *telemetrytypes.TelemetryFieldKey, con
|
||||
}
|
||||
|
||||
// inFamilyScope reports whether a match found under a sibling member name is
|
||||
// legitimate: the member must be a family spelling of the requested name for
|
||||
// the entry's own signal and context. A member lookup can otherwise find a
|
||||
// same-named field in a scope where the family does not apply.
|
||||
func inFamilyScope(field, item *telemetrytypes.TelemetryFieldKey, memberName string, metricCtx *telemetrytypes.MetricContext) bool {
|
||||
return slices.Contains(familySpellings(telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: item.Signal,
|
||||
FieldContext: item.FieldContext,
|
||||
MetricContext: metricCtx,
|
||||
// legitimate: the entry must be trace metadata, and the member must be in the
|
||||
// family of the requested name for the entry's context. A member lookup can
|
||||
// otherwise find a same-named field in a scope where the family does not
|
||||
// apply.
|
||||
func inFamilyScope(field, item *telemetrytypes.TelemetryFieldKey, memberName string) bool {
|
||||
if item.Signal != telemetrytypes.SignalTraces {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: item.FieldContext,
|
||||
}), memberName)
|
||||
}
|
||||
|
||||
// collectMemberMatches finds the metadata entries for every member spelling:
|
||||
// first under the member names, then under their context-prefixed spellings
|
||||
// (a context can be a legitimate part of a stored name, e.g. `attribute.key`).
|
||||
func collectMemberMatches(field *telemetrytypes.TelemetryFieldKey, members []string, metricCtx *telemetrytypes.MetricContext, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []memberMatch {
|
||||
func collectMemberMatches(field *telemetrytypes.TelemetryFieldKey, members []string, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []memberMatch {
|
||||
matches := make([]memberMatch, 0)
|
||||
collect := func(lookupName string, rank int, memberName string, contextMatched bool) {
|
||||
for _, item := range fieldKeys[lookupName] {
|
||||
if !matchesRequestedIdentity(field, item, contextMatched) {
|
||||
continue
|
||||
}
|
||||
if memberName != field.Name && !inFamilyScope(field, item, memberName, metricCtx) {
|
||||
if memberName != field.Name && !inFamilyScope(field, item, memberName) {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, memberMatch{key: item, rank: rank})
|
||||
@@ -1112,26 +1093,18 @@ func collectMemberMatches(field *telemetrytypes.TelemetryFieldKey, members []str
|
||||
return matches
|
||||
}
|
||||
|
||||
// groupIntoLogicalFields turns matches into logical fields. Entries of a
|
||||
// family-capable signal in family mode group by their (signal, context, data
|
||||
// type) identity; every other entry becomes its own single-member field.
|
||||
// Members sort by family rank at the end: precedence is a property of the
|
||||
// family, not of the order in which the lookups found the members.
|
||||
// groupIntoLogicalFields turns matches into logical fields. Trace entries in
|
||||
// family mode group by their (signal, context, data type) identity; every
|
||||
// other entry becomes its own single-member field. Members sort by family
|
||||
// rank at the end: precedence is a property of the family, not of the order
|
||||
// in which the lookups found the members.
|
||||
func groupIntoLogicalFields(requestedName string, familyMode bool, matches []memberMatch) []*telemetrytypes.LogicalField {
|
||||
fields := make([]*telemetrytypes.LogicalField, 0, len(matches))
|
||||
groups := make(map[string]*telemetrytypes.LogicalField)
|
||||
ranks := make(map[*telemetrytypes.TelemetryFieldKey]int)
|
||||
|
||||
for _, match := range matches {
|
||||
// Non-string entries stay single-member: the merged family expression
|
||||
// has no common ClickHouse type across storages for them yet, and no
|
||||
// numeric or boolean family is enabled. Entries outside the resource
|
||||
// and attribute contexts also stay single-member: a family names an
|
||||
// attribute, and merging a same-named body or scope key would skip
|
||||
// that context's own machinery.
|
||||
if !familyMode || !familySignal(match.key.Signal) ||
|
||||
!familyFieldContext(match.key.FieldContext) ||
|
||||
match.key.FieldDataType != telemetrytypes.FieldDataTypeString {
|
||||
if !familyMode || match.key.Signal != telemetrytypes.SignalTraces {
|
||||
fields = append(fields, telemetrytypes.SingleLogicalField(requestedName, match.key))
|
||||
continue
|
||||
}
|
||||
@@ -1171,19 +1144,3 @@ func groupHasMemberNamed(group *telemetrytypes.LogicalField, name string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func familySignal(signal telemetrytypes.Signal) bool {
|
||||
switch signal {
|
||||
case telemetrytypes.SignalTraces, telemetrytypes.SignalLogs, telemetrytypes.SignalMetrics:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func familyFieldContext(fieldContext telemetrytypes.FieldContext) bool {
|
||||
switch fieldContext {
|
||||
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextAttribute:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
sqlbuilder "github.com/huandu/go-sqlbuilder"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestPrepareWhereClause_EmptyVariableList ensures PrepareWhereClause errors when a variable has an empty list value.
|
||||
@@ -591,10 +590,9 @@ func TestVisitKey(t *testing.T) {
|
||||
// and decides not-found handling. Replay that here against the generic
|
||||
// builder behavior (error unless the key is ignored). The test maps carry
|
||||
// no signal, so every logical field is single-member and flattens losslessly.
|
||||
matching := MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, telemetrytypes.SignalUnspecified, nil, key, tt.fieldKeys)
|
||||
matching := MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, key, tt.fieldKeys)
|
||||
resolved, warning := ResolveLogicalFields(key, matching)
|
||||
keys, err := SingleKeys(resolved)
|
||||
require.NoError(t, err)
|
||||
keys := SingleKeys(resolved)
|
||||
|
||||
var gotErrors []string
|
||||
var gotMainErrURL, gotMainWrnURL string
|
||||
@@ -758,7 +756,6 @@ func (b *resourceConditionBuilder) ConditionFor(
|
||||
_ uint64,
|
||||
_ uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
_ []*telemetrytypes.LogicalField,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
_ qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
@@ -771,11 +768,8 @@ func (b *resourceConditionBuilder) ConditionFor(
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
resolved, warning := ResolveLogicalFields(key, MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, telemetrytypes.SignalUnspecified, nil, key, fieldKeys))
|
||||
keys, err := SingleKeys(resolved)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
resolved, warning := ResolveLogicalFields(key, MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, key, fieldKeys))
|
||||
keys := SingleKeys(resolved)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
@@ -800,7 +794,6 @@ func (b *conditionBuilder) ConditionFor(
|
||||
_ uint64,
|
||||
_ uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
_ []*telemetrytypes.LogicalField,
|
||||
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
options qbtypes.ConditionBuilderOptions,
|
||||
operator qbtypes.FilterOperator,
|
||||
@@ -818,11 +811,8 @@ func (b *conditionBuilder) ConditionFor(
|
||||
return []string{fmt.Sprintf("%s_cond", key.Name)}, nil, nil
|
||||
}
|
||||
|
||||
resolved, warning := ResolveLogicalFields(key, MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, telemetrytypes.SignalUnspecified, nil, key, fieldKeys))
|
||||
keys, err := SingleKeys(resolved)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
resolved, warning := ResolveLogicalFields(key, MatchingLogicalFields(context.Background(), valuer.UUID{}, nil, key, fieldKeys))
|
||||
keys := SingleKeys(resolved)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
|
||||
@@ -2,37 +2,21 @@
|
||||
|
||||
package semconv
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
|
||||
var families = []Family{
|
||||
{
|
||||
current: "container.cpu.usage",
|
||||
kind: KindMetric,
|
||||
members: []Member{
|
||||
{name: "container.cpu.utilization"},
|
||||
},
|
||||
Current: "db.system.name",
|
||||
Old: []string{"db.system"},
|
||||
Kind: KindAttribute,
|
||||
Contexts: nil,
|
||||
Signals: nil,
|
||||
ApplyToMetrics: nil,
|
||||
},
|
||||
{
|
||||
current: "deployment.environment.name",
|
||||
kind: KindAttribute,
|
||||
members: []Member{
|
||||
{name: "deployment.environment"},
|
||||
},
|
||||
contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextResource},
|
||||
signals: []telemetrytypes.Signal{telemetrytypes.SignalLogs, telemetrytypes.SignalMetrics, telemetrytypes.SignalTraces},
|
||||
},
|
||||
{
|
||||
current: "k8s.node.cpu.usage",
|
||||
kind: KindMetric,
|
||||
members: []Member{
|
||||
{name: "k8s.node.cpu.utilization"},
|
||||
},
|
||||
},
|
||||
{
|
||||
current: "k8s.pod.cpu.usage",
|
||||
kind: KindMetric,
|
||||
members: []Member{
|
||||
{name: "k8s.pod.cpu.utilization"},
|
||||
},
|
||||
Current: "deployment.environment.name",
|
||||
Old: []string{"deployment.environment"},
|
||||
Kind: KindAttribute,
|
||||
Contexts: nil,
|
||||
Signals: nil,
|
||||
ApplyToMetrics: nil,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package semconv
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
@@ -15,47 +14,16 @@ type Kind struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
// Member is one historical spelling of a family, with the scope its rename
|
||||
// edges declared. A nil axis places no constraint on that axis.
|
||||
type Member struct {
|
||||
name string
|
||||
contexts []telemetrytypes.FieldContext
|
||||
signals []telemetrytypes.Signal
|
||||
applyToMetrics []string
|
||||
}
|
||||
|
||||
func (m Member) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
// Family is one logical telemetry field. Members are ordered from the most
|
||||
// recent predecessor to the oldest one and therefore also define fallback
|
||||
// order. The family-level contexts and signals come from the overlay and gate
|
||||
// where the family may resolve at all; member scopes come from the schema
|
||||
// edges and gate which members apply for a given selector.
|
||||
// Family is one logical telemetry field. Old is ordered from the most recent
|
||||
// predecessor to the oldest one and therefore also defines fallback order.
|
||||
type Family struct {
|
||||
current string
|
||||
kind Kind
|
||||
members []Member
|
||||
contexts []telemetrytypes.FieldContext
|
||||
signals []telemetrytypes.Signal
|
||||
}
|
||||
|
||||
func (f Family) Current() string {
|
||||
return f.current
|
||||
}
|
||||
|
||||
func (f Family) Kind() Kind {
|
||||
return f.kind
|
||||
}
|
||||
|
||||
// Old returns the historical spellings in fallback order.
|
||||
func (f Family) Old() []string {
|
||||
names := make([]string, len(f.members))
|
||||
for i, member := range f.members {
|
||||
names[i] = member.name
|
||||
}
|
||||
return names
|
||||
Current string
|
||||
Old []string
|
||||
Kind Kind
|
||||
Contexts []telemetrytypes.FieldContext
|
||||
Signals []telemetrytypes.Signal
|
||||
ApplyToMetrics []string
|
||||
ValueMap map[string]string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -70,150 +38,90 @@ func (Kind) Enum() []any {
|
||||
return []any{KindAttribute, KindMetric}
|
||||
}
|
||||
|
||||
// Members returns the current name first, followed by the historical spellings
|
||||
// admitted for the selector, in fallback order. A name outside an enabled
|
||||
// family — or one the selector leaves ambiguous — is returned unchanged. The
|
||||
// Lookup returns the enabled family containing selector.Name for kind. The
|
||||
// returned family must not be modified.
|
||||
func Lookup(kind Kind, selector telemetrytypes.FieldKeySelector) (Family, bool) {
|
||||
idx, ok := lookupIndex(kind, selector)
|
||||
if !ok {
|
||||
return Family{}, false
|
||||
}
|
||||
return families[idx], true
|
||||
}
|
||||
|
||||
// Members returns the current name first, followed by historical names in
|
||||
// fallback order. A name outside an enabled family is returned unchanged. The
|
||||
// returned slice must not be modified.
|
||||
func Members(kind Kind, selector telemetrytypes.FieldKeySelector) []string {
|
||||
idx, ok := lookupIndex(kind, selector)
|
||||
if !ok {
|
||||
return []string{selector.Name}
|
||||
}
|
||||
return admittedMembers(idx, selector)
|
||||
}
|
||||
|
||||
func admittedMembers(idx int, selector telemetrytypes.FieldKeySelector) []string {
|
||||
admitted := 0
|
||||
for _, member := range families[idx].members {
|
||||
if memberAdmits(member, selector) {
|
||||
admitted++
|
||||
}
|
||||
}
|
||||
if admitted == len(families[idx].members) {
|
||||
return familyMembers[idx]
|
||||
}
|
||||
names := make([]string, 0, admitted+1)
|
||||
names = append(names, families[idx].current)
|
||||
for _, member := range families[idx].members {
|
||||
if memberAdmits(member, selector) {
|
||||
names = append(names, member.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
return familyMembers[idx]
|
||||
}
|
||||
|
||||
// Current returns the current name for selector.Name, or the input name when
|
||||
// it does not resolve to a family.
|
||||
// it does not belong to an enabled family.
|
||||
func Current(kind Kind, selector telemetrytypes.FieldKeySelector) string {
|
||||
idx, ok := lookupIndex(kind, selector)
|
||||
if !ok {
|
||||
return selector.Name
|
||||
}
|
||||
return families[idx].current
|
||||
return families[idx].Current
|
||||
}
|
||||
|
||||
// All iterates over every enabled family.
|
||||
func All() iter.Seq[Family] {
|
||||
return func(yield func(Family) bool) {
|
||||
for _, family := range families {
|
||||
if !yield(family) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// All returns every enabled family. The returned slice and families must not be
|
||||
// modified.
|
||||
func All() []Family {
|
||||
return families
|
||||
}
|
||||
|
||||
func buildIndexes() (map[string][]int, [][]string) {
|
||||
index := make(map[string][]int)
|
||||
members := make([][]string, len(families))
|
||||
add := func(name string, i int) {
|
||||
if !slices.Contains(index[name], i) {
|
||||
index[name] = append(index[name], i)
|
||||
}
|
||||
}
|
||||
for i, family := range families {
|
||||
members[i] = make([]string, 0, len(family.members)+1)
|
||||
members[i] = append(members[i], family.current)
|
||||
add(family.current, i)
|
||||
for _, member := range family.members {
|
||||
members[i] = append(members[i], member.name)
|
||||
add(member.name, i)
|
||||
members[i] = make([]string, 0, len(family.Old)+1)
|
||||
members[i] = append(members[i], family.Current)
|
||||
members[i] = append(members[i], family.Old...)
|
||||
index[family.Current] = append(index[family.Current], i)
|
||||
for _, old := range family.Old {
|
||||
index[old] = append(index[old], i)
|
||||
}
|
||||
}
|
||||
return index, members
|
||||
}
|
||||
|
||||
// lookupIndex returns the family that resolves selector.Name for kind.
|
||||
//
|
||||
// The missing-information policy is the same on every axis (signal, field
|
||||
// context, metric name): an axis the selector does not populate is a wildcard
|
||||
// and constrains nothing. When the wildcards leave more than one family
|
||||
// admitted, the name does not resolve — resolution never picks an arbitrary
|
||||
// winner, it asks for more information by staying literal.
|
||||
func lookupIndex(kind Kind, selector telemetrytypes.FieldKeySelector) (int, bool) {
|
||||
found, foundIdx := 0, 0
|
||||
for _, idx := range memberToFamilies[selector.Name] {
|
||||
if familyAdmits(families[idx], kind, selector) {
|
||||
found++
|
||||
foundIdx = idx
|
||||
if matchesSelector(families[idx], kind, selector) {
|
||||
return idx, true
|
||||
}
|
||||
}
|
||||
if found != 1 {
|
||||
return 0, false
|
||||
}
|
||||
return foundIdx, true
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// familyAdmits reports whether the family resolves selector.Name: the
|
||||
// family-level gate must admit the selector, and the name must be the current
|
||||
// name or an admitted member.
|
||||
func familyAdmits(family Family, kind Kind, selector telemetrytypes.FieldKeySelector) bool {
|
||||
if family.kind != kind {
|
||||
func matchesSelector(family Family, kind Kind, selector telemetrytypes.FieldKeySelector) bool {
|
||||
if family.Kind != kind {
|
||||
return false
|
||||
}
|
||||
if !axisAdmits(family.signals, selector.Signal, telemetrytypes.SignalUnspecified) {
|
||||
return false
|
||||
}
|
||||
if !axisAdmits(family.contexts, selector.FieldContext, telemetrytypes.FieldContextUnspecified) {
|
||||
return false
|
||||
}
|
||||
if selector.Name == family.current {
|
||||
for _, member := range family.members {
|
||||
if memberAdmits(member, selector) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, member := range family.members {
|
||||
if member.name == selector.Name && memberAdmits(member, selector) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// memberAdmits reports whether the member applies for the selector under the
|
||||
// wildcard policy: a selector axis without a value never constrains, and a
|
||||
// member axis without a value admits every selector value.
|
||||
func memberAdmits(member Member, selector telemetrytypes.FieldKeySelector) bool {
|
||||
if !axisAdmits(member.signals, selector.Signal, telemetrytypes.SignalUnspecified) {
|
||||
return false
|
||||
if selector.Signal != telemetrytypes.SignalUnspecified && len(family.Signals) > 0 {
|
||||
if !slices.Contains(family.Signals, selector.Signal) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !axisAdmits(member.contexts, selector.FieldContext, telemetrytypes.FieldContextUnspecified) {
|
||||
return false
|
||||
|
||||
if selector.FieldContext != telemetrytypes.FieldContextUnspecified && len(family.Contexts) > 0 {
|
||||
if !slices.Contains(family.Contexts, selector.FieldContext) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(member.applyToMetrics) > 0 &&
|
||||
selector.MetricContext != nil && selector.MetricContext.MetricName != "" &&
|
||||
!slices.Contains(member.applyToMetrics, selector.MetricContext.MetricName) {
|
||||
return false
|
||||
|
||||
if selector.Signal == telemetrytypes.SignalMetrics && len(family.ApplyToMetrics) > 0 {
|
||||
if selector.MetricContext == nil {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(family.ApplyToMetrics, selector.MetricContext.MetricName)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func axisAdmits[T comparable](scope []T, value T, unspecified T) bool {
|
||||
if len(scope) == 0 || value == unspecified {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(scope, value)
|
||||
}
|
||||
|
||||
@@ -78,116 +78,3 @@ func TestMembersReturnsInputWhenKindDoesNotMatch(t *testing.T) {
|
||||
"an attribute family must not match a metric-name lookup",
|
||||
)
|
||||
}
|
||||
|
||||
func TestFamilySignalsGateResolution(t *testing.T) {
|
||||
swapFamilies(t, []Family{{
|
||||
current: "gated.current",
|
||||
kind: KindAttribute,
|
||||
members: []Member{{name: "gated.old"}},
|
||||
signals: []telemetrytypes.Signal{telemetrytypes.SignalLogs, telemetrytypes.SignalTraces},
|
||||
}})
|
||||
metrics := telemetrytypes.FieldKeySelector{Name: "gated.old", Signal: telemetrytypes.SignalMetrics}
|
||||
logs := telemetrytypes.FieldKeySelector{Name: "gated.old", Signal: telemetrytypes.SignalLogs}
|
||||
|
||||
assert.Equal(t, []string{"gated.old"}, Members(KindAttribute, metrics),
|
||||
"a family gated to traces and logs must stay literal for metrics")
|
||||
assert.Equal(t, []string{"gated.current", "gated.old"}, Members(KindAttribute, logs),
|
||||
"the gate admits the signals it lists")
|
||||
}
|
||||
|
||||
func TestMetricNameFamilyResolves(t *testing.T) {
|
||||
selector := telemetrytypes.FieldKeySelector{Name: "k8s.pod.cpu.utilization", Signal: telemetrytypes.SignalMetrics}
|
||||
|
||||
assert.Equal(t, []string{"k8s.pod.cpu.usage", "k8s.pod.cpu.utilization"}, Members(KindMetric, selector))
|
||||
assert.Equal(t, "k8s.pod.cpu.usage", Current(KindMetric, selector))
|
||||
assert.Equal(t, []string{"k8s.pod.cpu.utilization"}, Members(KindAttribute, selector),
|
||||
"a metric-name family must not match an attribute lookup")
|
||||
}
|
||||
|
||||
func TestMembersReturnsSharedSliceForUnscopedFamily(t *testing.T) {
|
||||
selector := telemetrytypes.FieldKeySelector{Name: "deployment.environment", Signal: telemetrytypes.SignalTraces}
|
||||
|
||||
first := Members(KindAttribute, selector)
|
||||
second := Members(KindAttribute, selector)
|
||||
assert.Equal(t, &first[0], &second[0],
|
||||
"a family whose members all admit must return the precomputed slice, not a copy")
|
||||
}
|
||||
|
||||
func TestAllIteratesEnabledFamilies(t *testing.T) {
|
||||
currents := []string{}
|
||||
for family := range All() {
|
||||
currents = append(currents, family.Current())
|
||||
}
|
||||
assert.Contains(t, currents, "deployment.environment.name")
|
||||
assert.Contains(t, currents, "k8s.pod.cpu.usage")
|
||||
}
|
||||
|
||||
// swapFamilies replaces the generated table for one test so scoped-member and
|
||||
// fan-out behavior can be pinned without enabling such families for real.
|
||||
func swapFamilies(t *testing.T, replacement []Family) {
|
||||
t.Helper()
|
||||
prevFamilies, prevIndex, prevMembers := families, memberToFamilies, familyMembers
|
||||
families = replacement
|
||||
memberToFamilies, familyMembers = buildIndexes()
|
||||
t.Cleanup(func() {
|
||||
families, memberToFamilies, familyMembers = prevFamilies, prevIndex, prevMembers
|
||||
})
|
||||
}
|
||||
|
||||
func TestFanOutResolvesOnlyWithEnoughInformation(t *testing.T) {
|
||||
swapFamilies(t, []Family{
|
||||
{
|
||||
current: "cpu.mode",
|
||||
kind: KindAttribute,
|
||||
members: []Member{{name: "state", applyToMetrics: []string{"system.cpu.time"}}},
|
||||
},
|
||||
{
|
||||
current: "db.client.connection.state",
|
||||
kind: KindAttribute,
|
||||
members: []Member{{name: "state", applyToMetrics: []string{"db.client.connections.usage"}}},
|
||||
},
|
||||
})
|
||||
|
||||
ambiguous := telemetrytypes.FieldKeySelector{Name: "state", Signal: telemetrytypes.SignalMetrics}
|
||||
assert.Equal(t, []string{"state"}, Members(KindAttribute, ambiguous),
|
||||
"without a metric name, a fanned-out member admits several families and must stay literal")
|
||||
|
||||
pinned := ambiguous
|
||||
pinned.MetricContext = &telemetrytypes.MetricContext{MetricName: "system.cpu.time"}
|
||||
assert.Equal(t, []string{"cpu.mode", "state"}, Members(KindAttribute, pinned),
|
||||
"the metric name disambiguates the fan-out")
|
||||
|
||||
outside := ambiguous
|
||||
outside.MetricContext = &telemetrytypes.MetricContext{MetricName: "http.server.duration"}
|
||||
assert.Equal(t, []string{"state"}, Members(KindAttribute, outside),
|
||||
"a metric outside every apply_to_metrics list resolves no family")
|
||||
}
|
||||
|
||||
func TestMemberScopesFilterMembers(t *testing.T) {
|
||||
swapFamilies(t, []Family{{
|
||||
current: "user_agent.original",
|
||||
kind: KindAttribute,
|
||||
members: []Member{
|
||||
{name: "http.user_agent", contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextAttribute}, signals: []telemetrytypes.Signal{telemetrytypes.SignalTraces}},
|
||||
{name: "browser.user_agent", contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextResource}},
|
||||
},
|
||||
}})
|
||||
|
||||
resource := telemetrytypes.FieldKeySelector{
|
||||
Name: "user_agent.original",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
}
|
||||
assert.Equal(t, []string{"user_agent.original", "browser.user_agent"}, Members(KindAttribute, resource),
|
||||
"a strict resource lookup must not include the span-only member")
|
||||
|
||||
attribute := resource
|
||||
attribute.FieldContext = telemetrytypes.FieldContextAttribute
|
||||
assert.Equal(t, []string{"user_agent.original", "http.user_agent"}, Members(KindAttribute, attribute),
|
||||
"a strict attribute lookup must not include the resource-only member")
|
||||
|
||||
strictResourceOldSpan := resource
|
||||
strictResourceOldSpan.Name = "http.user_agent"
|
||||
assert.Equal(t, []string{"http.user_agent"}, Members(KindAttribute, strictResourceOldSpan),
|
||||
"an old spelling outside its own scope stays literal")
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ type Handlers struct {
|
||||
AIObservability aiobservability.Handler
|
||||
AuthzHandler authz.Handler
|
||||
ZeusHandler zeus.Handler
|
||||
LicensingHandler licensing.Handler
|
||||
QuerierHandler querier.Handler
|
||||
ServiceAccountHandler serviceaccount.Handler
|
||||
RegistryHandler factory.Handler
|
||||
@@ -95,7 +96,7 @@ func NewHandlers(
|
||||
providerSettings factory.ProviderSettings,
|
||||
analytics analytics.Analytics,
|
||||
querierHandler querier.Handler,
|
||||
licensing licensing.Licensing,
|
||||
licensingService licensing.Licensing,
|
||||
global global.Global,
|
||||
flaggerService flagger.Flagger,
|
||||
gatewayService gateway.Gateway,
|
||||
@@ -125,7 +126,8 @@ func NewHandlers(
|
||||
Fields: implfields.NewHandler(providerSettings, telemetryMetadataStore),
|
||||
AIObservability: implaiobservability.NewHandler(telemetryMetadataStore),
|
||||
AuthzHandler: signozauthzapi.NewHandler(authz),
|
||||
ZeusHandler: zeus.NewHandler(zeusService, licensing),
|
||||
ZeusHandler: zeus.NewHandler(zeusService, licensingService),
|
||||
LicensingHandler: licensing.NewHandler(licensingService),
|
||||
QuerierHandler: querierHandler,
|
||||
ServiceAccountHandler: implserviceaccount.NewHandler(modules.ServiceAccount, modules.ServiceAccountGetter),
|
||||
RegistryHandler: registryHandler,
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/aiobservability"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
@@ -80,6 +81,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ authz.Handler }{},
|
||||
struct{ rawdataexport.Handler }{},
|
||||
struct{ zeus.Handler }{},
|
||||
struct{ licensing.Handler }{},
|
||||
struct{ querier.Handler }{},
|
||||
struct{ serviceaccount.Handler }{},
|
||||
struct{ serviceaccount.Getter }{},
|
||||
|
||||
@@ -244,7 +244,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewDeleteOrphanUserRolesFactory(),
|
||||
sqlmigration.NewMigrateLambdaDashboardsFactory(),
|
||||
sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddDeploymentHostTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddLicenseTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -335,6 +335,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.AuthzHandler,
|
||||
handlers.RawDataExport,
|
||||
handlers.ZeusHandler,
|
||||
handlers.LicensingHandler,
|
||||
handlers.QuerierHandler,
|
||||
handlers.ServiceAccountHandler,
|
||||
modules.ServiceAccountGetter,
|
||||
|
||||
@@ -16,21 +16,21 @@ import (
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addDeploymentHostTuples struct {
|
||||
type addLicenseTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddDeploymentHostTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_deployment_host_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addDeploymentHostTuples{sqlstore: sqlstore}, nil
|
||||
func NewAddLicenseTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_license_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addLicenseTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addDeploymentHostTuples) Register(migrations *migrate.Migrations) error {
|
||||
func (migration *addLicenseTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addDeploymentHostTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
func (migration *addLicenseTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,14 +54,12 @@ func (migration *addDeploymentHostTuples) Up(ctx context.Context, db *bun.DB) er
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
// zeus hosts moved from the legacy ViewAccess/AdminAccess role gates to
|
||||
// CheckResources, which on enterprise requires real tuples -- existing orgs
|
||||
// never had these written, only new orgs get them from the registry at bootstrap.
|
||||
tuples := []migrationTuple{
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "deployment-host", "list"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "deployment-host", "update"},
|
||||
{authtypes.SigNozEditorRoleName, "metaresource", "deployment-host", "list"},
|
||||
{authtypes.SigNozViewerRoleName, "metaresource", "deployment-host", "list"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "license", "create"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "license", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "license", "update"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "license", "delete"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "license", "list"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
@@ -156,6 +154,6 @@ func (migration *addDeploymentHostTuples) Up(ctx context.Context, db *bun.DB) er
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addDeploymentHostTuples) Down(context.Context, *bun.DB) error {
|
||||
func (migration *addLicenseTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -994,8 +994,8 @@ func TestBuild_TraceList_MultiVariantGateKey(t *testing.T) {
|
||||
assert.Contains(t, got, "mapContains(attributes_string, 'gen_ai.tool.name') OR mapContains(attributes_number, 'gen_ai.tool.name')")
|
||||
}
|
||||
|
||||
// A `trace.`-prefixed aggregate in the filter box and the same condition in the
|
||||
// explicit Having box build the same query; output-only aggregates are rejected.
|
||||
// `trace.` marks a trace-level aggregate; `tracefield.` routes trace-level too but is
|
||||
// not a rewritable alias, so the HAVING rewriter rejects it.
|
||||
func TestBuild_TraceList_TraceContextPrefix(t *testing.T) {
|
||||
b := newTestBuilder(t)
|
||||
build := func(q qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) (*qbtypes.Statement, error) {
|
||||
@@ -1003,14 +1003,19 @@ func TestBuild_TraceList_TraceContextPrefix(t *testing.T) {
|
||||
return b.Build(context.Background(), valuer.UUID{}, testStartMs, testEndMs, qbtypes.RequestTypeTrace, q, nil)
|
||||
}
|
||||
|
||||
viaTrace, err := build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
_, err := build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Filter: &qbtypes.Filter{Expression: "trace.output_tokens > 1000"}})
|
||||
require.NoError(t, err)
|
||||
|
||||
viaHaving, err := build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Having: &qbtypes.Having{Expression: "trace.output_tokens > 1000"}})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, viaTrace.Query, viaHaving.Query)
|
||||
_, err = build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Filter: &qbtypes.Filter{Expression: "tracefield.output_tokens > 1000"}})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Invalid references in `Having` expression: [tracefield.output_tokens]")
|
||||
|
||||
_, err = build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Having: &qbtypes.Having{Expression: "tracefield.output_tokens > 1000"}})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Invalid references in `Having` expression: [tracefield.output_tokens]")
|
||||
|
||||
_, err = build(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Filter: &qbtypes.Filter{Expression: "trace.span_count > 3"}})
|
||||
@@ -1018,8 +1023,7 @@ func TestBuild_TraceList_TraceContextPrefix(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "cannot be used")
|
||||
}
|
||||
|
||||
// Query variables in a trace-level condition resolve like span filters: bound args,
|
||||
// list/IN handling, dynamic __all__ dropping the condition.
|
||||
// Query variables in a trace-level condition are substituted into the HAVING.
|
||||
func TestBuild_TraceList_VariableInAggregateFilter(t *testing.T) {
|
||||
b := newTestBuilder(t)
|
||||
build := func(expr string, vars map[string]qbtypes.VariableItem) (*qbtypes.Statement, error) {
|
||||
@@ -1031,18 +1035,17 @@ func TestBuild_TraceList_VariableInAggregateFilter(t *testing.T) {
|
||||
}, vars)
|
||||
}
|
||||
|
||||
// scalar variable -> bound arg via the filter pipeline
|
||||
// scalar variable -> literal in HAVING
|
||||
stmt, err := build("trace.output_tokens > $threshold",
|
||||
map[string]qbtypes.VariableItem{"threshold": {Value: 700}})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, stmt.Query, "HAVING output_tokens > ?")
|
||||
assert.Contains(t, stmt.Args, float64(700))
|
||||
assert.Contains(t, stmt.Query, "HAVING output_tokens > 700")
|
||||
|
||||
// list variable with IN
|
||||
stmt, err = build("trace.llm_call_count IN $counts",
|
||||
map[string]qbtypes.VariableItem{"counts": {Value: []any{1, 2}}})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, stmt.Query, "HAVING llm_call_count IN (?, ?)")
|
||||
assert.Contains(t, stmt.Query, "HAVING llm_call_count IN")
|
||||
|
||||
// dynamic __all__ -> condition dropped, no HAVING at all
|
||||
stmt, err = build("trace.output_tokens > $threshold",
|
||||
@@ -1050,7 +1053,7 @@ func TestBuild_TraceList_VariableInAggregateFilter(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, stmt.Query, "HAVING")
|
||||
|
||||
// unresolved variable -> rejected, though only as an unknown aggregate today
|
||||
// unresolved variable -> rejected, not compared as a literal
|
||||
_, err = build("trace.output_tokens > $missing", map[string]qbtypes.VariableItem{"other": {Value: 1}})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user