Compare commits

..

23 Commits

Author SHA1 Message Date
Nikhil Soni
2a3258e716 refactor: breakdown GetSelectedSpans for readability 2026-04-14 20:12:48 +05:30
Nikhil Soni
7da42c439a refactor: separate out store calls and computations 2026-04-14 19:29:49 +05:30
Nikhil Soni
864fd3785c refactor: move error to types as well 2026-04-14 19:09:55 +05:30
Nikhil Soni
fc684e3b5d refactor: extract ClickHouse queries into a store abstraction
Move GetTraceSummary and GetTraceSpans out of module.go into a
traceStore interface backed by clickhouseTraceStore in store.go.
The module struct now holds a traceStore instead of a raw
telemetrystore.TelemetryStore, keeping DB access separate from
business logic.
2026-04-14 19:06:25 +05:30
Nikhil Soni
7fb126596e refactor: move type creation and constants to types package
- Move DB/table/cache/windowing constants to tracedetailtypes package
- Add NewWaterfallTrace and NewWaterfallResponse constructors in types
- Use constructors in module.go instead of inline struct literals
- Reorder waterfall.go so public functions precede private ones
2026-04-14 19:06:19 +05:30
Nikhil Soni
9e5623e83e chore: avoid returning nil, nil 2026-04-14 16:35:26 +05:30
Nikhil Soni
94e4d8ea38 refactor: break down GetWaterfall method for readability 2026-04-14 16:23:00 +05:30
Nikhil Soni
c08840a827 fix: update openapi specs 2026-04-14 10:50:36 +05:30
Nikhil Soni
a3e7bb90b0 fix: use int16 for status code as per db schema 2026-04-14 10:50:36 +05:30
Nikhil Soni
8515d2f37c fix: remove timeout since waterfall take longer 2026-04-14 10:50:36 +05:30
Nikhil Soni
07c05ac3a6 chore: generate openapi spec for v3 waterfall 2026-04-14 10:50:36 +05:30
Nikhil Soni
6289f59ba3 fix: use typed paramter field in logs 2026-04-14 10:50:36 +05:30
Nikhil Soni
76371c9fa2 fix: add timeout to module context 2026-04-14 10:50:36 +05:30
Nikhil Soni
f082e396eb fix: rename timestamp to milli for readability 2026-04-14 10:50:36 +05:30
Nikhil Soni
840eb8f228 fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-14 10:50:36 +05:30
Nikhil Soni
2911baf6bb chore: avoid sorting on every traversal 2026-04-14 10:50:36 +05:30
Nikhil Soni
fc5be4eeb5 chore: add same test cases as for old waterfall api 2026-04-14 10:50:36 +05:30
Nikhil Soni
a1b92c79a4 refactor: convert waterfall api to modules format 2026-04-14 10:50:31 +05:30
Nikhil Soni
7a0acd5c8b fix: remove unused fields and rename span type
To avoid confusing with otel span
2026-04-14 10:49:58 +05:30
Nikhil Soni
069cbe2c6f fix: update span.attributes to map of string to any
To support otel format of diffrent types of attributes
2026-04-14 10:49:58 +05:30
Nikhil Soni
4c821f9721 chore: add reason for using snake case in response 2026-04-14 10:49:58 +05:30
Nikhil Soni
4eccea92db refactor: move type conversion logic to types pkg 2026-04-14 10:49:58 +05:30
Nikhil Soni
c8d8966a5d feat: setup types and interface for waterfall v3
v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service
2026-04-14 10:49:58 +05:30
44 changed files with 8726 additions and 6854 deletions

View File

@@ -2787,6 +2787,146 @@ components:
type: string
type: array
type: object
TracedetailtypesEvent:
properties:
attributeMap:
additionalProperties: {}
type: object
isError:
type: boolean
name:
type: string
timeUnixNano:
minimum: 0
type: integer
type: object
TracedetailtypesWaterfallRequest:
properties:
limit:
minimum: 0
type: integer
selectedSpanId:
type: string
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
TracedetailtypesWaterfallResponse:
properties:
endTimestampMillis:
minimum: 0
type: integer
hasMissingSpans:
type: boolean
hasMore:
type: boolean
rootServiceEntryPoint:
type: string
rootServiceName:
type: string
serviceNameToTotalDurationMap:
additionalProperties:
minimum: 0
type: integer
nullable: true
type: object
spans:
items:
$ref: '#/components/schemas/TracedetailtypesWaterfallSpan'
nullable: true
type: array
startTimestampMillis:
minimum: 0
type: integer
totalErrorSpansCount:
minimum: 0
type: integer
totalSpansCount:
minimum: 0
type: integer
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
TracedetailtypesWaterfallSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
db_name:
type: string
db_operation:
type: string
duration_nano:
minimum: 0
type: integer
events:
items:
$ref: '#/components/schemas/TracedetailtypesEvent'
nullable: true
type: array
external_http_method:
type: string
external_http_url:
type: string
flags:
minimum: 0
type: integer
has_children:
type: boolean
has_error:
type: boolean
http_host:
type: string
http_method:
type: string
http_url:
type: string
is_remote:
type: string
kind:
format: int32
type: integer
kind_string:
type: string
level:
minimum: 0
type: integer
name:
type: string
parent_span_id:
type: string
resource:
additionalProperties:
type: string
nullable: true
type: object
response_status_code:
type: string
span_id:
type: string
status_code:
type: integer
status_code_string:
type: string
status_message:
type: string
sub_tree_node_count:
minimum: 0
type: integer
timestamp:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
type: string
type: object
TypesChangePasswordRequest:
properties:
newPassword:
@@ -9514,6 +9654,76 @@ paths:
summary: Put profile in Zeus for a deployment.
tags:
- zeus
/api/v3/traces/{traceID}/waterfall:
post:
deprecated: false
description: Returns the waterfall view of spans for a given trace ID with tree
structure, metadata, and windowed pagination
operationId: GetWaterfall
parameters:
- in: path
name: traceID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TracedetailtypesWaterfallRequest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TracedetailtypesWaterfallResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get waterfall view for a trace
tags:
- tracedetail
/api/v5/query_range:
post:
deprecated: false

View File

@@ -3196,6 +3196,248 @@ export interface TelemetrytypesTelemetryFieldValuesDTO {
stringValues?: string[];
}
export type TracedetailtypesEventDTOAttributeMap = { [key: string]: unknown };
export interface TracedetailtypesEventDTO {
/**
* @type object
*/
attributeMap?: TracedetailtypesEventDTOAttributeMap;
/**
* @type boolean
*/
isError?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type integer
* @minimum 0
*/
timeUnixNano?: number;
}
export interface TracedetailtypesWaterfallRequestDTO {
/**
* @type integer
* @minimum 0
*/
limit?: number;
/**
* @type string
*/
selectedSpanId?: string;
/**
* @type array
* @nullable true
*/
uncollapsedSpans?: string[] | null;
}
/**
* @nullable
*/
export type TracedetailtypesWaterfallResponseDTOServiceNameToTotalDurationMap = {
[key: string]: number;
} | null;
export interface TracedetailtypesWaterfallResponseDTO {
/**
* @type integer
* @minimum 0
*/
endTimestampMillis?: number;
/**
* @type boolean
*/
hasMissingSpans?: boolean;
/**
* @type boolean
*/
hasMore?: boolean;
/**
* @type string
*/
rootServiceEntryPoint?: string;
/**
* @type string
*/
rootServiceName?: string;
/**
* @type object
* @nullable true
*/
serviceNameToTotalDurationMap?: TracedetailtypesWaterfallResponseDTOServiceNameToTotalDurationMap;
/**
* @type array
* @nullable true
*/
spans?: TracedetailtypesWaterfallSpanDTO[] | null;
/**
* @type integer
* @minimum 0
*/
startTimestampMillis?: number;
/**
* @type integer
* @minimum 0
*/
totalErrorSpansCount?: number;
/**
* @type integer
* @minimum 0
*/
totalSpansCount?: number;
/**
* @type array
* @nullable true
*/
uncollapsedSpans?: string[] | null;
}
/**
* @nullable
*/
export type TracedetailtypesWaterfallSpanDTOAttributes = {
[key: string]: unknown;
} | null;
/**
* @nullable
*/
export type TracedetailtypesWaterfallSpanDTOResource = {
[key: string]: string;
} | null;
export interface TracedetailtypesWaterfallSpanDTO {
/**
* @type object
* @nullable true
*/
attributes?: TracedetailtypesWaterfallSpanDTOAttributes;
/**
* @type string
*/
db_name?: string;
/**
* @type string
*/
db_operation?: string;
/**
* @type integer
* @minimum 0
*/
duration_nano?: number;
/**
* @type array
* @nullable true
*/
events?: TracedetailtypesEventDTO[] | null;
/**
* @type string
*/
external_http_method?: string;
/**
* @type string
*/
external_http_url?: string;
/**
* @type integer
* @minimum 0
*/
flags?: number;
/**
* @type boolean
*/
has_children?: boolean;
/**
* @type boolean
*/
has_error?: boolean;
/**
* @type string
*/
http_host?: string;
/**
* @type string
*/
http_method?: string;
/**
* @type string
*/
http_url?: string;
/**
* @type string
*/
is_remote?: string;
/**
* @type integer
* @format int32
*/
kind?: number;
/**
* @type string
*/
kind_string?: string;
/**
* @type integer
* @minimum 0
*/
level?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
parent_span_id?: string;
/**
* @type object
* @nullable true
*/
resource?: TracedetailtypesWaterfallSpanDTOResource;
/**
* @type string
*/
response_status_code?: string;
/**
* @type string
*/
span_id?: string;
/**
* @type integer
*/
status_code?: number;
/**
* @type string
*/
status_code_string?: string;
/**
* @type string
*/
status_message?: string;
/**
* @type integer
* @minimum 0
*/
sub_tree_node_count?: number;
/**
* @type integer
* @minimum 0
*/
timestamp?: number;
/**
* @type string
*/
trace_id?: string;
/**
* @type string
*/
trace_state?: string;
}
export interface TypesChangePasswordRequestDTO {
/**
* @type string
@@ -4873,6 +5115,17 @@ export type GetHosts200 = {
status: string;
};
export type GetWaterfallPathParameters = {
traceID: string;
};
export type GetWaterfall200 = {
data: TracedetailtypesWaterfallResponseDTO;
/**
* @type string
*/
status: string;
};
export type QueryRangeV5200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**

View File

@@ -0,0 +1,121 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
MutationFunction,
UseMutationOptions,
UseMutationResult,
} from 'react-query';
import { useMutation } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
GetWaterfall200,
GetWaterfallPathParameters,
RenderErrorResponseDTO,
TracedetailtypesWaterfallRequestDTO,
} from '../sigNoz.schemas';
/**
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
* @summary Get waterfall view for a trace
*/
export const getWaterfall = (
{ traceID }: GetWaterfallPathParameters,
tracedetailtypesWaterfallRequestDTO: BodyType<TracedetailtypesWaterfallRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetWaterfall200>({
url: `/api/v3/traces/${traceID}/waterfall`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: tracedetailtypesWaterfallRequestDTO,
signal,
});
};
export const getGetWaterfallMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
},
TContext
> => {
const mutationKey = ['getWaterfall'];
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 getWaterfall>>,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getWaterfall(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetWaterfallMutationResult = NonNullable<
Awaited<ReturnType<typeof getWaterfall>>
>;
export type GetWaterfallMutationBody = BodyType<TracedetailtypesWaterfallRequestDTO>;
export type GetWaterfallMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get waterfall view for a trace
*/
export const useGetWaterfall = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
},
TContext
> => {
const mutationOptions = getGetWaterfallMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -1,4 +1,4 @@
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query';
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
function ChQuerySection(): JSX.Element {

View File

@@ -143,8 +143,8 @@
background-color: #4568dc;
content: '';
display: inline-block;
background: url('../../../assets/Icons/status-inprogress-pill.svg')
no-repeat center;
background: url('/public/Icons/status-inprogress-pill.svg') no-repeat
center;
background-size: contain;
border: none !important;
@@ -160,8 +160,7 @@
background-color: var(--bg-amber-400);
content: '';
display: inline-block;
background: url('../../../assets/Icons/status-skipped-pill.svg') no-repeat
center;
background: url('/public/Icons/status-skipped-pill.svg') no-repeat center;
background-size: contain;
border: none;

View File

@@ -1,7 +0,0 @@
.info-banner-wrapper {
margin: 8px 8px 16px 16px;
a {
text-decoration: underline;
}
}

View File

@@ -1,14 +1,10 @@
import { PlusOutlined } from '@ant-design/icons';
import { Callout } from '@signozhq/ui';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { EQueryType } from 'types/common/dashboard';
import DOCLINKS from 'utils/docLinks';
import { QueryButton } from '../../styles';
import ClickHouseQueryBuilder from './query';
import './ClickHouse.styles.scss';
function ClickHouseQueryContainer(): JSX.Element | null {
const { currentQuery, addNewQueryItem } = useQueryBuilder();
const addQueryHandler = (): void => {
@@ -17,28 +13,6 @@ function ClickHouseQueryContainer(): JSX.Element | null {
return (
<>
<div className="info-banner-wrapper">
<Callout
type="info"
showIcon
title={
<span>
<a
href={DOCLINKS.QUERY_CLICKHOUSE_TRACES}
target="_blank"
rel="noreferrer"
>
Learn to write faster, optimized queries
</a>
{' · Using AI? '}
<a href={DOCLINKS.AGENT_SKILL_INSTALL} target="_blank" rel="noreferrer">
Install the SigNoz ClickHouse query agent skill
</a>
</span>
}
/>
</div>
{currentQuery.clickhouse_sql.map((q, idx) => (
<ClickHouseQueryBuilder
key={q.name}

View File

@@ -19,7 +19,7 @@ import { Atom, Terminal } from 'lucide-react';
import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import ClickHouseQueryContainer from './QueryBuilder/ClickHouse';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
import './QuerySection.styles.scss';

View File

@@ -23,11 +23,9 @@ import { CheckIcon, Goal, UserPlus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { isModifierKeyPressed } from 'utils/app';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
import InviteTeamMembers from '../InviteTeamMembers/InviteTeamMembers';
import onboardingConfigWithLinks from '../onboarding-configs/onboarding-config-with-links';
import onboardingConfigWithLinks from '../onboarding-configs/onboarding-config-with-links.json';
import '../OnboardingV2.styles.scss';
@@ -896,7 +894,7 @@ function OnboardingAddDataSource(): JSX.Element {
>
{option.imgUrl && (
<img
src={option.imgUrl || signozBrandLogoUrl}
src={option.imgUrl || '/Logos/signoz-brand-logo-new.svg'}
alt={option.label}
className="onboarding-data-source-button-img"
/>
@@ -959,7 +957,7 @@ function OnboardingAddDataSource(): JSX.Element {
}
>
<img
src={option.imgUrl || signozBrandLogoUrl}
src={option.imgUrl || '/Logos/signoz-brand-logo-new.svg'}
alt={option.label}
className="onboarding-data-source-button-img"
/>

View File

@@ -8,9 +8,6 @@ const DOCLINKS = {
'https://signoz.io/docs/userguide/send-metrics-cloud/',
EXTERNAL_API_MONITORING:
'https://signoz.io/docs/external-api-monitoring/overview/',
QUERY_CLICKHOUSE_TRACES:
'https://signoz.io/docs/userguide/writing-clickhouse-traces-query/#timestamp-bucketing-for-distributed_signoz_index_v3',
AGENT_SKILL_INSTALL: 'https://signoz.io/docs/ai/agent-skills/#installation',
};
export default DOCLINKS;

64
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.40.1
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.144.3-rc.4
github.com/SigNoz/signoz-otel-collector v0.144.2
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/cespare/xxhash/v2 v2.3.0
@@ -69,18 +69,18 @@ require (
go.opentelemetry.io/contrib/config v0.10.0
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.49.0
golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.35.0
golang.org/x/sync v0.20.0
golang.org/x/text v0.35.0
golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
golang.org/x/text v0.33.0
gonum.org/v1/gonum v0.17.0
google.golang.org/api v0.265.0
google.golang.org/protobuf v1.36.11
@@ -92,21 +92,21 @@ require (
require (
github.com/IBM/pgxpoolprometheus v1.1.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
@@ -189,7 +189,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@@ -216,7 +216,7 @@ require (
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
@@ -359,30 +359,30 @@ require (
go.opentelemetry.io/contrib/otelconf v0.18.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0
go.opentelemetry.io/otel/log v0.15.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0
go.opentelemetry.io/proto/otlp v1.9.0
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9
google.golang.org/grpc v1.80.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409
google.golang.org/grpc v1.78.0 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.35.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect

142
go.sum
View File

@@ -108,8 +108,8 @@ github.com/SigNoz/expr v1.17.7-beta h1:FyZkleM5dTQ0O6muQfwGpoH5A2ohmN/XTasRCO72g
github.com/SigNoz/expr v1.17.7-beta/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
github.com/SigNoz/signoz-otel-collector v0.144.3-rc.4 h1:EskJkEMfuuIyArWhV8SleDV/fuKxiaEGTXrCZIFqDT4=
github.com/SigNoz/signoz-otel-collector v0.144.3-rc.4/go.mod h1:9pLVpcIQvUT88ZiNnZN/MI+XLruvwC+Xm2UzPmGjNfA=
github.com/SigNoz/signoz-otel-collector v0.144.2 h1:+7qJ9KLFqXf/RunzNCOoW//gswB6DrX1WXf3iiMYhlM=
github.com/SigNoz/signoz-otel-collector v0.144.2/go.mod h1:kGd0nNlDTndGDGYlyczff+SNnCvS88NoXnlcW5OXiyU=
github.com/Yiling-J/theine-go v0.6.2 h1:1GeoXeQ0O0AUkiwj2S9Jc0Mzx+hpqzmqsJ4kIC4M9AY=
github.com/Yiling-J/theine-go v0.6.2/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
@@ -135,8 +135,8 @@ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
@@ -146,10 +146,10 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2u
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
@@ -158,11 +158,11 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0 h1:cRZQsqCy59DSJmvmUYzi9K+duty
github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY=
github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 h1:MzP/ElwTpINq+hS80ZQz4epKVnUTlz8Sz+P/AFORCKM=
github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0/go.mod h1:pMlGFDpHoLTJOIZHGdJOAWmi+xeIlQXuFTuQxs1epYE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/kafka v1.46.7 h1:0jDb9b505gbCmtjH1RT7kx8hDbVDzOhnTeZm7dzskpQ=
github.com/aws/aws-sdk-go-v2/service/kafka v1.46.7/go.mod h1:tWnHS64fg5ydLHivFlCAtEh/1iMNzr56QsH3F+UTwD4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc=
@@ -180,8 +180,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
@@ -235,8 +235,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/coder/quartz v0.3.0 h1:bUoSEJ77NBfKtUqv6CPSC0AS8dsjqAqqAv7bN02m1mg=
github.com/coder/quartz v0.3.0/go.mod h1:BgE7DOj/8NfvRgvKw0jPLDQH/2Lya2kxcTaNJ8X0rZk=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -299,7 +299,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -342,8 +342,8 @@ github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7F
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -572,8 +572,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajR
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
@@ -1303,22 +1303,22 @@ go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLb
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/contrib/zpages v0.63.0 h1:TppOKuZGbqXMgsfjqq3i09N5Vbo1JLtLImUqiTPGnX4=
go.opentelemetry.io/contrib/zpages v0.63.0/go.mod h1:5F8uugz75ay/MMhRRhxAXY33FuaI8dl7jTxefrIy5qk=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
@@ -1327,25 +1327,25 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A=
go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE=
go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI=
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8=
@@ -1385,8 +1385,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1424,8 +1424,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1474,8 +1474,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1496,8 +1496,8 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1510,8 +1510,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1598,12 +1598,12 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1614,8 +1614,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1678,8 +1678,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1820,10 +1820,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1856,8 +1856,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View File

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

View File

@@ -0,0 +1,33 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
if err := router.Handle("/api/v3/traces/{traceID}/waterfall", handler.New(
provider.authZ.ViewAccess(provider.traceDetailHandler.GetWaterfall),
handler.OpenAPIDef{
ID: "GetWaterfall",
Tags: []string{"tracedetail"},
Summary: "Get waterfall view for a trace",
Description: "Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination",
Request: new(tracedetailtypes.WaterfallRequest),
RequestContentType: "application/json",
Response: new(tracedetailtypes.WaterfallResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -14,7 +14,7 @@ import (
sdkmetric "go.opentelemetry.io/otel/metric"
sdkmetricnoop "go.opentelemetry.io/otel/metric/noop"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
sdktrace "go.opentelemetry.io/otel/trace"
)

View File

@@ -0,0 +1,51 @@
package impltracedetail
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
module tracedetail.Module
}
func NewHandler(module tracedetail.Module) tracedetail.Handler {
return &handler{module: module}
}
func (h *handler) GetWaterfall(rw http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
traceID := mux.Vars(r)["traceID"] // can't be empty or route won't match
var req tracedetailtypes.WaterfallRequest
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
result, err := h.module.GetWaterfall(r.Context(), orgID, traceID, &req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, result)
}

View File

@@ -0,0 +1,209 @@
package impltracedetail
import (
"context"
"log/slog"
"sort"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
"github.com/SigNoz/signoz/pkg/valuer"
tracedetailv2 "github.com/SigNoz/signoz/pkg/query-service/app/traces/tracedetail"
)
type module struct {
store traceStore
cache cache.Cache
logger *slog.Logger
}
func NewModule(telemetryStore telemetrystore.TelemetryStore, cache cache.Cache, providerSettings factory.ProviderSettings) tracedetail.Module {
return &module{
store: newClickhouseTraceStore(telemetryStore),
cache: cache,
logger: providerSettings.Logger,
}
}
func (m *module) GetWaterfall(ctx context.Context, orgID valuer.UUID, traceID string, req *tracedetailtypes.WaterfallRequest) (*tracedetailtypes.WaterfallResponse, error) {
traceData, err := m.getTraceData(ctx, orgID, traceID)
if err != nil {
if errors.Is(err, tracedetailtypes.ErrTraceNotFound) {
return new(tracedetailtypes.WaterfallResponse), nil
}
return nil, err
}
// Span selection: all spans or windowed
limit := min(req.Limit, tracedetailtypes.MaxLimitToSelectAllSpans)
selectAllSpans := traceData.TotalSpans <= uint64(limit)
var (
selectedSpans []*tracedetailtypes.WaterfallSpan
uncollapsedSpans []string
rootServiceName, rootServiceEntryPoint string
)
if selectAllSpans {
selectedSpans, rootServiceName, rootServiceEntryPoint = GetAllSpans(traceData.TraceRoots)
} else {
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint = GetSelectedSpans(
req.UncollapsedSpans, req.SelectedSpanID, traceData.TraceRoots, traceData.SpanIDToSpanNodeMap,
)
}
return tracedetailtypes.NewWaterfallResponse(traceData, selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint, selectAllSpans), nil
}
// getTraceData returns the waterfall cache for the given traceID with fallback on DB.
func (m *module) getTraceData(ctx context.Context, orgID valuer.UUID, traceID string) (*tracedetailtypes.WaterfallTrace, error) {
if cached, err := m.getFromCache(ctx, orgID, traceID); err == nil {
return cached, nil
}
m.logger.DebugContext(ctx, "cache miss for v3 waterfall", slog.String("trace_id", traceID))
summary, err := m.store.GetTraceSummary(ctx, traceID)
if err != nil {
return nil, err
}
spanItems, err := m.store.GetTraceSpans(ctx, traceID, summary)
if err != nil {
return nil, err
}
if len(spanItems) == 0 {
return nil, tracedetailtypes.ErrTraceNotFound
}
traceData := computeWaterfallTrace(spanItems)
cacheKey := strings.Join([]string{"v3_waterfall", traceID}, "-")
if cacheErr := m.cache.Set(ctx, orgID, cacheKey, traceData, tracedetailtypes.WaterfallCacheTTL); cacheErr != nil {
m.logger.ErrorContext(ctx, "failed to store v3 waterfall cache", slog.String("trace_id", traceID), errors.Attr(cacheErr))
}
return traceData, nil
}
// computeWaterfallTrace builds a WaterfallTrace from raw span rows by constructing
// the parent-child tree, inserting missing span placeholders, and calculating service times.
func computeWaterfallTrace(spanItems []tracedetailtypes.SpanModel) *tracedetailtypes.WaterfallTrace {
var (
startTime, endTime, durationNano, totalErrorSpans uint64
spanIDToSpanNodeMap = make(map[string]*tracedetailtypes.WaterfallSpan, len(spanItems))
serviceNameIntervalMap = map[string][]tracedetailv2.Interval{}
traceRoots []*tracedetailtypes.WaterfallSpan
hasMissingSpans bool
)
for _, item := range spanItems {
span := item.ToSpan()
startTimeUnixNano := span.TimeUnixMilli
if startTime == 0 || startTimeUnixNano < startTime {
startTime = startTimeUnixNano
}
if endTime == 0 || (startTimeUnixNano+span.DurationNano) > endTime {
endTime = startTimeUnixNano + span.DurationNano
}
if durationNano == 0 || span.DurationNano > durationNano {
durationNano = span.DurationNano
}
if span.HasError {
totalErrorSpans++
}
serviceNameIntervalMap[span.ServiceName] = append(
serviceNameIntervalMap[span.ServiceName],
tracedetailv2.Interval{StartTime: startTimeUnixNano, Duration: span.DurationNano, Service: span.ServiceName},
)
spanIDToSpanNodeMap[span.SpanID] = span
}
for _, spanNode := range spanIDToSpanNodeMap {
if spanNode.ParentSpanID != "" {
if parentNode, exists := spanIDToSpanNodeMap[spanNode.ParentSpanID]; exists {
parentNode.Children = append(parentNode.Children, spanNode)
} else {
missingSpan := &tracedetailtypes.WaterfallSpan{
SpanID: spanNode.ParentSpanID,
TraceID: spanNode.TraceID,
Name: "Missing Span",
TimeUnixMilli: spanNode.TimeUnixMilli,
DurationNano: spanNode.DurationNano,
Events: make([]tracedetailtypes.Event, 0),
Children: make([]*tracedetailtypes.WaterfallSpan, 0),
Attributes: make(map[string]any),
Resource: make(map[string]string),
}
missingSpan.Children = append(missingSpan.Children, spanNode)
spanIDToSpanNodeMap[missingSpan.SpanID] = missingSpan
traceRoots = append(traceRoots, missingSpan)
hasMissingSpans = true
}
} else if !containsSpan(traceRoots, spanNode) {
traceRoots = append(traceRoots, spanNode)
}
}
for _, root := range traceRoots {
SortSpanChildren(root)
}
sort.Slice(traceRoots, func(i, j int) bool {
if traceRoots[i].TimeUnixMilli == traceRoots[j].TimeUnixMilli {
return traceRoots[i].Name < traceRoots[j].Name
}
return traceRoots[i].TimeUnixMilli < traceRoots[j].TimeUnixMilli
})
return tracedetailtypes.NewWaterfallTrace(
startTime,
endTime,
durationNano,
uint64(len(spanItems)),
totalErrorSpans,
spanIDToSpanNodeMap,
tracedetailv2.CalculateServiceTime(serviceNameIntervalMap),
traceRoots,
hasMissingSpans,
)
}
func (m *module) getFromCache(ctx context.Context, orgID valuer.UUID, traceID string) (*tracedetailtypes.WaterfallTrace, error) {
cachedData := new(tracedetailtypes.WaterfallTrace)
cacheKey := strings.Join([]string{"v3_waterfall", traceID}, "-")
err := m.cache.Get(ctx, orgID, cacheKey, cachedData)
if err != nil {
return nil, err
}
// Skip cache if trace end time falls within flux interval
if time.Since(time.UnixMilli(int64(cachedData.EndTime))) < tracedetailtypes.FluxInterval {
m.logger.InfoContext(ctx, "trace end time within flux interval, skipping v3 waterfall cache", slog.String("trace_id", traceID))
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "trace end time within flux interval, traceID: %s", traceID)
}
m.logger.InfoContext(ctx, "cache hit for v3 waterfall", slog.String("trace_id", traceID))
return cachedData, nil
}
func containsSpan(spans []*tracedetailtypes.WaterfallSpan, target *tracedetailtypes.WaterfallSpan) bool {
for _, s := range spans {
if s.SpanID == target.SpanID {
return true
}
}
return false
}

View File

@@ -0,0 +1,71 @@
package impltracedetail
import (
"context"
"database/sql"
"fmt"
"strconv"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
)
type traceStore interface {
GetTraceSummary(ctx context.Context, traceID string) (*tracedetailtypes.TraceSummary, error)
GetTraceSpans(ctx context.Context, traceID string, summary *tracedetailtypes.TraceSummary) ([]tracedetailtypes.SpanModel, error)
}
type clickhouseTraceStore struct {
telemetryStore telemetrystore.TelemetryStore
}
func newClickhouseTraceStore(ts telemetrystore.TelemetryStore) traceStore {
return &clickhouseTraceStore{telemetryStore: ts}
}
func (s *clickhouseTraceStore) GetTraceSummary(ctx context.Context, traceID string) (*tracedetailtypes.TraceSummary, error) {
query := fmt.Sprintf(
"SELECT trace_id, min(start) AS start, max(end) AS end, sum(num_spans) AS num_spans FROM %s.%s WHERE trace_id=$1 GROUP BY trace_id",
tracedetailtypes.TraceDB, tracedetailtypes.TraceSummaryTable,
)
var summary tracedetailtypes.TraceSummary
err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, query, traceID).Scan(
&summary.TraceID, &summary.Start, &summary.End, &summary.NumSpans,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, tracedetailtypes.ErrTraceNotFound
}
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "error querying trace summary: %v", err)
}
return &summary, nil
}
func (s *clickhouseTraceStore) GetTraceSpans(ctx context.Context, traceID string, summary *tracedetailtypes.TraceSummary) ([]tracedetailtypes.SpanModel, error) {
query := fmt.Sprintf(`
SELECT DISTINCT ON (span_id)
timestamp, duration_nano, span_id, trace_id, has_error, kind,
resource_string_service$$name, name, links as references,
attributes_string, attributes_number, attributes_bool, resources_string,
events, status_message, status_code_string, kind_string, parent_span_id,
flags, is_remote, trace_state, status_code,
db_name, db_operation, http_method, http_url, http_host,
external_http_method, external_http_url, response_status_code
FROM %s.%s
WHERE trace_id=$1 AND ts_bucket_start>=$2 AND ts_bucket_start<=$3
ORDER BY timestamp ASC, name ASC`,
tracedetailtypes.TraceDB, tracedetailtypes.TraceTable,
)
var spanItems []tracedetailtypes.SpanModel
err := s.telemetryStore.ClickhouseDB().Select(
ctx, &spanItems, query,
traceID,
strconv.FormatInt(summary.Start.Unix()-1800, 10),
strconv.FormatInt(summary.End.Unix(), 10),
)
if err != nil {
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "error querying trace spans: %v", err)
}
return spanItems, nil
}

View File

@@ -0,0 +1,193 @@
package impltracedetail
import (
"maps"
"slices"
"sort"
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
)
func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoots []*tracedetailtypes.WaterfallSpan, spanIDToSpanNodeMap map[string]*tracedetailtypes.WaterfallSpan) ([]*tracedetailtypes.WaterfallSpan, []string, string, string) {
uncollapsedSpanMap := getUncollapsedSpanMap(uncollapsedSpans, selectedSpanID, traceRoots, spanIDToSpanNodeMap)
var preOrderTraversal = make([]*tracedetailtypes.WaterfallSpan, 0)
selectedSpanIndex := -1
for _, rootSpanID := range traceRoots {
rootNode, exists := spanIDToSpanNodeMap[rootSpanID.SpanID]
if !exists {
continue
}
opts := traverseOpts{
uncollapsedSpans: uncollapsedSpanMap,
selectedSpanID: selectedSpanID,
}
preOrderderedSpans, autoExpanded := traverseTrace(rootNode, opts, 0, true, 0)
for _, spanID := range autoExpanded {
uncollapsedSpanMap[spanID] = struct{}{}
}
if idx := findIndexForSelectedSpan(preOrderderedSpans, selectedSpanID); idx != -1 {
selectedSpanIndex = idx + len(preOrderTraversal)
}
preOrderTraversal = append(preOrderTraversal, preOrderderedSpans...)
}
startIndex, endIndex := windowAroundIndex(selectedSpanIndex, len(preOrderTraversal))
var rootServiceName, rootServiceEntryPoint string
if len(traceRoots) > 0 {
rootServiceName = traceRoots[0].ServiceName
rootServiceEntryPoint = traceRoots[0].Name
}
return preOrderTraversal[startIndex:endIndex], slices.Collect(maps.Keys(uncollapsedSpanMap)), rootServiceName, rootServiceEntryPoint
}
// getUncollapsedSpanMap creates a map from uncollapsed spans ids and root to selected span path.
func getUncollapsedSpanMap(uncollapsedSpans []string, selectedSpanID string, traceRoots []*tracedetailtypes.WaterfallSpan, spanIDToSpanNodeMap map[string]*tracedetailtypes.WaterfallSpan) map[string]struct{} {
uncollapsedSpanMap := make(map[string]struct{}, len(uncollapsedSpans))
for _, spanID := range uncollapsedSpans {
uncollapsedSpanMap[spanID] = struct{}{}
}
for _, root := range traceRoots {
rootNode, exists := spanIDToSpanNodeMap[root.SpanID]
if !exists {
continue
}
if found, path := getPathFromRootToSelectedSpanID(rootNode, selectedSpanID); found {
for _, spanID := range path {
if spanID != selectedSpanID {
uncollapsedSpanMap[spanID] = struct{}{}
}
}
break
}
}
return uncollapsedSpanMap
}
// windowAroundIndex returns start/end indices for a window of SpanLimitPerRequest spans.
func windowAroundIndex(selectedIndex, total int) (start, end int) {
selectedIndex = max(selectedIndex, 0)
start = selectedIndex - int(tracedetailtypes.SpanLimitPerRequest*0.4)
end = selectedIndex + int(tracedetailtypes.SpanLimitPerRequest*0.6)
if start < 0 {
end = end - start
start = 0
}
if end > total {
start = start - (end - total)
end = total
}
start = max(start, 0)
return
}
func GetAllSpans(traceRoots []*tracedetailtypes.WaterfallSpan) (spans []*tracedetailtypes.WaterfallSpan, rootServiceName, rootEntryPoint string) {
if len(traceRoots) > 0 {
rootServiceName = traceRoots[0].ServiceName
rootEntryPoint = traceRoots[0].Name
}
for _, root := range traceRoots {
childSpans, _ := traverseTrace(root, traverseOpts{selectAll: true}, 0, true, 0)
spans = append(spans, childSpans...)
}
return
}
// SortSpanChildren recursively sorts children of each span by TimeUnixNano then Name.
func SortSpanChildren(span *tracedetailtypes.WaterfallSpan) {
sort.Slice(span.Children, func(i, j int) bool {
if span.Children[i].TimeUnixMilli == span.Children[j].TimeUnixMilli {
return span.Children[i].Name < span.Children[j].Name
}
return span.Children[i].TimeUnixMilli < span.Children[j].TimeUnixMilli
})
for _, child := range span.Children {
SortSpanChildren(child)
}
}
type traverseOpts struct {
uncollapsedSpans map[string]struct{}
selectedSpanID string
selectAll bool
}
func traverseTrace(
span *tracedetailtypes.WaterfallSpan,
opts traverseOpts,
level uint64,
isPartOfPreOrder bool,
autoExpandDepth int,
) ([]*tracedetailtypes.WaterfallSpan, []string) {
preOrderTraversal := []*tracedetailtypes.WaterfallSpan{}
autoExpandedSpans := []string{}
span.SubTreeNodeCount = 0
nodeWithoutChildren := span.CopyWithoutChildren(level)
if isPartOfPreOrder {
preOrderTraversal = append(preOrderTraversal, nodeWithoutChildren)
}
remainingAutoExpandDepth := 0
_, isSelectedSpanUncollapsed := opts.uncollapsedSpans[opts.selectedSpanID]
if span.SpanID == opts.selectedSpanID && isSelectedSpanUncollapsed {
remainingAutoExpandDepth = tracedetailtypes.MaxDepthForSelectedChildren
} else if autoExpandDepth > 0 {
remainingAutoExpandDepth = autoExpandDepth - 1
}
_, isAlreadyUncollapsed := opts.uncollapsedSpans[span.SpanID]
for _, child := range span.Children {
isChildWithinMaxDepth := remainingAutoExpandDepth > 0
childIsPartOfPreOrder := opts.selectAll || (isPartOfPreOrder && (isAlreadyUncollapsed || isChildWithinMaxDepth))
if isPartOfPreOrder && isChildWithinMaxDepth && !isAlreadyUncollapsed {
if !slices.Contains(autoExpandedSpans, span.SpanID) {
autoExpandedSpans = append(autoExpandedSpans, span.SpanID)
}
}
childTraversal, childAutoExpanded := traverseTrace(child, opts, level+1, childIsPartOfPreOrder, remainingAutoExpandDepth)
preOrderTraversal = append(preOrderTraversal, childTraversal...)
autoExpandedSpans = append(autoExpandedSpans, childAutoExpanded...)
nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
span.SubTreeNodeCount += child.SubTreeNodeCount + 1
}
nodeWithoutChildren.SubTreeNodeCount += 1
return preOrderTraversal, autoExpandedSpans
}
func getPathFromRootToSelectedSpanID(node *tracedetailtypes.WaterfallSpan, selectedSpanID string) (bool, []string) {
path := []string{node.SpanID}
if node.SpanID == selectedSpanID {
return true, path
}
for _, child := range node.Children {
found, childPath := getPathFromRootToSelectedSpanID(child, selectedSpanID)
if found {
path = append(path, childPath...)
return true, path
}
}
return false, nil
}
func findIndexForSelectedSpan(spans []*tracedetailtypes.WaterfallSpan, selectedSpanID string) int {
for i, span := range spans {
if span.SpanID == selectedSpanID {
return i
}
}
return -1
}

View File

@@ -0,0 +1,559 @@
// Package impltracedetail tests — waterfall
//
// # Background
//
// The waterfall view renders a trace as a scrollable list of spans in
// pre-order (parent before children, siblings left-to-right). Because a trace
// can have thousands of spans, only a window of ~500 is returned per request.
// The window is centred on the selected span.
//
// # Key concepts
//
// uncollapsedSpans
//
// The set of span IDs the user has manually expanded in the UI.
// Only the direct children of an uncollapsed span are included in the
// output; grandchildren stay hidden until their parent is also uncollapsed.
// When multiple spans are uncollapsed their children are all visible at once.
//
// selectedSpanID
//
// The span currently focused — set when the user clicks a span in the
// waterfall or selects one from the flamegraph. The output window is always
// centred on this span. The path from the trace root down to the selected
// span is automatically uncollapsed so ancestors are visible even if they are
// not in uncollapsedSpans.
//
//
// traceRoots
//
// Root spans of the trace — spans with no parent in the current dataset.
// Normally one, but multiple roots are common when upstream services are
// not instrumented or their spans were not sampled/exported.
package impltracedetail
import (
"fmt"
"testing"
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
"github.com/stretchr/testify/assert"
)
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
func mkSpan(id, service string, children ...*tracedetailtypes.WaterfallSpan) *tracedetailtypes.WaterfallSpan {
return &tracedetailtypes.WaterfallSpan{
SpanID: id,
ServiceName: service,
Name: id + "-op",
Children: children,
}
}
func spanIDs(spans []*tracedetailtypes.WaterfallSpan) []string {
ids := make([]string, len(spans))
for i, s := range spans {
ids[i] = s.SpanID
}
return ids
}
func buildSpanMap(roots ...*tracedetailtypes.WaterfallSpan) map[string]*tracedetailtypes.WaterfallSpan {
m := map[string]*tracedetailtypes.WaterfallSpan{}
var walk func(*tracedetailtypes.WaterfallSpan)
walk = func(s *tracedetailtypes.WaterfallSpan) {
m[s.SpanID] = s
for _, c := range s.Children {
walk(c)
}
}
for _, r := range roots {
SortSpanChildren(r)
walk(r)
}
return m
}
// makeChain builds a linear trace: span0 → span1 → … → span(n-1).
func makeChain(n int) (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan, []string) {
spans := make([]*tracedetailtypes.WaterfallSpan, n)
for i := n - 1; i >= 0; i-- {
if i == n-1 {
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc")
} else {
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc", spans[i+1])
}
}
uncollapsed := make([]string, n)
for i := range spans {
uncollapsed[i] = fmt.Sprintf("span%d", i)
}
return spans[0], buildSpanMap(spans[0]), uncollapsed
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — span ordering and visibility
// ─────────────────────────────────────────────────────────────────────────────
func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
tests := []struct {
name string
buildRoots func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan)
uncollapsedSpans []string
selectedSpanID string
wantSpanIDs []string
}{
{
// Pre-order traversal is preserved: parent before children, siblings left-to-right.
name: "pre_order_traversal",
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
mkSpan("child2", "svc"),
)
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{"root", "child1"},
selectedSpanID: "root",
wantSpanIDs: []string{"root", "child1", "grandchild", "child2"},
},
{
// Multiple spans uncollapsed simultaneously: children of all uncollapsed spans are visible at once.
//
// root
// ├─ childA (uncollapsed) → grandchildA ✓
// └─ childB (uncollapsed) → grandchildB ✓
name: "multiple_uncollapsed",
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc", mkSpan("grandchildA", "svc")),
mkSpan("childB", "svc", mkSpan("grandchildB", "svc")),
)
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{"root", "childA", "childB"},
selectedSpanID: "root",
wantSpanIDs: []string{"root", "childA", "grandchildA", "childB", "grandchildB"},
},
{
// Collapsing a span with other uncollapsed spans.
//
// root
// ├─ childA (previously expanded — in uncollapsedSpans)
// │ ├─ grandchild1 ✓
// │ │ └─ greatGrandchild ✗ (grandchild1 not in uncollapsedSpans)
// │ └─ grandchild2 ✓
// └─ childB ← selected (not expanded)
name: "manual_uncollapse",
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc",
mkSpan("grandchild1", "svc", mkSpan("greatGrandchild", "svc")),
mkSpan("grandchild2", "svc"),
),
mkSpan("childB", "svc"),
)
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{"childA"},
selectedSpanID: "childB",
wantSpanIDs: []string{"root", "childA", "grandchild1", "grandchild2", "childB"},
},
{
// A collapsed span hides all children.
name: "collapsed_span_hides_children",
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("child1", "svc"),
mkSpan("child2", "svc"),
)
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "root",
wantSpanIDs: []string{"root"},
},
{
// Selecting a span auto-uncollpases the path from root to that span so it is visible.
//
// root → parent → selected
name: "path_to_selected_is_uncollapsed",
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
wantSpanIDs: []string{"root", "parent", "selected"},
},
{
// Siblings of ancestors are rendered as collapsed nodes but their subtrees must NOT be expanded.
//
// root
// ├─ unrelated → unrelated-child (✗)
// └─ parent → selected
name: "siblings_not_expanded",
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
// children of root sort alphabetically: parent < unrelated; unrelated-child stays hidden
wantSpanIDs: []string{"root", "parent", "selected", "unrelated"},
},
{
// An unknown selectedSpanID must not panic; returns a window from index 0.
name: "unknown_selected_span",
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc", mkSpan("child", "svc"))
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "nonexistent",
wantSpanIDs: []string{"root"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
roots, spanMap := tc.buildRoots()
spans, _, _, _ := GetSelectedSpans(tc.uncollapsedSpans, tc.selectedSpanID, roots, spanMap)
assert.Equal(t, tc.wantSpanIDs, spanIDs(spans))
})
}
}
// Multiple roots: both trees are flattened into a single pre-order list with
// root1's subtree before root2's. Service/entry-point come from the first root.
//
// root1 svc-a ← selected
// └─ child1
// root2 svc-b
// └─ child2
//
// Expected output order: root1 → child1 → root2 → child2.
func TestGetSelectedSpans_MultipleRoots(t *testing.T) {
root1 := mkSpan("root1", "svc-a", mkSpan("child1", "svc-a"))
root2 := mkSpan("root2", "svc-b", mkSpan("child2", "svc-b"))
spanMap := buildSpanMap(root1, root2)
spans, _, svcName, entryPoint := GetSelectedSpans([]string{"root1", "root2"}, "root1", []*tracedetailtypes.WaterfallSpan{root1, root2}, spanMap)
assert.Equal(t, []string{"root1", "child1", "root2", "child2"}, spanIDs(spans), "root1 subtree must precede root2 subtree")
assert.Equal(t, "svc-a", svcName, "metadata comes from first root")
assert.Equal(t, "root1-op", entryPoint, "metadata comes from first root")
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — uncollapsed span tracking
// ─────────────────────────────────────────────────────────────────────────────
func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
tests := []struct {
name string
buildRoot func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan)
uncollapsedSpans []string
selectedSpanID string
wantSpanIDs []string
checkUncollapsed func(t *testing.T, uncollapsed []string)
}{
{
// The path-to-selected spans are returned in updatedUncollapsedSpans.
name: "path_returned_in_uncollapsed",
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
wantSpanIDs: []string{"root", "parent", "selected"},
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
},
},
{
// Siblings of ancestors are not tracked as uncollapsed.
name: "siblings_not_in_uncollapsed",
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
wantSpanIDs: []string{"root", "parent", "selected", "unrelated"},
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
},
},
{
// Auto-expanded span IDs from ALL branches are returned in updatedUncollapsedSpans.
// Only internal nodes (spans with children) are tracked — leaf spans are never added.
// root is in uncollapsedSpans, so its children are auto-expanded.
//
// root (selected, expanded)
// ├─ childA (internal ✓)
// │ └─ grandchildA (internal ✓)
// │ └─ leafA (leaf ✗)
// └─ childB (internal ✓)
// └─ grandchildB (internal ✓)
// └─ leafB (leaf ✗)
name: "auto_expanded_spans_returned",
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc",
mkSpan("grandchildA", "svc",
mkSpan("leafA", "svc"),
),
),
mkSpan("childB", "svc",
mkSpan("grandchildB", "svc",
mkSpan("leafB", "svc"),
),
),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{"root"},
selectedSpanID: "root",
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
assert.Contains(t, uncollapsed, "root")
assert.Contains(t, uncollapsed, "childA", "internal node depth 1, branch A")
assert.Contains(t, uncollapsed, "childB", "internal node depth 1, branch B")
assert.Contains(t, uncollapsed, "grandchildA", "internal node depth 2, branch A")
assert.Contains(t, uncollapsed, "grandchildB", "internal node depth 2, branch B")
assert.NotContains(t, uncollapsed, "leafA", "leaf spans are never added to uncollapsedSpans")
assert.NotContains(t, uncollapsed, "leafB", "leaf spans are never added to uncollapsedSpans")
},
},
{
// If the selected span is already in uncollapsedSpans,
// it should appear exactly once in the result.
name: "duplicate_in_uncollapsed",
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("selected", "svc", mkSpan("child", "svc")),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{"selected"}, // already present
selectedSpanID: "selected",
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
count := 0
for _, id := range uncollapsed {
if id == "selected" {
count++
}
}
assert.Equal(t, 1, count, "should appear once")
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
root, spanMap := tc.buildRoot()
spans, uncollapsed, _, _ := GetSelectedSpans(tc.uncollapsedSpans, tc.selectedSpanID, []*tracedetailtypes.WaterfallSpan{root}, spanMap)
if tc.wantSpanIDs != nil {
assert.Equal(t, tc.wantSpanIDs, spanIDs(spans))
}
if tc.checkUncollapsed != nil {
tc.checkUncollapsed(t, uncollapsed)
}
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — span metadata
// ─────────────────────────────────────────────────────────────────────────────
// Test to check if Level, HasChildren, and SubTreeNodeCount are populated correctly.
//
// root level=0, hasChildren=true, subTree=4
// child1 level=1, hasChildren=true, subTree=2
// grandchild level=2, hasChildren=false, subTree=1
// child2 level=1, hasChildren=false, subTree=1
func TestGetSelectedSpans_SpanMetadata(t *testing.T) {
root := mkSpan("root", "svc",
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
mkSpan("child2", "svc"),
)
spanMap := buildSpanMap(root)
spans, _, _, _ := GetSelectedSpans([]string{"root", "child1"}, "root", []*tracedetailtypes.WaterfallSpan{root}, spanMap)
byID := map[string]*tracedetailtypes.WaterfallSpan{}
for _, s := range spans {
byID[s.SpanID] = s
}
tests := []struct {
spanID string
wantLevel uint64
wantHasChildren bool
wantSubTree uint64
}{
{"root", 0, true, 4},
{"child1", 1, true, 2},
{"child2", 1, false, 1},
{"grandchild", 2, false, 1},
}
for _, tc := range tests {
t.Run(tc.spanID, func(t *testing.T) {
s := byID[tc.spanID]
assert.Equal(t, tc.wantLevel, s.Level)
assert.Equal(t, tc.wantHasChildren, s.HasChildren)
assert.Equal(t, tc.wantSubTree, s.SubTreeNodeCount)
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — windowing
// ─────────────────────────────────────────────────────────────────────────────
func TestGetSelectedSpans_Window(t *testing.T) {
tests := []struct {
name string
selectedSpanID string
wantLen int
wantFirst string
wantLast string
wantSelectedPos int
}{
{
// The selected span is centred: 200 spans before it, 300 after (0.4 / 0.6 split).
name: "centred_on_selected",
selectedSpanID: "span300",
wantLen: 500,
wantFirst: "span100",
wantLast: "span599",
wantSelectedPos: 200,
},
{
// When the selected span is near the start, the window shifts right so no
// negative index is used — the result is still 500 spans.
name: "shifts_at_start",
selectedSpanID: "span10",
wantLen: 500,
wantFirst: "span0",
wantSelectedPos: 10,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
root, spanMap, uncollapsed := makeChain(600)
spans, _, _, _ := GetSelectedSpans(uncollapsed, tc.selectedSpanID, []*tracedetailtypes.WaterfallSpan{root}, spanMap)
assert.Equal(t, tc.wantLen, len(spans), "window size")
assert.Equal(t, tc.wantFirst, spans[0].SpanID, "first span in window")
if tc.wantLast != "" {
assert.Equal(t, tc.wantLast, spans[len(spans)-1].SpanID, "last span in window")
}
assert.Equal(t, tc.selectedSpanID, spans[tc.wantSelectedPos].SpanID, "selected span position")
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — depth limit
// ─────────────────────────────────────────────────────────────────────────────
// Depth is measured from the selected span, not the trace root.
// Ancestors appear via the path-to-root logic, not the depth limit.
// Each depth level has two children to confirm the limit is enforced on all
// branches, not just the first.
//
// root
// └─ A ancestor ✓ (path-to-root)
// └─ selected
// ├─ d1a depth 1 ✓
// │ ├─ d2a depth 2 ✓
// │ │ ├─ d3a depth 3 ✓
// │ │ │ ├─ d4a depth 4 ✓
// │ │ │ │ ├─ d5a depth 5 ✓
// │ │ │ │ │ └─ d6a depth 6 ✗
// │ │ │ │ └─ d5b depth 5 ✓
// │ │ │ └─ d4b depth 4 ✓
// │ │ └─ d3b depth 3 ✓
// │ └─ d2b depth 2 ✓
// └─ d1b depth 1 ✓
func TestGetSelectedSpans_DepthCountedFromSelectedSpan(t *testing.T) {
selected := mkSpan("selected", "svc",
mkSpan("d1a", "svc",
mkSpan("d2a", "svc",
mkSpan("d3a", "svc",
mkSpan("d4a", "svc",
mkSpan("d5a", "svc",
mkSpan("d6a", "svc"), // depth 6 — excluded
),
mkSpan("d5b", "svc"), // depth 5 — included
),
mkSpan("d4b", "svc"), // depth 4 — included
),
mkSpan("d3b", "svc"), // depth 3 — included
),
mkSpan("d2b", "svc"), // depth 2 — included
),
mkSpan("d1b", "svc"), // depth 1 — included
)
root := mkSpan("root", "svc", mkSpan("A", "svc", selected))
spanMap := buildSpanMap(root)
spans, _, _, _ := GetSelectedSpans([]string{"selected"}, "selected", []*tracedetailtypes.WaterfallSpan{root}, spanMap)
ids := spanIDs(spans)
assert.Contains(t, ids, "root", "ancestor shown via path-to-root")
assert.Contains(t, ids, "A", "ancestor shown via path-to-root")
for _, id := range []string{"d1a", "d1b", "d2a", "d2b", "d3a", "d3b", "d4a", "d4b", "d5a", "d5b"} {
assert.Contains(t, ids, id, "depth ≤ 5 — must be included")
}
assert.NotContains(t, ids, "d6a", "depth 6 > limit — excluded")
}
// ─────────────────────────────────────────────────────────────────────────────
// GetAllSpans
// ─────────────────────────────────────────────────────────────────────────────
func TestGetAllSpans(t *testing.T) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc",
mkSpan("grandchildA", "svc",
mkSpan("leafA", "svc2"),
),
),
mkSpan("childB", "svc3",
mkSpan("grandchildB", "svc",
mkSpan("leafB", "svc2"),
),
),
)
spans, rootServiceName, rootEntryPoint := GetAllSpans([]*tracedetailtypes.WaterfallSpan{root})
assert.ElementsMatch(t, spanIDs(spans), []string{"root", "childA", "grandchildA", "leafA", "childB", "grandchildB", "leafB"})
assert.Equal(t, "svc", rootServiceName)
assert.Equal(t, "root-op", rootEntryPoint)
}

View File

@@ -518,26 +518,21 @@ func TestExpressionVSEntry(t *testing.T) {
expression, err := Parse(tt.Query)
assert.NoError(t, err)
compiled, hasBodyFieldRef, _, err := signozstanzahelper.ExprCompileBool(expression)
compiled, hasBodyFieldRef, err := signozstanzahelper.ExprCompileBool(expression)
assert.NoError(t, err)
matchedIDs := []int{}
for _, d := range dataset {
err := signozstanzahelper.RunWithExprEnv(d.Entry, hasBodyFieldRef, nil, func(env map[string]any) error {
matches, err := vm.Run(compiled, env)
if err != nil {
// Eval error (e.g. fromJSON on non-JSON body) => treat as no match
return err
}
if matches != nil && matches.(bool) {
matchedIDs = append(matchedIDs, d.ID)
}
return nil
})
env := signozstanzahelper.GetExprEnv(d.Entry, hasBodyFieldRef)
matches, err := vm.Run(compiled, env)
signozstanzahelper.PutExprEnv(env)
if err != nil {
// Eval error (e.g. fromJSON on non-JSON body) => treat as no match
continue
}
if matches != nil && matches.(bool) {
matchedIDs = append(matchedIDs, d.ID)
}
}
assert.Equal(t, tt.ExpectedMatches, matchedIDs, "query %q", tt.Name)
})

View File

@@ -207,16 +207,23 @@ func AdjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemet
indexes := []telemetrytypes.JSONDataTypeIndex{}
fieldContextsSeen := map[telemetrytypes.FieldContext]bool{}
dataTypesSeen := map[telemetrytypes.FieldDataType]bool{}
jsonTypesSeen := map[string]*telemetrytypes.JSONDataType{}
for _, matchingKey := range matchingKeys {
materialized = materialized && matchingKey.Materialized
fieldContextsSeen[matchingKey.FieldContext] = true
dataTypesSeen[matchingKey.FieldDataType] = true
if matchingKey.JSONDataType != nil {
jsonTypesSeen[matchingKey.JSONDataType.StringValue()] = matchingKey.JSONDataType
}
indexes = append(indexes, matchingKey.Indexes...)
}
for _, matchingKey := range contextPrefixedMatchingKeys {
materialized = materialized && matchingKey.Materialized
fieldContextsSeen[matchingKey.FieldContext] = true
dataTypesSeen[matchingKey.FieldDataType] = true
if matchingKey.JSONDataType != nil {
jsonTypesSeen[matchingKey.JSONDataType.StringValue()] = matchingKey.JSONDataType
}
indexes = append(indexes, matchingKey.Indexes...)
}
key.Materialized = materialized
@@ -241,6 +248,15 @@ func AdjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemet
break
}
}
if len(jsonTypesSeen) == 1 && key.JSONDataType == nil {
// all matching keys have same JSON data type, use it
for _, jt := range jsonTypesSeen {
actions = append(actions, fmt.Sprintf("Adjusting key %s to have JSON data type %s", key, jt.StringValue()))
key.JSONDataType = jt
break
}
}
}
return actions

View File

@@ -34,6 +34,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/services/implservices"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
"github.com/SigNoz/signoz/pkg/querier"
@@ -62,6 +64,7 @@ type Handlers struct {
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
TraceDetail tracedetail.Handler
}
func NewHandlers(
@@ -99,5 +102,6 @@ func NewHandlers(
RegistryHandler: registryHandler,
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
}
}

View File

@@ -37,6 +37,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/session/implsession"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -73,6 +75,7 @@ type Modules struct {
ServiceAccount serviceaccount.Module
CloudIntegration cloudintegration.Module
RuleStateHistory rulestatehistory.Module
TraceDetail tracedetail.Module
}
func NewModules(
@@ -123,5 +126,6 @@ func NewModules(
ServiceAccount: serviceAccount,
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
CloudIntegration: cloudIntegrationModule,
TraceDetail: impltracedetail.NewModule(telemetryStore, cache, providerSettings),
}
}

View File

@@ -28,6 +28,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -69,6 +70,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ tracedetail.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

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

View File

@@ -276,12 +276,11 @@ func (m *fieldMapper) FieldFor(ctx context.Context, tsStart, tsEnd uint64, key *
continue
}
if key.FieldDataType == telemetrytypes.FieldDataTypeUnspecified {
if key.JSONDataType == nil {
return "", qbtypes.ErrColumnNotFound
}
jdt := key.GetJSONDataType()
if key.KeyNameContainsArray() && !jdt.IsArray {
if key.KeyNameContainsArray() && !key.JSONDataType.IsArray {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "FieldFor not supported for nested fields; only supported for flat paths (e.g. body.status.detail) and paths of Array type: %s(%s)", key.Name, key.FieldDataType)
}

View File

@@ -220,7 +220,7 @@ func TestJSONStmtBuilder_PrimitivePaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((LOWER(toString(dynamicElement(body_v2.`user.age`, 'Int64'))) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')) OR ((LOWER(dynamicElement(body_v2.`user.age`, 'String')) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%25%", "%25%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64 name=user.age,context=body,datatype=string]."},
Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64,jsondatatype=Int64 name=user.age,context=body,datatype=string,jsondatatype=String]."},
},
},
{
@@ -414,7 +414,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -441,7 +441,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -455,7 +455,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -549,7 +549,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "((NOT arrayExists(`body_v2.education`-> toFloat64OrNull(dynamicElement(`body_v2.education`.`type`, 'String')) = ?, dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND (NOT arrayExists(`body_v2.education`-> dynamicElement(`body_v2.education`.`type`, 'Int64') = ?, dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))))",
Args: []any{uint64(1747945619), uint64(1747983448), int64(10001), int64(10001), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].type` is ambiguous, found 2 different combinations of field context / data type: [name=education[].type,context=body,datatype=string name=education[].type,context=body,datatype=int64]."},
Warnings: []string{"Key `education[].type` is ambiguous, found 2 different combinations of field context / data type: [name=education[].type,context=body,datatype=string,jsondatatype=String name=education[].type,context=body,datatype=int64,jsondatatype=Int64]."},
},
},
{
@@ -576,7 +576,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -585,7 +585,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), "passed", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -594,7 +594,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> toString(x) = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -603,7 +603,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) IN (?, ?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.99, "1.65", "1.99", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -612,7 +612,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "((NOT arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND (NOT arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))))",
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", float64(1.65), "%1.65%", float64(1.65), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -622,7 +622,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
WhereClause: "(has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?) OR has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?))",
Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{
"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic].",
"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)].",
},
},
},
@@ -702,7 +702,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')) OR ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')))",
Args: []any{uint64(1747945619), uint64(1747983448), "%4%", float64(4), "%4%", float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64 name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string]."},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
},
},
{
@@ -942,171 +942,20 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
})
}
}
func TestJSONStmtBuilder_SelectField(t *testing.T) {
enable, disable := jsonQueryTestUtil(t)
enable()
defer disable()
statementBuilder := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErrContains string
}{
{
name: "select_x_education[].awards[].participated[].members",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "education[].awards[].participated[].members",
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AS `education[].awards[].participated[].members` FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "select_x_user.name",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "user.name",
},
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, dynamicElement(body_v2.`user.name`, 'String') AS `user.name` FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErrContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErrContains)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func TestJSONStmtBuilder_OrderBy(t *testing.T) {
enable, disable := jsonQueryTestUtil(t)
enable()
defer disable()
statementBuilder := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErrContains string
}{
{
name: "order_by_education[].awards[].participated[].members",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
Order: []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "education[].awards[].participated[].members",
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AS `education[].awards[].participated[].members` asc LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "order_by_user.name",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
Order: []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "user.name",
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY dynamicElement(body_v2.`user.name`, 'String') AS `user.name` asc LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErrContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErrContains)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func buildTestTelemetryMetadataStore(t *testing.T, addIndexes bool) *telemetrytypestest.MockMetadataStore {
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
mockMetadataStore.SetStaticFields(IntrinsicFields)
types, _ := telemetrytypes.TestJSONTypeSet()
for path, fieldDataTypes := range types {
for _, fdt := range fieldDataTypes {
for path, jsonTypes := range types {
for _, jsonType := range jsonTypes {
key := &telemetrytypes.TelemetryFieldKey{
Name: path,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: fdt,
FieldDataType: telemetrytypes.MappingJSONDataTypeToFieldDataType[jsonType],
JSONDataType: &jsonType,
}
if addIndexes {
jsonType := telemetrytypes.MappingFieldDataTypeToJSONDataType[fdt]
idx := slices.IndexFunc(telemetrytypes.TestIndexedPaths, func(entry telemetrytypes.TestIndexedPathEntry) bool {
return entry.Path == path && entry.Type == jsonType
})

View File

@@ -875,6 +875,7 @@ func TestAdjustKey(t *testing.T) {
require.Equal(t, c.expectedKey.FieldContext, key.FieldContext, "field context should match")
require.Equal(t, c.expectedKey.FieldDataType, key.FieldDataType, "field data type should match")
require.Equal(t, c.expectedKey.Materialized, key.Materialized, "materialized should match")
require.Equal(t, c.expectedKey.JSONDataType, key.JSONDataType, "json data type should match")
require.Equal(t, c.expectedKey.Indexes, key.Indexes, "json exists should match")
})
}

View File

@@ -21,72 +21,133 @@ import (
)
var (
defaultPathLimit = 100 // Default limit to prevent full table scans
CodeUnknownJSONDataType = errors.MustNewCode("unknown_json_data_type")
CodeFailLoadPromotedPaths = errors.MustNewCode("fail_load_promoted_paths")
CodeFailCheckPathPromoted = errors.MustNewCode("fail_check_path_promoted")
CodeFailIterateBodyJSONKeys = errors.MustNewCode("fail_iterate_body_json_keys")
CodeFailExtractBodyJSONKeys = errors.MustNewCode("fail_extract_body_json_keys")
CodeFailLoadLogsJSONIndexes = errors.MustNewCode("fail_load_logs_json_indexes")
CodeFailListJSONValues = errors.MustNewCode("fail_list_json_values")
CodeFailScanJSONValue = errors.MustNewCode("fail_scan_json_value")
CodeFailScanVariant = errors.MustNewCode("fail_scan_variant")
CodeFailBuildJSONPathsQuery = errors.MustNewCode("fail_build_json_paths_query")
CodeNoPathsToQueryIndexes = errors.MustNewCode("no_paths_to_query_indexes_provided")
CodeFailedToPrepareBatch = errors.MustNewCode("failed_to_prepare_batch_promoted_paths")
CodeFailedToSendBatch = errors.MustNewCode("failed_to_send_batch_promoted_paths")
CodeFailedToAppendPath = errors.MustNewCode("failed_to_append_path_promoted_paths")
)
// enrichBodyKeys enriches body-context keys with promoted path info, indexes,
// and JSON access plans.
// parentTypeCache contains parent array types (ArrayJSON/ArrayDynamic) pre-fetched in the main UNION query.
func (t *telemetryMetaStore) enrichBodyKeys(ctx context.Context, keys []*telemetrytypes.TelemetryFieldKey, parentTypeCache map[string][]telemetrytypes.FieldDataType) error {
if len(keys) == 0 {
return nil
}
// fetchBodyJSONPaths extracts body JSON paths from the path_types table
// This function can be used by both JSONQueryBuilder and metadata extraction
// uniquePathLimit: 0 for no limit, >0 for maximum number of unique paths to return
// - For startup load: set to 10000 to get top 10k unique paths
// - For lookup: set to 0 (no limit needed for single path)
// - For metadata API: set to desired pagination limit
//
// searchOperator: LIKE for pattern matching, EQUAL for exact match.
func (t *telemetryMetaStore) fetchBodyJSONPaths(ctx context.Context,
fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, []string, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
instrumentationtypes.CodeNamespace: "metadata",
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
})
var filteredKeys []*telemetrytypes.TelemetryFieldKey
for _, key := range keys {
if key.FieldContext == telemetrytypes.FieldContextBody {
filteredKeys = append(filteredKeys, key)
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, nil, false, errors.WrapInternalf(err, CodeFailExtractBodyJSONKeys, "failed to extract body JSON keys")
}
defer rows.Close()
fieldKeys := []*telemetrytypes.TelemetryFieldKey{}
paths := []string{}
rowCount := 0
for rows.Next() {
var path string
var typesArray []string // ClickHouse returns array as []string
var lastSeen uint64
err = rows.Scan(&path, &typesArray, &lastSeen)
if err != nil {
return nil, nil, false, errors.WrapInternalf(err, CodeFailExtractBodyJSONKeys, "failed to scan body JSON key row")
}
for _, typ := range typesArray {
mapping, found := telemetrytypes.MappingStringToJSONDataType[typ]
if !found {
t.logger.ErrorContext(ctx, "failed to map type string to JSON data type", slog.String("type", typ), slog.String("path", path))
continue
}
fieldKeys = append(fieldKeys, &telemetrytypes.TelemetryFieldKey{
Name: path,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: telemetrytypes.MappingJSONDataTypeToFieldDataType[mapping],
JSONDataType: &mapping,
})
}
paths = append(paths, path)
rowCount++
}
if rows.Err() != nil {
return nil, nil, false, errors.WrapInternalf(rows.Err(), CodeFailIterateBodyJSONKeys, "error iterating body JSON keys")
}
// collect paths for batch queries
paths := make([]string, 0, len(filteredKeys))
for _, key := range filteredKeys {
paths = append(paths, key.Name)
}
// fetch promoted paths
promoted, err := t.GetPromotedPaths(ctx, paths...)
if err != nil {
return err
}
// fetch JSON path indexes
indexes, err := t.getJSONPathIndexes(ctx, paths...)
if err != nil {
return err
}
// apply promoted/index metadata to keys
for _, key := range filteredKeys {
promotedKey := strings.Split(key.Name, telemetrytypes.ArraySep)[0]
key.Materialized = promoted[promotedKey]
key.Indexes = indexes[key.Name]
}
// build JSON access plans using the pre-fetched parent type cache
return t.buildJSONPlans(ctx, filteredKeys, parentTypeCache)
return fieldKeys, paths, rowCount <= limit, nil
}
// buildJSONPlans builds JSON access plans for the given keys
// using the provided parent type cache (pre-fetched in the main UNION query).
func (t *telemetryMetaStore) buildJSONPlans(_ context.Context, keys []*telemetrytypes.TelemetryFieldKey, typeCache map[string][]telemetrytypes.FieldDataType) error {
if len(keys) == 0 {
return nil
func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
fieldKeys, paths, finished, err := t.fetchBodyJSONPaths(ctx, fieldKeySelectors)
if err != nil {
return nil, false, err
}
columnMeta := t.jsonColumnMetadata[telemetrytypes.SignalLogs][telemetrytypes.FieldContextBody]
promoted, err := t.GetPromotedPaths(ctx, paths...)
if err != nil {
return nil, false, err
}
indexes, err := t.getJSONPathIndexes(ctx, paths...)
if err != nil {
return nil, false, err
}
for _, fieldKey := range fieldKeys {
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
fieldKey.Materialized = promoted[promotedKey]
fieldKey.Indexes = indexes[fieldKey.Name]
}
return fieldKeys, finished, t.buildJSONPlans(ctx, fieldKeys)
}
func (t *telemetryMetaStore) buildJSONPlans(ctx context.Context, keys []*telemetrytypes.TelemetryFieldKey) error {
parentSelectors := make([]*telemetrytypes.FieldKeySelector, 0, len(keys))
for _, key := range keys {
if err := key.SetJSONAccessPlan(columnMeta, typeCache); err != nil {
parentSelectors = append(parentSelectors, key.ArrayParentSelectors()...)
}
parentKeys, _, _, err := t.fetchBodyJSONPaths(ctx, parentSelectors)
if err != nil {
return err
}
typeCache := make(map[string][]telemetrytypes.JSONDataType)
for _, key := range parentKeys {
typeCache[key.Name] = append(typeCache[key.Name], *key.JSONDataType)
}
// build plans for keys now
for _, key := range keys {
err = key.SetJSONAccessPlan(t.jsonColumnMetadata[telemetrytypes.SignalLogs][telemetrytypes.FieldContextBody], typeCache)
if err != nil {
return err
}
}
@@ -94,6 +155,51 @@ func (t *telemetryMetaStore) buildJSONPlans(_ context.Context, keys []*telemetry
return nil
}
func buildGetBodyJSONPathsQuery(fieldKeySelectors []*telemetrytypes.FieldKeySelector) (string, []any, int) {
if len(fieldKeySelectors) == 0 {
return "", nil, defaultPathLimit
}
from := fmt.Sprintf("%s.%s", DBName, PathTypesTableName)
// Build a better query using GROUP BY to deduplicate at database level
// This aggregates all types per path and gets the max last_seen, then applies LIMIT
sb := sqlbuilder.Select(
"path",
"groupArray(DISTINCT type) AS types",
"max(last_seen) AS last_seen",
).From(from)
limit := 0
// Add search filter if provided
orClauses := []string{}
for _, fieldKeySelector := range fieldKeySelectors {
// replace [*] with []
fieldKeySelector.Name = strings.ReplaceAll(fieldKeySelector.Name, telemetrytypes.ArrayAnyIndex, telemetrytypes.ArraySep)
// Extract search text for body JSON keys
keyName := CleanPathPrefixes(fieldKeySelector.Name)
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
orClauses = append(orClauses, sb.Equal("path", keyName))
} else {
// Pattern matching for metadata API (defaults to LIKE behavior for other operators)
orClauses = append(orClauses, sb.ILike("path", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(keyName))))
}
limit += fieldKeySelector.Limit
}
sb.Where(sb.Or(orClauses...))
// Group by path to get unique paths with aggregated types
sb.GroupBy("path")
// Order by max last_seen to get most recent paths first
sb.OrderBy("last_seen DESC")
if limit == 0 {
limit = defaultPathLimit
}
sb.Limit(limit)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return query, args, limit
}
func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...string) (map[string][]telemetrytypes.JSONDataTypeIndex, error) {
filteredPaths := []string{}
for _, path := range paths {

View File

@@ -7,9 +7,99 @@ import (
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/require"
)
func TestBuildGetBodyJSONPathsQuery(t *testing.T) {
testCases := []struct {
name string
fieldKeySelectors []*telemetrytypes.FieldKeySelector
expectedSQL string
expectedArgs []any
expectedLimit int
}{
{
name: "Single search text with EQUAL operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user.name",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (path = ?) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"user.name", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Single search text with LIKE operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%user%", 100},
expectedLimit: 100,
},
{
name: "Multiple search texts with EQUAL operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user.name",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
{
Name: "user.age",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (path = ? OR path = ?) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"user.name", "user.age", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Multiple search texts with LIKE operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
{
Name: "admin",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (LOWER(path) LIKE LOWER(?) OR LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%user%", "%admin%", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Search with Contains operator (should default to LIKE)",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "test",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_json_path_types WHERE (LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%test%", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
query, args, limit := buildGetBodyJSONPathsQuery(tc.fieldKeySelectors)
require.Equal(t, tc.expectedSQL, query)
require.Equal(t, tc.expectedArgs, args)
require.Equal(t, tc.expectedLimit, limit)
})
}
}
func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
testCases := []struct {
name string

View File

@@ -368,20 +368,6 @@ func (t *telemetryMetaStore) logsTblStatementToFieldKeys(ctx context.Context) ([
return materialisedKeys, nil
}
// logKeysUnionArm declares one arm of the UNION ALL in getLogsKeys.
// All per-table variance is captured here so the loop body can stay uniform.
type logKeysUnionArm struct {
shouldQuery bool
fieldContext telemetrytypes.FieldContext
table string
nameColumn string // column holding the field/key name (e.g., "name" or "field_name")
dataTypeColumn string // column used in WHERE/GROUP BY
dataTypeSelectExpr string // expression used in SELECT (may wrap with lower())
addBaseFilters func(sb *sqlbuilder.SelectBuilder) // mandatory WHERE filters (e.g., signal, field_context)
encodeDataType func(telemetrytypes.FieldDataType) string // how to render a FieldDataType in WHERE values
extraOrBranch func(sb *sqlbuilder.SelectBuilder) string // optional extra OR branch (e.g., body parent-types)
}
// getLogsKeys returns the keys from the spans that match the field selection criteria.
func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
@@ -411,152 +397,90 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
// tables to query based on field selectors
queryAttributeTable := false
queryResourceTable := false
queryBodyTable := false
for _, selector := range fieldKeySelectors {
if selector.FieldContext == telemetrytypes.FieldContextUnspecified {
// unspecified context, query all tables
// unspecified context, query both tables
queryAttributeTable = true
queryResourceTable = true
queryBodyTable = true
break
} else if selector.FieldContext == telemetrytypes.FieldContextAttribute {
queryAttributeTable = true
} else if selector.FieldContext == telemetrytypes.FieldContextResource {
queryResourceTable = true
} else if selector.FieldContext == telemetrytypes.FieldContextBody && querybuilder.BodyJSONQueryEnabled {
queryBodyTable = true
}
}
// body keys are gated behind the feature flag
queryBodyTable = queryBodyTable && querybuilder.BodyJSONQueryEnabled
// pre-compute parent array path names from body selectors for JSON plan building;
// these will be fetched as a separate UNION arm filtered to ArrayJSON/ArrayDynamic only
parentPaths := make(map[string]bool)
if queryBodyTable {
for _, sel := range fieldKeySelectors {
if sel.FieldContext != telemetrytypes.FieldContextBody &&
sel.FieldContext != telemetrytypes.FieldContextUnspecified {
continue
}
if !strings.Contains(sel.Name, telemetrytypes.ArraySep) {
continue
}
key := &telemetrytypes.TelemetryFieldKey{
Name: sel.Name,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
}
for _, ps := range key.ArrayParentSelectors() {
parentPaths[ps.Name] = true
}
}
tablesToQuery := []struct {
fieldContext telemetrytypes.FieldContext
shouldQuery bool
}{
{telemetrytypes.FieldContextAttribute, queryAttributeTable},
{telemetrytypes.FieldContextResource, queryResourceTable},
}
// Each UNION arm differs only in: table, data-type column name and SELECT
// expression (lower-wrapped for historical mixed-case in attr/resource),
// base WHERE filters, the per-selector data-type encoding, and (for body)
// an extra OR branch that fetches parent array types for JSON plan building.
// All other logic is shared by the loop below.
tablesToQuery := []logKeysUnionArm{
{
shouldQuery: queryAttributeTable,
fieldContext: telemetrytypes.FieldContextAttribute,
table: t.logsDBName + "." + t.logAttributeKeysTblName,
nameColumn: "name",
dataTypeColumn: "datatype",
dataTypeSelectExpr: "lower(datatype)",
addBaseFilters: func(*sqlbuilder.SelectBuilder) {},
encodeDataType: func(ft telemetrytypes.FieldDataType) string { return ft.TagDataType() },
extraOrBranch: func(*sqlbuilder.SelectBuilder) string { return "" },
},
{
shouldQuery: queryResourceTable,
fieldContext: telemetrytypes.FieldContextResource,
table: t.logsDBName + "." + t.logResourceKeysTblName,
nameColumn: "name",
dataTypeColumn: "datatype",
dataTypeSelectExpr: "lower(datatype)",
addBaseFilters: func(*sqlbuilder.SelectBuilder) {},
encodeDataType: func(ft telemetrytypes.FieldDataType) string { return ft.TagDataType() },
extraOrBranch: func(*sqlbuilder.SelectBuilder) string { return "" },
},
{
shouldQuery: queryBodyTable,
fieldContext: telemetrytypes.FieldContextBody,
table: fmt.Sprintf("%s.%s", DBName, FieldKeysTable),
nameColumn: "field_name",
dataTypeColumn: "field_data_type",
dataTypeSelectExpr: "field_data_type",
addBaseFilters: func(sb *sqlbuilder.SelectBuilder) {
sb.Where(sb.E("signal", telemetrytypes.SignalLogs.StringValue()))
sb.Where(sb.E("field_context", telemetrytypes.FieldContextBody.StringValue()))
},
encodeDataType: func(ft telemetrytypes.FieldDataType) string { return ft.StringValue() },
extraOrBranch: func(sb *sqlbuilder.SelectBuilder) string {
if len(parentPaths) == 0 {
return ""
}
names := make([]any, 0, len(parentPaths))
for n := range parentPaths {
names = append(names, n)
}
return sb.And(
sb.In("field_name", names...),
sb.In("field_data_type",
telemetrytypes.FieldDataTypeArrayDynamic.StringValue(),
telemetrytypes.FieldDataTypeArrayJSON.StringValue(),
),
)
},
},
}
for _, arm := range tablesToQuery {
if !arm.shouldQuery {
for _, table := range tablesToQuery {
if !table.shouldQuery {
continue
}
fieldContext := table.fieldContext
// table name based on field context
var tblName string
if fieldContext == telemetrytypes.FieldContextAttribute {
tblName = t.logsDBName + "." + t.logAttributeKeysTblName
} else {
tblName = t.logsDBName + "." + t.logResourceKeysTblName
}
sb := sqlbuilder.Select(
arm.nameColumn+" AS tag_key",
fmt.Sprintf("'%s' AS tag_type", arm.fieldContext.TagType()),
arm.dataTypeSelectExpr+" AS tag_data_type",
fmt.Sprintf("%d AS priority", getPriorityForContext(arm.fieldContext)),
).From(arm.table)
"name AS tag_key",
fmt.Sprintf("'%s' AS tag_type", fieldContext.TagType()),
"lower(datatype) AS tag_data_type", // in logs, we had some historical data with capital and small case
fmt.Sprintf(`%d AS priority`, getPriorityForContext(fieldContext)),
).From(tblName)
arm.addBaseFilters(sb)
var limit int
conds := []string{}
branches := []string{}
for _, sel := range fieldKeySelectors {
if sel.FieldContext != telemetrytypes.FieldContextUnspecified && sel.FieldContext != arm.fieldContext {
for _, fieldKeySelector := range fieldKeySelectors {
// Include this selector if:
// 1. It has unspecified context (matches all tables)
// 2. Its context matches the current table's context
if fieldKeySelector.FieldContext != telemetrytypes.FieldContextUnspecified &&
fieldKeySelector.FieldContext != fieldContext {
continue
}
parts := []string{}
if sel.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
parts = append(parts, sb.E(arm.nameColumn, sel.Name))
// key part of the selector
fieldKeyConds := []string{}
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldKeyConds = append(fieldKeyConds, sb.E("name", fieldKeySelector.Name))
} else {
parts = append(parts, sb.ILike(arm.nameColumn, "%"+escapeForLike(sel.Name)+"%"))
fieldKeyConds = append(fieldKeyConds, sb.ILike("name", "%"+escapeForLike(fieldKeySelector.Name)+"%"))
}
if sel.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
parts = append(parts, sb.E(arm.dataTypeColumn, arm.encodeDataType(sel.FieldDataType)))
// now look at the field data type
if fieldKeySelector.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
fieldKeyConds = append(fieldKeyConds, sb.E("datatype", fieldKeySelector.FieldDataType.TagDataType()))
}
if len(parts) > 0 {
branches = append(branches, sb.And(parts...))
if len(fieldKeyConds) > 0 {
conds = append(conds, sb.And(fieldKeyConds...))
}
limit += fieldKeySelector.Limit
}
if extra := arm.extraOrBranch(sb); extra != "" {
branches = append(branches, extra)
if len(conds) > 0 {
sb.Where(sb.Or(conds...))
}
if len(branches) > 0 {
sb.Where(sb.Or(branches...))
sb.GroupBy("name", "datatype")
if limit == 0 {
limit = 1000
}
sb.GroupBy(arm.nameColumn, arm.dataTypeColumn)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
queries = append(queries, query)
allArgs = append(allArgs, args...)
@@ -593,7 +517,6 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
defer rows.Close()
keys := []*telemetrytypes.TelemetryFieldKey{}
parentTypeCache := make(map[string][]telemetrytypes.FieldDataType)
rowCount := 0
searchTexts := []string{}
@@ -617,17 +540,6 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
if err != nil {
return nil, false, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, ErrFailedToGetLogsKeys.Error())
}
// body keys with ArrayJSON/ArrayDynamic types are internal container types
// used only for JSON access plan building; route to parentTypeCache, not to results
switch fieldDataType {
case telemetrytypes.FieldDataTypeArrayJSON, telemetrytypes.FieldDataTypeArrayDynamic:
if fieldContext == telemetrytypes.FieldContextBody && parentPaths[name] {
parentTypeCache[name] = append(parentTypeCache[name], fieldDataType)
continue
}
}
key, ok := mapOfKeys[name+";"+fieldContext.StringValue()+";"+fieldDataType.StringValue()]
// if there is no materialised column, create a key with the field context and data type
@@ -681,11 +593,13 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
}
}
// enrich body keys with promoted paths, indexes, and JSON access plans
if querybuilder.BodyJSONQueryEnabled {
if err := t.enrichBodyKeys(ctx, keys, parentTypeCache); err != nil {
return nil, false, err
bodyJSONPaths, finished, err := t.buildBodyJSONPaths(ctx, fieldKeySelectors) // LIKE for pattern matching
if err != nil {
t.logger.ErrorContext(ctx, "failed to extract body JSON paths", errors.Attr(err))
}
keys = append(keys, bodyJSONPaths...)
complete = complete && finished
}
if _, err := t.updateColumnEvolutionMetadataForKeys(ctx, keys); err != nil {

View File

@@ -1,13 +1,13 @@
package telemetrymetadata
import otelconst "github.com/SigNoz/signoz-otel-collector/constants"
import otelcollectorconst "github.com/SigNoz/signoz-otel-collector/constants"
const (
DBName = "signoz_metadata"
AttributesMetadataTableName = "distributed_attributes_metadata"
AttributesMetadataLocalTableName = "attributes_metadata"
ColumnEvolutionMetadataTableName = "distributed_column_evolution_metadata"
FieldKeysTable = otelconst.DistributedFieldKeysTable
PathTypesTableName = otelcollectorconst.DistributedPathTypesTable
// Column Evolution table stores promoted paths as (signal, column_name, field_context, field_name); see signoz-otel-collector metadata_migrations.
PromotedPathsTableName = "distributed_column_evolution_metadata"
SkipIndexTableName = "system.data_skipping_indices"

View File

@@ -37,6 +37,7 @@ type TelemetryFieldKey struct {
FieldContext FieldContext `json:"fieldContext,omitzero"`
FieldDataType FieldDataType `json:"fieldDataType,omitzero"`
JSONDataType *JSONDataType `json:"-"`
JSONPlan JSONAccessPlan `json:"-"`
Indexes []JSONDataTypeIndex `json:"-"`
Materialized bool `json:"-"` // refers to promoted in case of body.... fields
@@ -79,11 +80,6 @@ func (f *TelemetryFieldKey) ArrayParentSelectors() []*FieldKeySelector {
return selectors
}
// GetJSONDataType derives the JSONDataType from FieldDataType.
func (f *TelemetryFieldKey) GetJSONDataType() JSONDataType {
return MappingFieldDataTypeToJSONDataType[f.FieldDataType]
}
func (f TelemetryFieldKey) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "name=%s", f.Name)
@@ -96,6 +92,9 @@ func (f TelemetryFieldKey) String() string {
if f.Materialized {
sb.WriteString(",materialized=true")
}
if f.JSONDataType != nil {
fmt.Fprintf(&sb, ",jsondatatype=%s", f.JSONDataType.StringValue())
}
if len(f.Indexes) > 0 {
sb.WriteString(",indexes=[")
for i, index := range f.Indexes {
@@ -118,6 +117,7 @@ func (f TelemetryFieldKey) Text() string {
func (f *TelemetryFieldKey) OverrideMetadataFrom(src *TelemetryFieldKey) {
f.FieldContext = src.FieldContext
f.FieldDataType = src.FieldDataType
f.JSONDataType = src.JSONDataType
f.Indexes = src.Indexes
f.Materialized = src.Materialized
f.JSONPlan = src.JSONPlan

View File

@@ -31,7 +31,7 @@ var (
FieldDataTypeArrayInt64 = FieldDataType{valuer.NewString("[]int64")}
FieldDataTypeArrayNumber = FieldDataType{valuer.NewString("[]number")}
FieldDataTypeArrayJSON = FieldDataType{valuer.NewString("[]json")}
FieldDataTypeArrayObject = FieldDataType{valuer.NewString("[]object")}
FieldDataTypeArrayDynamic = FieldDataType{valuer.NewString("[]dynamic")}
// Map string representations to FieldDataType values
@@ -72,8 +72,6 @@ var (
"[]float64": FieldDataTypeArrayFloat64,
"[]number": FieldDataTypeArrayNumber,
"[]bool": FieldDataTypeArrayBool,
"[]json": FieldDataTypeArrayJSON,
"[]dynamic": FieldDataTypeArrayDynamic,
// c-style array types
"string[]": FieldDataTypeArrayString,
@@ -81,8 +79,6 @@ var (
"float64[]": FieldDataTypeArrayFloat64,
"number[]": FieldDataTypeArrayNumber,
"bool[]": FieldDataTypeArrayBool,
"json[]": FieldDataTypeArrayJSON,
"dynamic[]": FieldDataTypeArrayDynamic,
}
fieldDataTypeToCHDataType = map[FieldDataType]string{

View File

@@ -19,8 +19,7 @@ var (
BranchJSON = JSONAccessBranchType{valuer.NewString("json")}
BranchDynamic = JSONAccessBranchType{valuer.NewString("dynamic")}
CodePlanIndexOutOfBounds = errors.MustNewCode("plan_index_out_of_bounds")
CodePlanFieldDataTypeMissing = errors.MustNewCode("field_data_type_missing")
CodePlanIndexOutOfBounds = errors.MustNewCode("plan_index_out_of_bounds")
)
type JSONColumnMetadata struct {
@@ -44,7 +43,7 @@ type JSONAccessNode struct {
isRoot bool // marked true for only body_v2 and body_promoted
// Precomputed type information (single source of truth)
AvailableTypes []FieldDataType
AvailableTypes []JSONDataType
// Array type branches (Array(JSON) vs Array(Dynamic))
Branches map[JSONAccessBranchType]*JSONAccessNode
@@ -107,7 +106,7 @@ type planBuilder struct {
paths []string // cumulative paths for type cache lookups
segments []string // individual path segments for node names
isPromoted bool
typeCache map[string][]FieldDataType
typeCache map[string][]JSONDataType
}
// buildPlan recursively builds the path plan tree.
@@ -156,22 +155,14 @@ func (pb *planBuilder) buildPlan(index int, parent *JSONAccessNode, isDynArrChil
MaxDynamicPaths: maxPaths,
}
hasJSON := slices.Contains(node.AvailableTypes, FieldDataTypeArrayJSON)
hasDynamic := slices.Contains(node.AvailableTypes, FieldDataTypeArrayDynamic)
if !hasJSON && !hasDynamic {
return nil, errors.NewInternalf(CodePlanFieldDataTypeMissing, "array data types missing for path %s", pathSoFar)
}
hasJSON := slices.Contains(node.AvailableTypes, ArrayJSON)
hasDynamic := slices.Contains(node.AvailableTypes, ArrayDynamic)
// Configure terminal if this is the last part
if isTerminal {
// fielddatatype must not be unspecified else expression can not be generated
if pb.key.FieldDataType == FieldDataTypeUnspecified {
return nil, errors.NewInternalf(CodePlanFieldDataTypeMissing, "field data type is missing for path %s", pathSoFar)
}
node.TerminalConfig = &TerminalConfig{
Key: pb.key,
ElemType: pb.key.GetJSONDataType(),
ElemType: *pb.key.JSONDataType,
}
} else {
var err error
@@ -194,7 +185,7 @@ func (pb *planBuilder) buildPlan(index int, parent *JSONAccessNode, isDynArrChil
// buildJSONAccessPlan builds a tree structure representing the complete JSON path traversal
// that precomputes all possible branches and their types.
func (key *TelemetryFieldKey) SetJSONAccessPlan(columnInfo JSONColumnMetadata, typeCache map[string][]FieldDataType,
func (key *TelemetryFieldKey) SetJSONAccessPlan(columnInfo JSONColumnMetadata, typeCache map[string][]JSONDataType,
) error {
// if path is empty, return nil
if key.Name == "" {

View File

@@ -19,11 +19,11 @@ const (
// ============================================================================
// makeKey creates a TelemetryFieldKey for testing.
func makeKey(name string, dataType FieldDataType, materialized bool) *TelemetryFieldKey {
func makeKey(name string, dataType JSONDataType, materialized bool) *TelemetryFieldKey {
return &TelemetryFieldKey{
Name: name,
FieldDataType: dataType,
Materialized: materialized,
Name: name,
JSONDataType: &dataType,
Materialized: materialized,
}
}
@@ -242,12 +242,12 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
}{
{
name: "Simple path not promoted",
key: makeKey("user.name", FieldDataTypeString, false),
key: makeKey("user.name", String, false),
expectedYAML: fmt.Sprintf(`
- name: user.name
column: %s
availableTypes:
- string
- String
maxDynamicTypes: 16
isTerminal: true
elemType: String
@@ -255,19 +255,19 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
},
{
name: "Simple path promoted",
key: makeKey("user.name", FieldDataTypeString, true),
key: makeKey("user.name", String, true),
expectedYAML: fmt.Sprintf(`
- name: user.name
column: %s
availableTypes:
- string
- String
maxDynamicTypes: 16
isTerminal: true
elemType: String
- name: user.name
column: %s
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -276,7 +276,7 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
},
{
name: "Empty path returns error",
key: makeKey("", FieldDataTypeString, false),
key: makeKey("", String, false),
expectErr: true,
expectedYAML: "",
},
@@ -315,13 +315,13 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: name
availableTypes:
- string
- String
maxDynamicTypes: 8
isTerminal: true
elemType: String
@@ -334,27 +334,27 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
isTerminal: true
elemType: String
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -368,42 +368,42 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: interests
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: entities
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: reviews
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 4
branches:
json:
name: entries
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 2
branches:
json:
name: metadata
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 1
branches:
json:
name: positions
availableTypes:
- "[]json"
- Array(JSON)
branches:
json:
name: name
availableTypes:
- string
- String
isTerminal: true
elemType: String
`, bodyV2Column),
@@ -415,13 +415,13 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: name
availableTypes:
- string
- String
maxDynamicTypes: 8
isTerminal: true
elemType: String
@@ -431,7 +431,7 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := makeKey(tt.path, FieldDataTypeString, false)
key := makeKey(tt.path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
@@ -450,7 +450,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
path := "education[].awards[].type"
t.Run("Non-promoted plan", func(t *testing.T) {
key := makeKey(path, FieldDataTypeString, false)
key := makeKey(path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
@@ -462,27 +462,27 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
isTerminal: true
elemType: String
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -493,7 +493,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
})
t.Run("Promoted plan", func(t *testing.T) {
key := makeKey(path, FieldDataTypeString, true)
key := makeKey(path, String, true)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
@@ -505,27 +505,27 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
isTerminal: true
elemType: String
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -533,22 +533,22 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
maxDynamicPaths: 64
branches:
json:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 4
maxDynamicPaths: 16
isTerminal: true
@@ -556,7 +556,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
dynamic:
name: type
availableTypes:
- string
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
@@ -588,42 +588,42 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
- name: interests
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: entities
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: reviews
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 4
branches:
json:
name: entries
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 2
branches:
json:
name: metadata
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 1
branches:
json:
name: positions
availableTypes:
- "[]json"
- Array(JSON)
branches:
json:
name: name
availableTypes:
- string
- String
isTerminal: true
elemType: String
`, bodyV2Column),
@@ -635,14 +635,14 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: type
availableTypes:
- string
- int64
- String
- Int64
maxDynamicTypes: 8
isTerminal: true
elemType: String
@@ -655,7 +655,7 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
isTerminal: true
elemType: Array(JSON)
@@ -666,12 +666,12 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Choose key type based on path; operator does not affect the tree shape asserted here.
keyType := FieldDataTypeString
keyType := String
switch tt.path {
case "education":
keyType = FieldDataTypeArrayJSON
keyType = ArrayJSON
case "education[].type":
keyType = FieldDataTypeString
keyType = String
}
key := makeKey(tt.path, keyType, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
@@ -692,7 +692,7 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
func TestPlanJSON_TreeStructure(t *testing.T) {
types, _ := TestJSONTypeSet()
path := "education[].awards[].participated[].team[].branch"
key := makeKey(path, FieldDataTypeString, false)
key := makeKey(path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
@@ -704,47 +704,47 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
- name: education
column: %s
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
branches:
json:
name: awards
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 8
branches:
json:
name: participated
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 4
branches:
json:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 2
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 1
isTerminal: true
elemType: String
dynamic:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 8
maxDynamicPaths: 64
isTerminal: true
@@ -752,22 +752,22 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
dynamic:
name: participated
availableTypes:
- "[]dynamic"
- "[]json"
- Array(Dynamic)
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 8
maxDynamicPaths: 64
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 4
maxDynamicPaths: 16
isTerminal: true
@@ -775,14 +775,14 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
dynamic:
name: team
availableTypes:
- "[]json"
- Array(JSON)
maxDynamicTypes: 16
maxDynamicPaths: 256
branches:
json:
name: branch
availableTypes:
- string
- String
maxDynamicTypes: 8
maxDynamicPaths: 64
isTerminal: true

View File

@@ -46,6 +46,14 @@ var MappingStringToJSONDataType = map[string]JSONDataType{
"Array(JSON)": ArrayJSON,
}
var ScalerTypeToArrayType = map[JSONDataType]JSONDataType{
String: ArrayString,
Int64: ArrayInt64,
Float64: ArrayFloat64,
Bool: ArrayBool,
Dynamic: ArrayDynamic,
}
var MappingFieldDataTypeToJSONDataType = map[FieldDataType]JSONDataType{
FieldDataTypeString: String,
FieldDataTypeInt64: Int64,
@@ -55,8 +63,18 @@ var MappingFieldDataTypeToJSONDataType = map[FieldDataType]JSONDataType{
FieldDataTypeArrayString: ArrayString,
FieldDataTypeArrayInt64: ArrayInt64,
FieldDataTypeArrayFloat64: ArrayFloat64,
FieldDataTypeArrayNumber: ArrayFloat64,
FieldDataTypeArrayBool: ArrayBool,
FieldDataTypeArrayDynamic: ArrayDynamic,
FieldDataTypeArrayJSON: ArrayJSON,
}
var MappingJSONDataTypeToFieldDataType = map[JSONDataType]FieldDataType{
String: FieldDataTypeString,
Int64: FieldDataTypeInt64,
Float64: FieldDataTypeFloat64,
Bool: FieldDataTypeBool,
ArrayString: FieldDataTypeArrayString,
ArrayInt64: FieldDataTypeArrayInt64,
ArrayFloat64: FieldDataTypeArrayFloat64,
ArrayBool: FieldDataTypeArrayBool,
ArrayDynamic: FieldDataTypeArrayDynamic,
ArrayJSON: FieldDataTypeArrayObject,
}

View File

@@ -4,69 +4,69 @@ package telemetrytypes
// Test JSON Type Set Data Setup
// ============================================================================
// TestJSONTypeSet returns a map of path->field data types for testing.
// TestJSONTypeSet returns a map of path->types for testing.
// This represents the type information available in the test JSON structure.
func TestJSONTypeSet() (map[string][]FieldDataType, MetadataStore) {
types := map[string][]FieldDataType{
func TestJSONTypeSet() (map[string][]JSONDataType, MetadataStore) {
types := map[string][]JSONDataType{
// ── user (primitives) ─────────────────────────────────────────────
"user.name": {FieldDataTypeString},
"user.permissions": {FieldDataTypeArrayString},
"user.age": {FieldDataTypeInt64, FieldDataTypeString}, // Int64/String ambiguity
"user.height": {FieldDataTypeFloat64},
"user.active": {FieldDataTypeBool}, // Bool — not IndexSupported
"user.name": {String},
"user.permissions": {ArrayString},
"user.age": {Int64, String}, // Int64/String ambiguity
"user.height": {Float64},
"user.active": {Bool}, // Bool — not IndexSupported
// Deeper non-array nesting (a.b.c — no array hops)
"user.address.zip": {FieldDataTypeInt64},
"user.address.zip": {Int64},
// ── education[] ───────────────────────────────────────────────────
// Pattern: x[].y
"education": {FieldDataTypeArrayJSON},
"education[].name": {FieldDataTypeString},
"education[].type": {FieldDataTypeString, FieldDataTypeInt64},
"education[].year": {FieldDataTypeInt64},
"education[].scores": {FieldDataTypeArrayInt64},
"education[].parameters": {FieldDataTypeArrayFloat64, FieldDataTypeArrayDynamic},
"education": {ArrayJSON},
"education[].name": {String},
"education[].type": {String, Int64},
"education[].year": {Int64},
"education[].scores": {ArrayInt64},
"education[].parameters": {ArrayFloat64, ArrayDynamic},
// Pattern: x[].y[]
"education[].awards": {FieldDataTypeArrayDynamic, FieldDataTypeArrayJSON},
"education[].awards": {ArrayDynamic, ArrayJSON},
// Pattern: x[].y[].z
"education[].awards[].name": {FieldDataTypeString},
"education[].awards[].type": {FieldDataTypeString},
"education[].awards[].semester": {FieldDataTypeInt64},
"education[].awards[].name": {String},
"education[].awards[].type": {String},
"education[].awards[].semester": {Int64},
// Pattern: x[].y[].z[]
"education[].awards[].participated": {FieldDataTypeArrayDynamic, FieldDataTypeArrayJSON},
"education[].awards[].participated": {ArrayDynamic, ArrayJSON},
// Pattern: x[].y[].z[].w
"education[].awards[].participated[].members": {FieldDataTypeArrayString},
"education[].awards[].participated[].members": {ArrayString},
// Pattern: x[].y[].z[].w[]
"education[].awards[].participated[].team": {FieldDataTypeArrayJSON},
"education[].awards[].participated[].team": {ArrayJSON},
// Pattern: x[].y[].z[].w[].v
"education[].awards[].participated[].team[].branch": {FieldDataTypeString},
"education[].awards[].participated[].team[].branch": {String},
// ── interests[] ───────────────────────────────────────────────────
"interests": {FieldDataTypeArrayJSON},
"interests[].entities": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries[].metadata": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions": {FieldDataTypeArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions[].name": {FieldDataTypeString},
"interests[].entities[].reviews[].entries[].metadata[].positions[].ratings": {FieldDataTypeArrayInt64, FieldDataTypeArrayString},
"http-events": {FieldDataTypeArrayJSON},
"http-events[].request-info.host": {FieldDataTypeString},
"ids": {FieldDataTypeArrayDynamic},
"interests": {ArrayJSON},
"interests[].entities": {ArrayJSON},
"interests[].entities[].reviews": {ArrayJSON},
"interests[].entities[].reviews[].entries": {ArrayJSON},
"interests[].entities[].reviews[].entries[].metadata": {ArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions": {ArrayJSON},
"interests[].entities[].reviews[].entries[].metadata[].positions[].name": {String},
"interests[].entities[].reviews[].entries[].metadata[].positions[].ratings": {ArrayInt64, ArrayString},
"http-events": {ArrayJSON},
"http-events[].request-info.host": {String},
"ids": {ArrayDynamic},
// ── top-level primitives ──────────────────────────────────────────
"message": {FieldDataTypeString},
"http-status": {FieldDataTypeInt64, FieldDataTypeString}, // hyphen in root key, ambiguous
"message": {String},
"http-status": {Int64, String}, // hyphen in root key, ambiguous
// ── top-level nested objects (no array hops) ───────────────────────
"response.time-taken": {FieldDataTypeFloat64}, // hyphen inside nested key
"response.time-taken": {Float64}, // hyphen inside nested key
}
return types, nil

View File

@@ -5,9 +5,33 @@ import (
"maps"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/cachetypes"
)
// ClickHouse database and table names for trace queries.
const (
TraceDB = "signoz_traces"
TraceTable = "distributed_signoz_index_v3"
TraceSummaryTable = "distributed_trace_summary"
)
// Cache and freshness thresholds.
const (
WaterfallCacheTTL = 5 * time.Minute
FluxInterval = 2 * time.Minute
)
// Windowing constants for span selection.
const (
SpanLimitPerRequest float64 = 500
MaxDepthForSelectedChildren int = 5
MaxLimitToSelectAllSpans uint = 10_000
)
// ErrTraceNotFound is returned when a trace ID has no matching spans in ClickHouse.
var ErrTraceNotFound = errors.NewNotFoundf(errors.CodeNotFound, "trace not found")
// WaterfallRequest is the request body for the v3 waterfall API.
type WaterfallRequest struct {
SelectedSpanID string `json:"selectedSpanId"`
@@ -19,7 +43,6 @@ type WaterfallRequest struct {
type WaterfallResponse struct {
StartTimestampMillis uint64 `json:"startTimestampMillis"`
EndTimestampMillis uint64 `json:"endTimestampMillis"`
DurationNano uint64 `json:"durationNano"`
RootServiceName string `json:"rootServiceName"`
RootServiceEntryPoint string `json:"rootServiceEntryPoint"`
TotalSpansCount uint64 `json:"totalSpansCount"`
@@ -28,6 +51,7 @@ type WaterfallResponse struct {
Spans []*WaterfallSpan `json:"spans"`
HasMissingSpans bool `json:"hasMissingSpans"`
UncollapsedSpans []string `json:"uncollapsedSpans"`
HasMore bool `json:"hasMore"`
}
// Event represents a span event.
@@ -58,16 +82,15 @@ type WaterfallSpan struct {
IsRemote string `json:"is_remote"`
Kind int32 `json:"kind"`
KindString string `json:"kind_string"`
Links string `json:"links"`
Name string `json:"name"`
ParentSpanID string `json:"parent_span_id"`
Resource map[string]string `json:"resource"`
ResponseStatusCode string `json:"response_status_code"`
SpanID string `json:"span_id"`
StatusCode int32 `json:"status_code"`
StatusCode int16 `json:"status_code"`
StatusCodeString string `json:"status_code_string"`
StatusMessage string `json:"status_message"`
Timestamp string `json:"timestamp"`
TimeUnixMilli uint64 `json:"timestamp"`
TraceID string `json:"trace_id"`
TraceState string `json:"trace_state"`
@@ -77,10 +100,7 @@ type WaterfallSpan struct {
HasChildren bool `json:"has_children"`
Level uint64 `json:"level"`
// timeUnixNano is an internal field used for tree building and sorting.
// It is not serialized in the JSON response.
TimeUnixNano uint64 `json:"-"`
// serviceName is an internal field used for service time calculation.
// used only for service time calculation
ServiceName string `json:"-"`
}
@@ -117,7 +137,7 @@ type SpanModel struct {
Flags uint32 `ch:"flags"`
IsRemote string `ch:"is_remote"`
TraceState string `ch:"trace_state"`
StatusCode int32 `ch:"status_code"`
StatusCode int16 `ch:"status_code"`
DBName string `ch:"db_name"`
DBOperation string `ch:"db_operation"`
HTTPMethod string `ch:"http_method"`
@@ -149,7 +169,7 @@ func (item *SpanModel) ToSpan() *WaterfallSpan {
for _, eventStr := range item.Events {
var event Event
if err := json.Unmarshal([]byte(eventStr), &event); err != nil {
continue
continue // skipping malformed events
}
events = append(events, event)
}
@@ -170,7 +190,6 @@ func (item *SpanModel) ToSpan() *WaterfallSpan {
IsRemote: item.IsRemote,
Kind: int32(item.Kind),
KindString: item.SpanKind,
Links: item.References,
Name: item.Name,
ParentSpanID: item.ParentSpanID,
Resource: resources,
@@ -179,11 +198,10 @@ func (item *SpanModel) ToSpan() *WaterfallSpan {
StatusCode: item.StatusCode,
StatusCodeString: item.StatusCodeString,
StatusMessage: item.StatusMessage,
Timestamp: item.TimeUnixNano.Format(time.RFC3339Nano),
TraceID: item.TraceID,
TraceState: item.TraceState,
Children: make([]*WaterfallSpan, 0),
TimeUnixNano: uint64(item.TimeUnixNano.UnixNano()),
TimeUnixMilli: uint64(item.TimeUnixNano.UnixNano() / 1000_000),
ServiceName: item.ServiceName,
}
}
@@ -203,8 +221,8 @@ type OtelSpanRef struct {
RefType string `json:"refType,omitempty"`
}
// WaterfallCache holds pre-processed trace data for caching.
type WaterfallCache struct {
// WaterfallTrace holds pre-processed trace data for caching.
type WaterfallTrace struct {
StartTime uint64 `json:"startTime"`
EndTime uint64 `json:"endTime"`
DurationNano uint64 `json:"durationNano"`
@@ -216,7 +234,7 @@ type WaterfallCache struct {
HasMissingSpans bool `json:"hasMissingSpans"`
}
func (c *WaterfallCache) Clone() cachetypes.Cacheable {
func (c *WaterfallTrace) Clone() cachetypes.Cacheable {
copyOfServiceNameToTotalDurationMap := make(map[string]uint64)
maps.Copy(copyOfServiceNameToTotalDurationMap, c.ServiceNameToTotalDurationMap)
@@ -225,7 +243,7 @@ func (c *WaterfallCache) Clone() cachetypes.Cacheable {
copyOfTraceRoots := make([]*WaterfallSpan, len(c.TraceRoots))
copy(copyOfTraceRoots, c.TraceRoots)
return &WaterfallCache{
return &WaterfallTrace{
StartTime: c.StartTime,
EndTime: c.EndTime,
DurationNano: c.DurationNano,
@@ -238,10 +256,58 @@ func (c *WaterfallCache) Clone() cachetypes.Cacheable {
}
}
func (c *WaterfallCache) MarshalBinary() (data []byte, err error) {
func (c *WaterfallTrace) MarshalBinary() (data []byte, err error) {
return json.Marshal(c)
}
func (c *WaterfallCache) UnmarshalBinary(data []byte) error {
func (c *WaterfallTrace) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
// NewWaterfallTrace constructs a WaterfallTrace from processed span data.
func NewWaterfallTrace(
startTime, endTime, durationNano, totalSpans, totalErrorSpans uint64,
spanIDToSpanNodeMap map[string]*WaterfallSpan,
serviceNameToTotalDurationMap map[string]uint64,
traceRoots []*WaterfallSpan,
hasMissingSpans bool,
) *WaterfallTrace {
return &WaterfallTrace{
StartTime: startTime,
EndTime: endTime,
DurationNano: durationNano,
TotalSpans: totalSpans,
TotalErrorSpans: totalErrorSpans,
SpanIDToSpanNodeMap: spanIDToSpanNodeMap,
ServiceNameToTotalDurationMap: serviceNameToTotalDurationMap,
TraceRoots: traceRoots,
HasMissingSpans: hasMissingSpans,
}
}
// NewWaterfallResponse constructs a WaterfallResponse from processed trace data and selected spans.
func NewWaterfallResponse(
traceData *WaterfallTrace,
selectedSpans []*WaterfallSpan,
uncollapsedSpans []string,
rootServiceName, rootServiceEntryPoint string,
selectAllSpans bool,
) *WaterfallResponse {
serviceDurationsMillis := make(map[string]uint64, len(traceData.ServiceNameToTotalDurationMap))
for svc, dur := range traceData.ServiceNameToTotalDurationMap {
serviceDurationsMillis[svc] = dur / 1_000_000
}
return &WaterfallResponse{
Spans: selectedSpans,
UncollapsedSpans: uncollapsedSpans,
StartTimestampMillis: traceData.StartTime / 1_000_000,
EndTimestampMillis: traceData.EndTime / 1_000_000,
TotalSpansCount: traceData.TotalSpans,
TotalErrorSpansCount: traceData.TotalErrorSpans,
RootServiceName: rootServiceName,
RootServiceEntryPoint: rootServiceEntryPoint,
ServiceNameToTotalDurationMap: serviceDurationsMillis,
HasMissingSpans: traceData.HasMissingSpans,
HasMore: !selectAllSpans,
}
}