Compare commits

..

12 Commits

Author SHA1 Message Date
aks07
4ec3754b65 feat: increase limit 2026-02-26 17:45:21 +05:30
aks07
c36f780964 feat: poc flamegraph 3 2026-02-26 15:57:41 +05:30
aks07
e0e0870e23 feat: poc flamegraph 2 2026-02-26 15:41:15 +05:30
aks07
ea406dac52 feat: poc flamegraph 1 2026-02-26 15:40:36 +05:30
aks07
c6152b33e9 feat: poc flamegraph 2026-02-26 15:37:28 +05:30
Nikhil Soni
ca87244961 feat: increase fg limits and add timestamp boundaries 2026-02-25 18:27:12 +05:30
Nikhil Soni
e41a85febd feat: return all spans for flamegraph under a limit 2026-02-25 11:23:58 +05:30
Nikhil Soni
9d4154a117 fix: add missing filtering for ip address for scalar data (#10264)
* fix: add missing filtering for ip address for scalar data

In domain listing api for external api monitoring,
we have option to filter out the IP address but
it only handles timeseries and raw type data while
domain list handler returns scalar data.

* fix: switch to new derived attributes for ip filtering

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2026-02-25 11:23:58 +05:30
Nikhil Soni
4b9dd63e8c Revert "feat: show spinner on expanding span and fix autoscroll"
This reverts commit 6bd194dfb4.
2026-02-24 19:53:00 +05:30
Nikhil Soni
30e7736a74 feat: add support for returning all the spans
If total spans are less than a max value.
This allows UI to not have to call API
for smaller spans on ever collapse/uncollapse
2026-02-24 19:48:34 +05:30
Nikhil Soni
6bd194dfb4 feat: show spinner on expanding span and fix autoscroll
Two improvemnts are included in this commit:
- Show a loading spiner in UI when a span is expanded
  to signal api call.
- Don't auto scroll the waterfall to bring the selected
  span to centre in case it's expanded or scrolled up
  because it looks very flickering behaviour to user
2026-02-24 15:24:57 +05:30
Nikhil Soni
f27e3cabb2 feat: add support for configurable pre expanded levels
When a span is expanded, now it will return nested children
up to a max depth level
2026-02-24 15:24:57 +05:30
97 changed files with 7603 additions and 7633 deletions

View File

@@ -17,7 +17,5 @@
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
}
}

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.112.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
@@ -213,7 +213,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.142.1
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.112.1
ports:
- "8080:8080" # signoz port
volumes:
@@ -139,7 +139,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.142.1
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -82,12 +82,6 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -99,19 +93,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, metadataexporter, signozmeter]
exporters: [clickhousetraces, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
exporters: [clickhouselogsexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.112.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -204,7 +204,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -229,7 +229,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.112.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -132,7 +132,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -157,7 +157,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -82,12 +82,6 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -99,19 +93,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, metadataexporter, signozmeter]
exporters: [clickhousetraces, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
exporters: [clickhouselogsexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -842,17 +842,6 @@ components:
- temporality
- isMonotonic
type: object
MetrictypesComparisonSpaceAggregationParam:
properties:
operator:
type: string
threshold:
format: double
type: number
required:
- operator
- threshold
type: object
MetrictypesSpaceAggregation:
enum:
- sum
@@ -1149,8 +1138,6 @@ components:
type: object
Querybuildertypesv5MetricAggregation:
properties:
comparisonSpaceAggregationParam:
$ref: '#/components/schemas/MetrictypesComparisonSpaceAggregationParam'
metricName:
type: string
reduceTo:
@@ -2858,186 +2845,6 @@ paths:
summary: Update auth domain
tags:
- authdomains
/api/v1/export_raw_data:
get:
deprecated: false
description: This endpoints allows simple query exporting raw data for traces
and logs
operationId: HandleExportRawDataGET
parameters:
- in: query
name: format
schema:
default: csv
enum:
- csv
- jsonl
type: string
- in: query
name: signal
required: true
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
- deprecated: true
in: query
name: source
schema:
deprecated: true
type: string
- in: query
name: start
schema:
minimum: 0
type: integer
- in: query
name: end
schema:
minimum: 0
type: integer
- in: query
name: limit
schema:
default: 10000
maximum: 50000
minimum: 1
type: integer
- deprecated: true
in: query
name: filter
schema:
deprecated: true
type: string
- content:
application/json:
schema:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
in: query
name: filterExpression
- deprecated: true
in: query
name: columns
schema:
deprecated: true
items:
type: string
type: array
- in: query
name: selectFields
schema:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
- deprecated: true
in: query
name: order_by
schema:
deprecated: true
type: string
- in: query
name: order
schema:
items:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
type: array
responses:
"200":
content:
application/json:
schema:
type: string
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Export raw data
tags:
- logs
- traces
post:
deprecated: false
description: This endpoints allows complex query exporting raw data for traces
and logs
operationId: HandleExportRawDataPOST
parameters:
- in: query
name: format
schema:
default: csv
enum:
- csv
- jsonl
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeRequest'
responses:
"200":
content:
application/json:
schema:
type: string
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Export raw data
tags:
- logs
- traces
/api/v1/fields/keys:
get:
deprecated: false

View File

@@ -123,7 +123,6 @@ if err := router.Handle("/api/v1/things", handler.New(
Description: "This endpoint creates a thing",
Request: new(types.PostableThing),
RequestContentType: "application/json",
RequestQuery: new(types.QueryableThing),
Response: new(types.GettableThing),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
@@ -156,8 +155,6 @@ The `handler.New` function ties the HTTP handler to OpenAPI metadata via `OpenAP
- **Request / RequestContentType**:
- `Request` is a Go type that describes the request body or form.
- `RequestContentType` is usually `"application/json"` or `"application/x-www-form-urlencoded"` (for callbacks like SAML).
- **RequestQuery**:
- `RequestQuery` is a Go type that descirbes query url params.
- **RequestExamples**: An array of `handler.OpenAPIExample` that provide concrete request payloads in the generated spec. See [Adding request examples](#adding-request-examples) below.
- **Response / ResponseContentType**:
- `Response` is the Go type for the successful response payload.

View File

@@ -308,15 +308,3 @@ export const PublicDashboardPage = Loadable(
/* webpackChunkName: "Public Dashboard Page" */ 'pages/PublicDashboard'
),
);
export const AlertTypeSelectionPage = Loadable(
() =>
import(
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
),
);
export const MeterExplorerPage = Loadable(
() =>
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
);

View File

@@ -1,10 +1,12 @@
import { RouteProps } from 'react-router-dom';
import ROUTES from 'constants/routes';
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import {
AlertHistory,
AlertOverview,
AlertTypeSelectionPage,
AllAlertChannels,
AllErrors,
ApiMonitoring,
@@ -27,8 +29,6 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
Onboarding,
@@ -399,28 +399,28 @@ const routes: AppRoutes[] = [
{
path: ROUTES.MESSAGING_QUEUES_KAFKA,
exact: true,
component: MessagingQueuesMainPage,
component: MessagingQueues,
key: 'MESSAGING_QUEUES_KAFKA',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
exact: true,
component: MessagingQueuesMainPage,
component: MessagingQueues,
key: 'MESSAGING_QUEUES_CELERY_TASK',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_OVERVIEW,
exact: true,
component: MessagingQueuesMainPage,
component: MessagingQueues,
key: 'MESSAGING_QUEUES_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL,
exact: true,
component: MessagingQueuesMainPage,
component: MessagingQueues,
key: 'MESSAGING_QUEUES_KAFKA_DETAIL',
isPrivate: true,
},
@@ -463,21 +463,21 @@ const routes: AppRoutes[] = [
{
path: ROUTES.METER,
exact: true,
component: MeterExplorerPage,
component: MeterExplorer,
key: 'METER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER,
exact: true,
component: MeterExplorerPage,
component: MeterExplorer,
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorerPage,
component: MeterExplorer,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},

View File

@@ -20,11 +20,8 @@ import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
HandleExportRawDataGETParams,
HandleExportRawDataPOSTParams,
ListPromotedAndIndexedPaths200,
PromotetypesPromotePathDTO,
Querybuildertypesv5QueryRangeRequestDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -32,206 +29,6 @@ type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoints allows simple query exporting raw data for traces and logs
* @summary Export raw data
*/
export const handleExportRawDataGET = (
params?: HandleExportRawDataGETParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/export_raw_data`,
method: 'GET',
params,
signal,
});
};
export const getHandleExportRawDataGETQueryKey = (
params?: HandleExportRawDataGETParams,
) => {
return [`/api/v1/export_raw_data`, ...(params ? [params] : [])] as const;
};
export const getHandleExportRawDataGETQueryOptions = <
TData = Awaited<ReturnType<typeof handleExportRawDataGET>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
params?: HandleExportRawDataGETParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof handleExportRawDataGET>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getHandleExportRawDataGETQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof handleExportRawDataGET>>
> = ({ signal }) => handleExportRawDataGET(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof handleExportRawDataGET>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type HandleExportRawDataGETQueryResult = NonNullable<
Awaited<ReturnType<typeof handleExportRawDataGET>>
>;
export type HandleExportRawDataGETQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Export raw data
*/
export function useHandleExportRawDataGET<
TData = Awaited<ReturnType<typeof handleExportRawDataGET>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
params?: HandleExportRawDataGETParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof handleExportRawDataGET>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getHandleExportRawDataGETQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Export raw data
*/
export const invalidateHandleExportRawDataGET = async (
queryClient: QueryClient,
params?: HandleExportRawDataGETParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getHandleExportRawDataGETQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoints allows complex query exporting raw data for traces and logs
* @summary Export raw data
*/
export const handleExportRawDataPOST = (
querybuildertypesv5QueryRangeRequestDTO: BodyType<Querybuildertypesv5QueryRangeRequestDTO>,
params?: HandleExportRawDataPOSTParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/export_raw_data`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: querybuildertypesv5QueryRangeRequestDTO,
params,
signal,
});
};
export const getHandleExportRawDataPOSTMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
> => {
const mutationKey = ['handleExportRawDataPOST'];
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 handleExportRawDataPOST>>,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
}
> = (props) => {
const { data, params } = props ?? {};
return handleExportRawDataPOST(data, params);
};
return { mutationFn, ...mutationOptions };
};
export type HandleExportRawDataPOSTMutationResult = NonNullable<
Awaited<ReturnType<typeof handleExportRawDataPOST>>
>;
export type HandleExportRawDataPOSTMutationBody = BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
export type HandleExportRawDataPOSTMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Export raw data
*/
export const useHandleExportRawDataPOST = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
> => {
const mutationOptions = getHandleExportRawDataPOSTMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoints promotes and indexes paths
* @summary Promote and index paths

View File

@@ -1006,18 +1006,6 @@ export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
unit: string;
}
export interface MetrictypesComparisonSpaceAggregationParamDTO {
/**
* @type string
*/
operator: string;
/**
* @type number
* @format double
*/
threshold: number;
}
export enum MetrictypesSpaceAggregationDTO {
sum = 'sum',
avg = 'avg',
@@ -1379,7 +1367,6 @@ export interface Querybuildertypesv5LogAggregationDTO {
}
export interface Querybuildertypesv5MetricAggregationDTO {
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
/**
* @type string
*/
@@ -2732,76 +2719,6 @@ export type DeleteAuthDomainPathParameters = {
export type UpdateAuthDomainPathParameters = {
id: string;
};
export type HandleExportRawDataGETParams = {
/**
* @enum csv,jsonl
* @type string
* @description undefined
*/
format?: HandleExportRawDataGETFormat;
/**
* @enum logs,traces
* @type string
* @description undefined
*/
source?: HandleExportRawDataGETSource;
/**
* @type integer
* @minimum 0
* @description undefined
*/
start?: number;
/**
* @type integer
* @minimum 0
* @description undefined
*/
end?: number;
/**
* @type integer
* @maximum 50000
* @minimum 1
* @description undefined
*/
limit?: number;
/**
* @type string
* @description undefined
*/
filter?: string;
/**
* @type array
* @description undefined
*/
columns?: string[];
/**
* @type string
* @description undefined
*/
order_by?: string;
};
export enum HandleExportRawDataGETFormat {
csv = 'csv',
jsonl = 'jsonl',
}
export enum HandleExportRawDataGETSource {
logs = 'logs',
traces = 'traces',
}
export type HandleExportRawDataPOSTParams = {
/**
* @enum csv,jsonl
* @type string
* @description undefined
*/
format?: HandleExportRawDataPOSTFormat;
};
export enum HandleExportRawDataPOSTFormat {
csv = 'csv',
jsonl = 'jsonl',
}
export type GetFieldsKeysParams = {
/**
* @description undefined

View File

@@ -86,13 +86,8 @@ function LogDetailInner({
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on drawer content, overlays, or portal elements
if (
target.closest('[data-log-detail-ignore="true"]') ||
target.closest('.cm-tooltip-autocomplete') ||
target.closest('.drawer-popover') ||
target.closest('.query-status-popover')
) {
// Don't close if clicking on explicitly ignored regions
if (target.closest('[data-log-detail-ignore="true"]')) {
return;
}
@@ -405,11 +400,7 @@ function LogDetailInner({
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip
title={removeEscapeCharacters(log?.body)}
placement="left"
mouseLeaveDelay={0}
>
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
@@ -475,7 +466,6 @@ function LogDetailInner({
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
mouseLeaveDelay={0}
>
<Button
className="action-btn"
@@ -491,7 +481,6 @@ function LogDetailInner({
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
mouseLeaveDelay={0}
>
<Button
className="action-btn"

View File

@@ -27,11 +27,7 @@ function AddToQueryHOC({
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
<Popover
overlayClassName="drawer-popover"
placement="top"
content={popOverContent}
>
<Popover placement="top" content={popOverContent}>
{children}
</Popover>
</div>

View File

@@ -32,7 +32,6 @@ function CopyClipboardHOC({
<span onClick={onClick} role="presentation" tabIndex={-1}>
<Popover
placement="top"
overlayClassName="drawer-popover"
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
>
{children}

View File

@@ -21,7 +21,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '90rem',
maxWidth: '60rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@@ -1328,10 +1328,7 @@ function QuerySearch({
)}
<div className="query-where-clause-editor-container">
<Tooltip
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
placement="left"
>
<Tooltip title={getTooltipContent()} placement="left">
<a
href="https://signoz.io/docs/userguide/search-syntax/"
target="_blank"

View File

@@ -14,10 +14,16 @@ interface ITimelineV2Props {
startTimestamp: number;
endTimestamp: number;
timelineHeight: number;
offsetTimestamp: number;
}
function TimelineV2(props: ITimelineV2Props): JSX.Element {
const { startTimestamp, endTimestamp, timelineHeight } = props;
const {
startTimestamp,
endTimestamp,
timelineHeight,
offsetTimestamp,
} = props;
const [intervals, setIntervals] = useState<Interval[]>([]);
const [ref, { width }] = useMeasure<HTMLDivElement>();
const isDarkMode = useIsDarkMode();
@@ -30,8 +36,10 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element {
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
const intervalisedSpread = (spread / minIntervals) * 1.0;
setIntervals(getIntervals(intervalisedSpread, spread));
}, [startTimestamp, endTimestamp, width]);
const intervals = getIntervals(intervalisedSpread, spread, offsetTimestamp);
setIntervals(intervals);
}, [startTimestamp, endTimestamp, width, offsetTimestamp]);
if (endTimestamp < startTimestamp) {
console.error(

View File

@@ -64,6 +64,71 @@ export const resolveTimeFromInterval = (
export function getIntervals(
intervalSpread: number,
baseSpread: number,
offsetTimestamp: number, // ms offset from trace start (e.g. viewStart - traceStart)
): Interval[] {
const integerPartString = intervalSpread.toString().split('.')[0];
const integerPartLength = integerPartString.length;
const intervalSpreadNormalized =
intervalSpread < 1.0
? intervalSpread
: Math.floor(Number(integerPartString) / 10 ** (integerPartLength - 1)) *
10 ** (integerPartLength - 1);
let intervalUnit = INTERVAL_UNITS[0];
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {
const standardInterval = INTERVAL_UNITS[idx];
if (intervalSpread * standardInterval.multiplier >= 1) {
intervalUnit = INTERVAL_UNITS[idx];
break;
}
}
const intervals: Interval[] = [
{
// ✅ start label should reflect window start offset (relative to trace start)
label: `${toFixed(
resolveTimeFromInterval(offsetTimestamp, intervalUnit),
2,
)}${intervalUnit.name}`,
percentage: 0,
},
];
let tempBaseSpread = baseSpread;
let elapsedIntervals = 0;
while (tempBaseSpread && intervals.length < 20) {
let intervalTime: number;
if (tempBaseSpread <= 1.5 * intervalSpreadNormalized) {
intervalTime = elapsedIntervals + tempBaseSpread;
tempBaseSpread = 0;
} else {
intervalTime = elapsedIntervals + intervalSpreadNormalized;
tempBaseSpread -= intervalSpreadNormalized;
}
elapsedIntervals = intervalTime;
// ✅ label time = window offset + elapsed time inside window
const labelTime = offsetTimestamp + intervalTime;
intervals.push({
label: `${toFixed(resolveTimeFromInterval(labelTime, intervalUnit), 2)}${
intervalUnit.name
}`,
percentage: (intervalTime / baseSpread) * 100,
});
}
return intervals;
}
export function getIntervalsOld(
intervalSpread: number,
baseSpread: number,
offsetTimestamp: number,
): Interval[] {
const integerPartString = intervalSpread.toString().split('.')[0];
const integerPartLength = integerPartString.length;
@@ -106,9 +171,10 @@ export function getIntervals(
}
elapsedIntervals = intervalTime;
const interval: Interval = {
label: `${toFixed(resolveTimeFromInterval(intervalTime, intervalUnit), 2)}${
intervalUnit.name
}`,
label: `${toFixed(
resolveTimeFromInterval(intervalTime + offsetTimestamp, intervalUnit),
2,
)}${intervalUnit.name}`,
percentage: (intervalTime / baseSpread) * 100,
};
intervals.push(interval);

View File

@@ -117,7 +117,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
const { currentVersion } = useSelector<AppState, AppReducer>(
const { latestVersion } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
@@ -213,9 +213,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
},
{
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
getChangelogByVersion(currentVersion, changelogForTenant),
queryKey: ['getChangelogByVersion', currentVersion, changelogForTenant],
enabled: isLoggedIn && Boolean(currentVersion),
getChangelogByVersion(latestVersion, changelogForTenant),
queryKey: ['getChangelogByVersion', latestVersion, changelogForTenant],
enabled: isLoggedIn && Boolean(latestVersion),
},
]);
@@ -226,7 +226,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
!changelog &&
!getChangelogByVersionResponse.isLoading &&
isLoggedIn &&
Boolean(currentVersion)
Boolean(latestVersion)
) {
getChangelogByVersionResponse.refetch();
}
@@ -237,9 +237,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
let timer: ReturnType<typeof setTimeout>;
if (
isCloudUserVal &&
Boolean(currentVersion) &&
Boolean(latestVersion) &&
seenChangelogVersion != null &&
currentVersion !== seenChangelogVersion &&
latestVersion !== seenChangelogVersion &&
daysSinceAccountCreation > MIN_ACCOUNT_AGE_FOR_CHANGELOG && // Show to only users older than 2 weeks
!isWorkspaceAccessRestricted
) {
@@ -255,7 +255,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isCloudUserVal,
currentVersion,
latestVersion,
seenChangelogVersion,
toggleChangelogModal,
isWorkspaceAccessRestricted,

View File

@@ -9,7 +9,6 @@ import {
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
@@ -32,9 +31,6 @@ function DashboardVariableSelection(): JSX.Element | null {
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
(state) => state.dashboardId,
);
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
@@ -100,28 +96,6 @@ function DashboardVariableSelection(): JSX.Element | null {
updateUrlVariable(name || id, value);
}
// Synchronously update the external store with the new variable value so that
// child variables see the updated parent value when they refetch, rather than
// waiting for setSelectedDashboard → useEffect → updateDashboardVariablesStore.
const updatedVariables = { ...dashboardVariables };
if (updatedVariables[id]) {
updatedVariables[id] = {
...updatedVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (updatedVariables[name]) {
updatedVariables[name] = {
...updatedVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
setSelectedDashboard((prev) => {
if (prev) {
const oldVariables = { ...prev?.data.variables };
@@ -156,12 +130,10 @@ function DashboardVariableSelection(): JSX.Element | null {
return prev;
});
// Cascade: enqueue query-type descendants for refetching.
// Safe to call synchronously now that the store already has the updated value.
// Cascade: enqueue query-type descendants for refetching
enqueueDescendantsOfVariable(name);
},
[
dashboardId,
dashboardVariables,
updateLocalStorageDashboardVariables,
updateUrlVariable,

View File

@@ -5,7 +5,7 @@ import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQ
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useVariableFetchState } from 'hooks/dashboard/useVariableFetchState';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import { isArray, isEmpty } from 'lodash-es';
import { isArray, isEmpty, isString } from 'lodash-es';
import { AppState } from 'store/reducers';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -54,7 +54,7 @@ function QueryVariableInput({
onChange,
onDropdownVisibleChange,
handleClear,
getDefaultValue,
applyDefaultIfNeeded,
} = useDashboardVariableSelectHelper({
variableData,
optionsData,
@@ -68,93 +68,81 @@ function QueryVariableInput({
try {
setErrorMessage(null);
// This is just a check given the previously undefined typed name prop. Not significant
// This will be changed when we change the schema
// TODO: @AshwinBhatkal Perses
if (!variableData.name) {
return;
}
// if the response is not an array, premature return
if (
!variablesRes?.variableValues ||
!Array.isArray(variablesRes?.variableValues)
variablesRes?.variableValues &&
Array.isArray(variablesRes?.variableValues)
) {
return;
}
const sortedNewOptions = sortValues(
variablesRes.variableValues,
variableData.sort,
);
const sortedOldOptions = sortValues(optionsData, variableData.sort);
// if options are the same as before, no need to update state or check for selected value validity
// ! selectedValue needs to be set in the first pass though, as options are initially empty array and we need to apply default if needed
// Expecatation is that when oldOptions are not empty, then there is always some selectedValue
if (areArraysEqual(sortedNewOptions, sortedOldOptions)) {
return;
}
setOptionsData(sortedNewOptions);
let isSelectedValueMissingInNewOptions = false;
// Check if currently selected value(s) are present in the new options list
if (isArray(variableData.selectedValue)) {
isSelectedValueMissingInNewOptions = variableData.selectedValue.some(
(val) => !sortedNewOptions.includes(val),
const newOptionsData = sortValues(
variablesRes?.variableValues,
variableData.sort,
);
} else if (
variableData.selectedValue &&
!sortedNewOptions.includes(variableData.selectedValue)
) {
isSelectedValueMissingInNewOptions = true;
}
// If multi-select with ALL option enabled, and ALL is currently selected, we want to maintain that state and select all new options
// This block does not depend on selected value because of ALL and also because we would only come here if options are different from the previous
if (
variableData.multiSelect &&
variableData.showALLOption &&
variableData.allSelected &&
isSelectedValueMissingInNewOptions
) {
onValueUpdate(variableData.name, variableData.id, sortedNewOptions, true);
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(sortedNewOptions.map((option) => option.toString()));
}
return;
}
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
let valueNotInList = false;
const value = variableData.selectedValue;
let allSelected = false;
if (isArray(variableData.selectedValue)) {
variableData.selectedValue.forEach((val) => {
if (!newOptionsData.includes(val)) {
valueNotInList = true;
}
});
} else if (
isString(variableData.selectedValue) &&
!newOptionsData.includes(variableData.selectedValue)
) {
valueNotInList = true;
}
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
sortedNewOptions.length > 0 &&
Array.isArray(selectedValue) &&
sortedNewOptions.every((option) => selectedValue.includes(option));
}
if (variableData.name && (valueNotInList || variableData.allSelected)) {
if (
variableData.allSelected &&
variableData.multiSelect &&
variableData.showALLOption
) {
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(
variableData.name,
variableData.id,
newOptionsData,
true,
);
}
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
} else {
const defaultValue = getDefaultValue(sortedNewOptions);
if (defaultValue !== undefined) {
onValueUpdate(
variableData.name,
variableData.id,
defaultValue,
allSelected,
);
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(newOptionsData.map((option) => option.toString()));
}
} else {
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
newOptionsData.length > 0 &&
Array.isArray(selectedValue) &&
newOptionsData.every((option) => selectedValue.includes(option));
}
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
}
}
}
setOptionsData(newOptionsData);
// Apply default if no value is selected (e.g., new variable, first load)
applyDefaultIfNeeded(newOptionsData);
}
}
} catch (e) {
@@ -167,7 +155,7 @@ function QueryVariableInput({
onValueUpdate,
tempSelection,
setTempSelection,
getDefaultValue,
applyDefaultIfNeeded,
],
);

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { act, render } from '@testing-library/react';
import * as dashboardVariablesStoreModule from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
dashboardVariablesStore,
setDashboardVariablesStore,
@@ -11,7 +10,6 @@ import {
IDashboardVariablesStoreState,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
initializeVariableFetchStore,
} from 'providers/Dashboard/store/variableFetchStore';
@@ -19,17 +17,6 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import DashboardVariableSelection from '../DashboardVariableSelection';
// Mutable container to capture the onValueUpdate callback from VariableItem
const mockVariableItemCallbacks: {
onValueUpdate?: (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
) => void;
} = {};
// Mock providers/Dashboard/Dashboard
const mockSetSelectedDashboard = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
@@ -69,14 +56,10 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests
// Mock VariableItem to avoid rendering complexity
jest.mock('../VariableItem', () => ({
__esModule: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: (props: any): JSX.Element => {
mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate;
return <div data-testid="variable-item" />;
},
default: (): JSX.Element => <div data-testid="variable-item" />,
}));
function createVariable(
@@ -217,162 +200,4 @@ describe('DashboardVariableSelection', () => {
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
});
describe('onValueUpdate', () => {
let updateStoreSpy: jest.SpyInstance;
beforeEach(() => {
resetStore();
jest.clearAllMocks();
// Real implementation pass-through — we just want to observe calls
updateStoreSpy = jest.spyOn(
dashboardVariablesStoreModule,
'updateDashboardVariablesStore',
);
});
afterEach(() => {
updateStoreSpy.mockRestore();
});
it('updates dashboardVariablesStore synchronously before enqueueDescendantsOfVariable', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
const callOrder: string[] = [];
updateStoreSpy.mockImplementation(() => {
callOrder.push('updateDashboardVariablesStore');
});
(enqueueDescendantsOfVariable as jest.Mock).mockImplementation(() => {
callOrder.push('enqueueDescendantsOfVariable');
});
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(callOrder).toEqual([
'updateDashboardVariablesStore',
'enqueueDescendantsOfVariable',
]);
});
it('passes updated variable value to dashboardVariablesStore', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
selectedValue: 'staging',
}),
},
});
render(<DashboardVariableSelection />);
// Clear spy calls that happened during setup/render
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
dashboardId: 'dash-1',
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: 'production',
allSelected: false,
}),
}),
}),
);
});
it('calls enqueueDescendantsOfVariable synchronously without a timer', () => {
jest.useFakeTimers();
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
// Must be called immediately — no timer advancement needed
expect(enqueueDescendantsOfVariable).toHaveBeenCalledWith('env');
jest.useRealTimers();
});
it('propagates allSelected and haveCustomValuesSelected to the store', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
multiSelect: true,
showALLOption: true,
}),
},
});
render(<DashboardVariableSelection />);
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
['production', 'staging'],
true,
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: ['production', 'staging'],
allSelected: true,
haveCustomValuesSelected: false,
}),
}),
}),
);
});
});
});

View File

@@ -1,275 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryClient, QueryClientProvider } from 'react-query';
import { act, render, waitFor } from '@testing-library/react';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { variableFetchStore } from 'providers/Dashboard/store/variableFetchStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import QueryVariableInput from '../QueryVariableInput';
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
function createTestQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: { retry: false, refetchOnWindowFocus: false },
},
});
}
function Wrapper({
children,
queryClient,
}: {
children: React.ReactNode;
queryClient: QueryClient;
}): JSX.Element {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
function createVariable(
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable {
return {
id: 'env-id',
name: 'env',
description: '',
type: 'QUERY',
sort: 'DISABLED',
showALLOption: false,
multiSelect: false,
order: 0,
queryValue: 'SELECT env FROM table',
...overrides,
};
}
/** Put the named variable into 'loading' state so useQuery fires on mount */
function setVariableLoading(name: string): void {
variableFetchStore.update((draft) => {
draft.states[name] = 'loading';
draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1;
});
}
function resetFetchStore(): void {
variableFetchStore.set(() => ({
states: {},
lastUpdated: {},
cycleIds: {},
}));
}
describe('QueryVariableInput - getOptions logic', () => {
const mockOnValueUpdate = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
resetFetchStore();
});
afterEach(() => {
resetFetchStore();
});
it('applies default value (first option) when selectedValue is empty on first load', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging', 'dev'] },
});
const variable = createVariable({ selectedValue: undefined });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'production', // first option by default
false,
);
});
});
it('keeps existing selectedValue when it is present in new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({ selectedValue: 'staging' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'staging',
false,
);
});
});
it('selects all new options when allSelected=true and value is missing from new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({
selectedValue: ['old-env'],
allSelected: true,
multiSelect: true,
showALLOption: true,
});
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
['production', 'staging'],
true,
);
});
});
it('does not call onValueUpdate a second time when options have not changed', async () => {
const mockQueryFn = jest.fn().mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
(dashboardVariablesQuery as jest.Mock).mockImplementation(mockQueryFn);
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
// Wait for first fetch and onValueUpdate call
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
});
mockOnValueUpdate.mockClear();
// Trigger a second fetch cycle with the same API response
act(() => {
variableFetchStore.update((draft) => {
draft.states['env'] = 'revalidating';
draft.cycleIds['env'] = (draft.cycleIds['env'] || 0) + 1;
});
});
// Wait for second query to fire
await waitFor(() => {
expect(mockQueryFn).toHaveBeenCalledTimes(2);
});
// Options are unchanged, so onValueUpdate must not fire again
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not call onValueUpdate when API returns a non-array response', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: null },
});
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(dashboardVariablesQuery).toHaveBeenCalled();
});
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not fire the query when variableData.name is empty', () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production'] },
});
// Variable with no name — useVariableFetchState will be called with ''
// and the query key will have an empty name, leaving it disabled
const variable = createVariable({ name: '' });
// Note: we do NOT put it in 'loading' state since name is empty
// (no variableFetchStore entry for '' means isVariableFetching=false)
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
expect(dashboardVariablesQuery).not.toHaveBeenCalled();
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
});

View File

@@ -46,9 +46,6 @@ interface UseDashboardVariableSelectHelperReturn {
applyDefaultIfNeeded: (
overrideOptions?: (string | number | boolean)[],
) => void;
getDefaultValue: (
overrideOptions?: (string | number | boolean)[],
) => string | string[] | undefined;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -251,6 +248,5 @@ export function useDashboardVariableSelectHelper({
defaultValue,
onChange,
applyDefaultIfNeeded,
getDefaultValue,
};
}

View File

@@ -121,23 +121,9 @@ function BodyTitleRenderer({
return (
<TitleWrapper onClick={handleNodeClick}>
{typeof value !== 'object' && (
<span
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
onMouseDown={(e): void => e.preventDefault()}
>
<Dropdown
menu={menu}
trigger={['click']}
dropdownRender={(originNode): React.ReactNode => (
<div data-log-detail-ignore="true">{originNode}</div>
)}
>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
</span>
<Dropdown menu={menu} trigger={['click']}>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
)}
{title.toString()}{' '}
{!parentIsArray && typeof value !== 'object' && (

View File

@@ -13,7 +13,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<span className="field-renderer-container">
{dataType && newField && logType ? (
<>
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
<Tooltip placement="left" title={newField}>
<Typography.Text ellipsis className="label">
{newField}{' '}
</Typography.Text>

View File

@@ -46,7 +46,7 @@ function Overview({
handleChangeSelectedView,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
true,
);

View File

@@ -245,7 +245,7 @@ function TableView({
<Typography.Text>{renderedField}</Typography.Text>
{traceId && (
<Tooltip title="Inspect in Trace" mouseLeaveDelay={0}>
<Tooltip title="Inspect in Trace">
<Button
className="periscope-btn"
onClick={(

View File

@@ -1,34 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { getColorsForSeverityLabels, isRedLike } from '../utils';
describe('getColorsForSeverityLabels', () => {
it('should return slate for blank labels', () => {
expect(getColorsForSeverityLabels('', 0)).toBe(Color.BG_SLATE_300);
expect(getColorsForSeverityLabels(' ', 0)).toBe(Color.BG_SLATE_300);
});
it('should return correct colors for known severity variants', () => {
expect(getColorsForSeverityLabels('INFO', 0)).toBe(Color.BG_ROBIN_600);
expect(getColorsForSeverityLabels('ERROR', 0)).toBe(Color.BG_CHERRY_600);
expect(getColorsForSeverityLabels('WARN', 0)).toBe(Color.BG_AMBER_600);
expect(getColorsForSeverityLabels('DEBUG', 0)).toBe(Color.BG_AQUA_600);
expect(getColorsForSeverityLabels('TRACE', 0)).toBe(Color.BG_FOREST_600);
expect(getColorsForSeverityLabels('FATAL', 0)).toBe(Color.BG_SAKURA_600);
});
it('should return non-red colors for unrecognized labels at any index', () => {
for (let i = 0; i < 30; i++) {
const color = getColorsForSeverityLabels('4', i);
expect(isRedLike(color)).toBe(false);
}
});
it('should return non-red colors for numeric severity text', () => {
const numericLabels = ['1', '2', '4', '9', '13', '17', '21'];
numericLabels.forEach((label) => {
const color = getColorsForSeverityLabels(label, 0);
expect(isRedLike(color)).toBe(false);
});
});
});

View File

@@ -1,16 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme';
import { colors } from 'lib/getRandomColor';
// Function to determine if a color is "red-like" based on its RGB values
export function isRedLike(hex: string): boolean {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return r > 180 && r > g * 1.4 && r > b * 1.4;
}
const SAFE_FALLBACK_COLORS = colors.filter((c) => !isRedLike(c));
const SEVERITY_VARIANT_COLORS: Record<string, string> = {
TRACE: Color.BG_FOREST_600,
Trace: Color.BG_FOREST_500,
@@ -76,13 +67,8 @@ export function getColorsForSeverityLabels(
label: string,
index: number,
): string {
const trimmed = label.trim();
if (!trimmed) {
return Color.BG_SLATE_300;
}
const variantColor = SEVERITY_VARIANT_COLORS[trimmed];
// Check if we have a direct mapping for this severity variant
const variantColor = SEVERITY_VARIANT_COLORS[label.trim()];
if (variantColor) {
return variantColor;
}
@@ -117,8 +103,5 @@ export function getColorsForSeverityLabels(
return Color.BG_SAKURA_500;
}
return (
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
Color.BG_SLATE_400
);
return colors[index % colors.length] || themeColors.red;
}

View File

@@ -111,19 +111,23 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
),
(index: number, log: Record<string, unknown>): JSX.Element => {
return (
<div key={log.id as string}>
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
</div>
);
},
[
tableColumns,
onSetActiveLog,

View File

@@ -0,0 +1,88 @@
# Flamegraph Canvas POC Notes
## Overview
This document tracks the proof-of-concept (POC) implementation of a canvas-based flamegraph rendering system, replacing the previous DOM-based approach.
## Implementation Status
### ✅ Completed Features
1. **Canvas-based Rendering**
- Replaced DOM elements (`react-virtuoso` and `div.span-item`) with a single HTML5 Canvas
- Implemented `drawFlamegraph` function to render all spans as rectangles on canvas
- Added device pixel ratio (DPR) support for crisp rendering
2. **Time-window-based Zoom**
- Replaced pixel-based zoom/pan with time-window-based approach (`viewStartTs`, `viewEndTs`)
- Prevents pixelation by redrawing from data with new time bounds
- Zoom anchors to cursor position
- Horizontal zoom works correctly (min: 1/100th of trace, max: full trace)
3. **Drag to Pan**
- Implemented drag-to-pan functionality for navigating the canvas
- Differentiates between click (span selection) and drag (panning) based on distance moved
- Prevents unwanted window zoom
4. **Minimap with 2D Navigation**
- Canvas-based minimap showing density histogram (time × levels)
- 2D brush overlay for both horizontal (time) and vertical (levels) navigation
- Draggable brush to pan both dimensions
- Bidirectional synchronization between main canvas and minimap
5. **Timeline Synchronization**
- `TimelineV2` component synchronized with visible time window
- Updates correctly during zoom and pan operations
6. **Hit Testing**
- Implemented span rectangle tracking for click detection
- Tooltip on hover
- Span selection via click
### ❌ Known Issues / Not Working
1. **Vertical Zoom - NOT WORKING**
- **Status**: Attempted implementation but not functioning correctly
- **Issue**: When horizontal zoom reaches maximum (full trace width), vertical zoom cannot continue to zoom out further
- **Attempted Solution**: Added `rowHeightScale` state to control vertical row spacing, but the implementation does not work as expected
- **Impact**: Users cannot fully zoom out vertically to see all levels when horizontal zoom is at maximum
- **Next Steps**: Needs further investigation and alternative approach
2. **Timeline Scale Alignment - NOT WORKING PROPERLY**
- **Status**: Issue identified but not fully resolved
- **Issue**: The timeline scale does not align properly when dragging/panning the canvas. The timeline aligns correctly during zoom operations, but not during drag/pan operations.
- **Impact**: Timeline may show incorrect time values while dragging the canvas
- **Attempted Solution**: Used refs (`viewStartTsRef`, `viewEndTsRef`) to track current time window and incremental delta calculation, but issue persists
- **Next Steps**: Needs further investigation to ensure timeline stays synchronized during all interaction types
### 🔄 Pending / Future Work
1. **Performance Optimization**
- Consider adding an interaction layer (separate canvas on top) for better performance
- Optimize rendering for large traces
2. **Code Quality**
- Reduce cognitive complexity of `drawFlamegraph` function (currently 26, target: 15)
- Reduce cognitive complexity of `drawMinimap` function (currently 30, target: 15)
3. **Additional Features**
- Keyboard shortcuts for navigation
- Better zoom controls
- Export functionality
## Technical Details
### Key Files Modified
- `frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx` - Main rendering component
- `frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss` - Styles for canvas and minimap
- `frontend/src/components/TimelineV2/TimelineV2.tsx` - Timeline synchronization
### Key Concepts
- **Time-window-based zoom**: Instead of scaling canvas bitmap, redraw from data with new time bounds
- **Device Pixel Ratio**: Render at DPR resolution for crisp display on high-DPI screens
- **2D Minimap**: Shows density heatmap across both time (horizontal) and levels (vertical) dimensions
- **Brush Navigation**: Draggable rectangle overlay for panning both dimensions
## Notes
- This is a POC implementation - code quality and optimization can be improved after validation
- Some linting warnings (cognitive complexity) are acceptable for POC phase
- All changes should be validated before production use

View File

@@ -1,3 +1,5 @@
// @ts-nocheck
/* eslint-disable */
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
@@ -13,8 +15,13 @@ import { Span } from 'types/api/trace/getTraceV2';
import { TraceFlamegraphStates } from './constants';
import Error from './TraceFlamegraphStates/Error/Error';
import NoData from './TraceFlamegraphStates/NoData/NoData';
import Success from './TraceFlamegraphStates/Success/Success';
// import Success from './TraceFlamegraphStates/Success/SuccessV2';
// import Success from './TraceFlamegraphStates/Success/SuccessV3_without_minimap_best';
import Success from './TraceFlamegraphStates/Success/Success_zoom';
// import Success from './TraceFlamegraphStates/Success/Success_zoom_api';
// import Success from './TraceFlamegraphStates/Success/SuccessCursor';
// import Success from './TraceFlamegraphStates/Success/Success';
import './PaginatedTraceFlamegraph.styles.scss';
interface ITraceFlamegraphProps {
@@ -38,7 +45,6 @@ function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState<string>(
urlQuery.get('spanId') || '',
);
useEffect(() => {
setFirstSpanAtFetchLevel(urlQuery.get('spanId') || '');
}, [urlQuery]);
@@ -46,6 +52,9 @@ function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
const { data, isFetching, error } = useGetTraceFlamegraph({
traceId,
selectedSpanId: firstSpanAtFetchLevel,
limit: 100001,
// boundaryStartTsMilli: 0,
// boundarEndTsMilli: 10000,
});
// get the current state of trace flamegraph based on the API lifecycle

View File

@@ -3,6 +3,11 @@
overflow-x: hidden;
overflow-y: auto;
&.trace-flamegraph-canvas {
overflow: hidden;
position: relative;
}
.trace-flamegraph-virtuoso {
overflow-x: hidden;

View File

@@ -1,3 +1,6 @@
// @ts-nocheck
/* eslint-disable */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import {
@@ -23,6 +26,12 @@ import { toFixed } from 'utils/toFixed';
import './Success.styles.scss';
// Constants for rendering
const ROW_HEIGHT = 24; // 18px height + 6px padding
const SPAN_BAR_HEIGHT = 12;
const EVENT_DOT_SIZE = 6;
const SPAN_BAR_Y_OFFSET = 3; //
interface ITraceMetadata {
startTime: number;
endTime: number;
@@ -35,6 +44,14 @@ interface ISuccessProps {
traceMetadata: ITraceMetadata;
selectedSpan: Span | undefined;
}
interface SpanRect {
span: FlamegraphSpan;
x: number;
y: number;
width: number;
height: number;
level: number;
}
function Success(props: ISuccessProps): JSX.Element {
const {
@@ -48,6 +65,28 @@ function Success(props: ISuccessProps): JSX.Element {
const history = useHistory();
const isDarkMode = useIsDarkMode();
const virtuosoRef = useRef<VirtuosoHandle>(null);
const baseCanvasRef = useRef<HTMLCanvasElement>(null);
const interactionCanvasRef = useRef<HTMLCanvasElement>(null);
const minimapRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
console.log('spans.length', spans.length);
// Calculate total canvas height. this is coming less
const totalHeight = spans.length * ROW_HEIGHT;
// Build a flat array of span rectangles for hit testing.
// consider per level buckets to improve hit testing
const spanRects = useRef<SpanRect[]>([]);
// Time window state (instead of zoom/pan in pixel space)
const [viewStartTs, setViewStartTs] = useState<number>(
traceMetadata.startTime,
);
const [viewEndTs, setViewEndTs] = useState<number>(traceMetadata.endTime);
const [scrollTop, setScrollTop] = useState<number>(0);
const [hoveredSpanId, setHoveredSpanId] = useState<string>('');
const renderSpanLevel = useCallback(
(_: number, spans: FlamegraphSpan[]): JSX.Element => (
@@ -157,16 +196,305 @@ function Success(props: ISuccessProps): JSX.Element {
});
}, [firstSpanAtFetchLevel, spans]);
// Draw a single event dot
const drawEventDot = useCallback(
(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
isError: boolean,
): void => {
// could be optimized:
// ctx.beginPath();
// ctx.moveTo(x, y - size/2);
// ctx.lineTo(x + size/2, y);
// ctx.lineTo(x, y + size/2);
// ctx.lineTo(x - size/2, y);
// ctx.closePath();
// ctx.fill();
// ctx.stroke();
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.PI / 4); // 45 degrees
if (isError) {
ctx.fillStyle = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
ctx.strokeStyle = isDarkMode ? 'rgb(185, 28, 28)' : 'rgb(153, 27, 27)';
} else {
ctx.fillStyle = isDarkMode ? 'rgb(14, 165, 233)' : 'rgb(6, 182, 212)';
ctx.strokeStyle = isDarkMode ? 'rgb(2, 132, 199)' : 'rgb(8, 145, 178)';
}
ctx.lineWidth = 1;
ctx.fillRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.strokeRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.restore();
},
[isDarkMode],
);
// Get CSS color value from color string or CSS variable
// const getColorValue = useCallback((color: string): string => {
// // if (color.startsWith('var(')) {
// // // For CSS variables, we need to get computed value
// // const tempDiv = document.createElement('div');
// // tempDiv.style.color = color;
// // document.body.appendChild(tempDiv);
// // const computedColor = window.getComputedStyle(tempDiv).color;
// // document.body.removeChild(tempDiv);
// // return computedColor;
// // }
// return color;
// }, []);
// Get span color based on service, error state, and selection
// separate this when introducing interaction canvas
const getSpanColor = useCallback(
(span: FlamegraphSpan): string => {
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
if (span.hasError) {
color = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
}
// else {
// color = getColorValue(color);
// }
// Apply selection/hover highlight
//hover/selection highlight in getSpanColor forces base redraw. clipping necessary.
if (selectedSpan?.spanId === span.spanId || hoveredSpanId === span.spanId) {
const colorObj = Color(color);
color = isDarkMode
? colorObj.lighten(0.7).hex()
: colorObj.darken(0.7).hex();
}
return color;
},
[isDarkMode, selectedSpan, hoveredSpanId],
);
// Draw a single span and its events
const drawSpan = useCallback(
(
ctx: CanvasRenderingContext2D,
span: FlamegraphSpan,
x: number,
y: number,
width: number,
levelIndex: number,
spanRectsArray: SpanRect[],
): void => {
const color = getSpanColor(span); // do not depend on hover/clicks
const spanY = y + SPAN_BAR_Y_OFFSET;
// Draw span rectangle
ctx.fillStyle = color;
ctx.beginPath();
// see if we can avoid roundRect as it is performance intensive
ctx.roundRect(x, spanY, width, SPAN_BAR_HEIGHT, 6);
ctx.fill();
// Store rect for hit testing
// consider per level buckets to improve hit testing
// So hover can:
// compute level from y
// search only within that row
spanRectsArray.push({
span,
x,
y: spanY,
width,
height: SPAN_BAR_HEIGHT,
level: levelIndex,
});
// Draw events
// think about optimizing this.
// if span is too small to draw events, skip drawing events???
span.event?.forEach((event) => {
const eventTimeMs = event.timeUnixNano / 1e6;
const eventOffsetPercent =
((eventTimeMs - span.timestamp) / (span.durationNano / 1e6)) * 100;
const clampedOffset = Math.max(1, Math.min(eventOffsetPercent, 99));
const eventX = x + (clampedOffset / 100) * width;
const eventY = spanY + SPAN_BAR_HEIGHT / 2;
// LOD guard: skip events if span too narrow
// if (width < EVENT_DOT_SIZE) {
// return;
// }
drawEventDot(ctx, eventX, eventY, event.isError);
});
},
[getSpanColor, drawEventDot],
);
const drawFlamegraph = useCallback(() => {
const canvas = baseCanvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const dpr = window.devicePixelRatio || 1;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const timeSpan = viewEndTs - viewStartTs;
if (timeSpan <= 0) {
return;
}
const cssWidth = canvas.width / dpr;
// ---- Vertical clipping window ----
const viewportHeight = container.clientHeight;
const overscan = 4;
const firstLevel = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - overscan);
const visibleLevelCount =
Math.ceil(viewportHeight / ROW_HEIGHT) + 2 * overscan;
const lastLevel = Math.min(spans.length - 1, firstLevel + visibleLevelCount);
// ---- Clear only visible region (recommended) ----
const clearTop = firstLevel * ROW_HEIGHT;
const clearHeight = (lastLevel - firstLevel + 1) * ROW_HEIGHT;
ctx.clearRect(0, clearTop, cssWidth, clearHeight);
const spanRectsArray: SpanRect[] = [];
// ---- Draw only visible levels ----
for (let levelIndex = firstLevel; levelIndex <= lastLevel; levelIndex++) {
const levelSpans = spans[levelIndex];
if (!levelSpans) {
continue;
}
const y = levelIndex * ROW_HEIGHT;
for (let i = 0; i < levelSpans.length; i++) {
const span = levelSpans[i];
const spanStartMs = span.timestamp;
const spanEndMs = span.timestamp + span.durationNano / 1e6;
// Time culling (already correct)
if (spanEndMs < viewStartTs || spanStartMs > viewEndTs) {
continue;
}
const leftOffset = ((spanStartMs - viewStartTs) / timeSpan) * cssWidth;
const rightEdge = ((spanEndMs - viewStartTs) / timeSpan) * cssWidth;
let width = rightEdge - leftOffset;
// Clamp to visible x-range
if (leftOffset < 0) {
width += leftOffset;
if (width <= 0) {
continue;
}
}
if (rightEdge > cssWidth) {
width = cssWidth - Math.max(0, leftOffset);
if (width <= 0) {
continue;
}
}
// Optional: minimum 1px width
if (width > 0 && width < 1) {
width = 1;
}
drawSpan(
ctx,
span,
Math.max(0, leftOffset),
y,
width,
levelIndex,
spanRectsArray,
);
}
}
spanRects.current = spanRectsArray;
}, [spans, viewStartTs, viewEndTs, scrollTop, drawSpan]);
// Handle canvas resize with device pixel ratio
useEffect(() => {
const canvas = baseCanvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const updateCanvasSize = (): void => {
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// Set CSS size
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${totalHeight}px`;
// Set actual pixel size (accounting for DPR)
// Only update if size actually changed to prevent unnecessary redraws
const newWidth = Math.floor(rect.width * dpr);
const newHeight = Math.floor(totalHeight * dpr);
if (canvas.width !== newWidth || canvas.height !== newHeight) {
canvas.width = newWidth;
canvas.height = newHeight;
// Redraw with current time window (preserves zoom/pan)
drawFlamegraph();
}
};
const resizeObserver = new ResizeObserver(updateCanvasSize);
resizeObserver.observe(container);
// Initial size
updateCanvasSize();
// Handle DPR changes (e.g., moving window between screens)
const handleDPRChange = (): void => {
updateCanvasSize();
};
window
.matchMedia('(resolution: 1dppx)')
.addEventListener('change', handleDPRChange);
return (): void => {
resizeObserver.disconnect();
window
.matchMedia('(resolution: 1dppx)')
.removeEventListener('change', handleDPRChange);
};
}, [drawFlamegraph, totalHeight]);
// Re-draw when data changes
useEffect(() => {
drawFlamegraph();
}, [drawFlamegraph]);
return (
<>
<div className="trace-flamegraph">
<Virtuoso
ref={virtuosoRef}
className="trace-flamegraph-virtuoso"
data={spans}
itemContent={renderSpanLevel}
rangeChanged={handleRangeChanged}
/>
<div ref={containerRef} className="trace-flamegraph trace-flamegraph-canvas">
<canvas ref={baseCanvasRef}></canvas>
</div>
<TimelineV2
startTimestamp={traceMetadata.startTime}

View File

@@ -0,0 +1,890 @@
// @ts-nocheck
/* eslint-disable */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { Button } from 'antd';
import Color from 'color';
import TimelineV2 from 'components/TimelineV2/TimelineV2';
import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
import { Span } from 'types/api/trace/getTraceV2';
import './Success.styles.scss';
interface ITraceMetadata {
startTime: number;
endTime: number;
}
interface ISuccessProps {
spans: FlamegraphSpan[][];
firstSpanAtFetchLevel: string;
setFirstSpanAtFetchLevel: Dispatch<SetStateAction<string>>;
traceMetadata: ITraceMetadata;
selectedSpan: Span | undefined;
}
// Constants for rendering
const ROW_HEIGHT = 24; // 18px height + 6px padding
const SPAN_BAR_HEIGHT = 12;
const EVENT_DOT_SIZE = 6;
const SPAN_BAR_Y_OFFSET = 3; // Center the 12px bar in 18px row
interface SpanRect {
span: FlamegraphSpan;
x: number;
y: number;
width: number;
height: number;
level: number;
}
function Success(props: ISuccessProps): JSX.Element {
const {
spans,
setFirstSpanAtFetchLevel,
traceMetadata,
firstSpanAtFetchLevel,
selectedSpan,
} = props;
const { search } = useLocation();
const history = useHistory();
const isDarkMode = useIsDarkMode();
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [hoveredSpanId, setHoveredSpanId] = useState<string>('');
const [tooltipContent, setTooltipContent] = useState<{
content: string;
x: number;
y: number;
} | null>(null);
const [scrollTop, setScrollTop] = useState<number>(0);
// Time window state (instead of zoom/pan in pixel space)
const [viewStartTs, setViewStartTs] = useState<number>(
traceMetadata.startTime,
);
const [viewEndTs, setViewEndTs] = useState<number>(traceMetadata.endTime);
const [isSpacePressed, setIsSpacePressed] = useState<boolean>(false);
// Refs to avoid stale state during rapid wheel events and dragging
const viewStartRef = useRef(viewStartTs);
const viewEndRef = useRef(viewEndTs);
useEffect(() => {
viewStartRef.current = viewStartTs;
viewEndRef.current = viewEndTs;
}, [viewStartTs, viewEndTs]);
// Drag state in refs to avoid re-renders during drag
const isDraggingRef = useRef(false);
const dragStartRef = useRef<{ x: number; y: number } | null>(null);
const dragDistanceRef = useRef(0);
const suppressClickRef = useRef(false);
// Scroll ref to avoid recreating getCanvasPointer on every scroll
const scrollTopRef = useRef(0);
useEffect(() => {
scrollTopRef.current = scrollTop;
}, [scrollTop]);
// Build a flat array of span rectangles for hit testing
const spanRects = useRef<SpanRect[]>([]);
// Get span color based on service, error state, and selection
const getSpanColor = useCallback(
(span: FlamegraphSpan): string => {
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
if (span.hasError) {
color = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
}
// Apply selection/hover highlight
if (selectedSpan?.spanId === span.spanId || hoveredSpanId === span.spanId) {
const colorObj = Color(color);
color = isDarkMode
? colorObj.lighten(0.7).hex()
: colorObj.darken(0.7).hex();
}
return color;
},
[isDarkMode, selectedSpan, hoveredSpanId],
);
// Draw a single event dot
const drawEventDot = useCallback(
(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
isError: boolean,
): void => {
// could be optimized:
// ctx.beginPath();
// ctx.moveTo(x, y - size/2);
// ctx.lineTo(x + size/2, y);
// ctx.lineTo(x, y + size/2);
// ctx.lineTo(x - size/2, y);
// ctx.closePath();
// ctx.fill();
// ctx.stroke();
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.PI / 4); // 45 degrees
if (isError) {
ctx.fillStyle = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
ctx.strokeStyle = isDarkMode ? 'rgb(185, 28, 28)' : 'rgb(153, 27, 27)';
} else {
ctx.fillStyle = isDarkMode ? 'rgb(14, 165, 233)' : 'rgb(6, 182, 212)';
ctx.strokeStyle = isDarkMode ? 'rgb(2, 132, 199)' : 'rgb(8, 145, 178)';
}
ctx.lineWidth = 1;
ctx.fillRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.strokeRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.restore();
},
[isDarkMode],
);
// Draw a single span and its events
const drawSpan = useCallback(
(
ctx: CanvasRenderingContext2D,
span: FlamegraphSpan,
x: number,
y: number,
width: number,
levelIndex: number,
spanRectsArray: SpanRect[],
): void => {
const color = getSpanColor(span); // do not depend on hover/clicks
const spanY = y + SPAN_BAR_Y_OFFSET;
// Draw span rectangle
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(x, spanY, width, SPAN_BAR_HEIGHT, 6);
ctx.fill();
// Store rect for hit testing
// consider per level buckets to improve hit testing
// So hover can:
// compute level from y
// search only within that row
spanRectsArray.push({
span,
x,
y: spanY,
width,
height: SPAN_BAR_HEIGHT,
level: levelIndex,
});
// Draw events
// think about optimizing this.
// if span is too small to draw events, skip drawing events???
span.event?.forEach((event) => {
const eventTimeMs = event.timeUnixNano / 1e6;
const eventOffsetPercent =
((eventTimeMs - span.timestamp) / (span.durationNano / 1e6)) * 100;
const clampedOffset = Math.max(1, Math.min(eventOffsetPercent, 99));
const eventX = x + (clampedOffset / 100) * width;
const eventY = spanY + SPAN_BAR_HEIGHT / 2;
drawEventDot(ctx, eventX, eventY, event.isError);
});
},
[getSpanColor, drawEventDot],
);
// Draw the flamegraph on canvas
const drawFlamegraph = useCallback(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const dpr = window.devicePixelRatio || 1;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const timeSpan = viewEndTs - viewStartTs;
if (timeSpan <= 0) {
return;
}
const cssWidth = canvas.width / dpr;
// ---- Vertical clipping window ----
const viewportHeight = container.clientHeight;
const overscan = 4;
const firstLevel = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - overscan);
const visibleLevelCount =
Math.ceil(viewportHeight / ROW_HEIGHT) + 2 * overscan;
const lastLevel = Math.min(spans.length - 1, firstLevel + visibleLevelCount);
// ---- Clear only visible region (recommended) ----
const clearTop = firstLevel * ROW_HEIGHT;
const clearHeight = (lastLevel - firstLevel + 1) * ROW_HEIGHT;
ctx.clearRect(0, clearTop, cssWidth, clearHeight);
const spanRectsArray: SpanRect[] = [];
// ---- Draw only visible levels ----
for (let levelIndex = firstLevel; levelIndex <= lastLevel; levelIndex++) {
const levelSpans = spans[levelIndex];
if (!levelSpans) {
continue;
}
const y = levelIndex * ROW_HEIGHT;
for (let i = 0; i < levelSpans.length; i++) {
const span = levelSpans[i];
const spanStartMs = span.timestamp;
const spanEndMs = span.timestamp + span.durationNano / 1e6;
// Time culling (already correct)
if (spanEndMs < viewStartTs || spanStartMs > viewEndTs) {
continue;
}
const leftOffset = ((spanStartMs - viewStartTs) / timeSpan) * cssWidth;
const rightEdge = ((spanEndMs - viewStartTs) / timeSpan) * cssWidth;
let width = rightEdge - leftOffset;
// Clamp to visible x-range
if (leftOffset < 0) {
width += leftOffset;
if (width <= 0) {
continue;
}
}
if (rightEdge > cssWidth) {
width = cssWidth - Math.max(0, leftOffset);
if (width <= 0) {
continue;
}
}
// Optional: minimum 1px width
if (width > 0 && width < 1) {
width = 1;
}
drawSpan(
ctx,
span,
Math.max(0, leftOffset),
y,
width,
levelIndex,
spanRectsArray,
);
}
}
spanRects.current = spanRectsArray;
}, [spans, viewStartTs, viewEndTs, scrollTop, drawSpan]);
// Calculate total canvas height
const totalHeight = spans.length * ROW_HEIGHT;
console.log('time: ', {
start: traceMetadata.startTime,
end: traceMetadata.endTime,
});
// Initialize time window when trace metadata changes (only if not already set)
useEffect(() => {
// Only reset if we're at the default view (full trace)
// This prevents resetting zoom/pan when metadata updates
if (
viewStartTs === traceMetadata.startTime &&
viewEndTs === traceMetadata.endTime
) {
// Already at default, no need to update
return;
}
// Only reset if the trace bounds have actually changed significantly
const currentSpan = viewEndTs - viewStartTs;
const fullSpan = traceMetadata.endTime - traceMetadata.startTime;
// If we're zoomed in, preserve the zoom level relative to new bounds
if (currentSpan < fullSpan * 0.99) {
// We're zoomed in, adjust the window proportionally
const ratio = currentSpan / fullSpan;
const newSpan = (traceMetadata.endTime - traceMetadata.startTime) * ratio;
const center = (viewStartTs + viewEndTs) / 2;
const newStart = Math.max(
traceMetadata.startTime,
Math.min(center - newSpan / 2, traceMetadata.endTime - newSpan),
);
setViewStartTs(newStart);
setViewEndTs(newStart + newSpan);
} else {
// We're at full view, reset to new full view
setViewStartTs(traceMetadata.startTime);
setViewEndTs(traceMetadata.endTime);
}
}, [traceMetadata.startTime, traceMetadata.endTime, viewStartTs, viewEndTs]);
// Handle canvas resize with device pixel ratio
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const updateCanvasSize = (): void => {
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// Set CSS size
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${totalHeight}px`;
// Set actual pixel size (accounting for DPR)
// Only update if size actually changed to prevent unnecessary redraws
const newWidth = Math.floor(rect.width * dpr);
const newHeight = Math.floor(totalHeight * dpr);
if (canvas.width !== newWidth || canvas.height !== newHeight) {
canvas.width = newWidth;
canvas.height = newHeight;
// Redraw with current time window (preserves zoom/pan)
drawFlamegraph();
}
};
const resizeObserver = new ResizeObserver(updateCanvasSize);
resizeObserver.observe(container);
// Initial size
updateCanvasSize();
// Handle DPR changes (e.g., moving window between screens)
const handleDPRChange = (): void => {
updateCanvasSize();
};
window
.matchMedia('(resolution: 1dppx)')
.addEventListener('change', handleDPRChange);
return (): void => {
resizeObserver.disconnect();
window
.matchMedia('(resolution: 1dppx)')
.removeEventListener('change', handleDPRChange);
};
}, [drawFlamegraph, totalHeight]);
// Re-draw when data changes
useEffect(() => {
drawFlamegraph();
}, [drawFlamegraph]);
// Find span at given canvas coordinates
const findSpanAtPosition = useCallback((x: number, y: number):
| SpanRect
| undefined => {
return spanRects.current.find(
(spanRect) =>
x >= spanRect.x &&
x <= spanRect.x + spanRect.width &&
y >= spanRect.y &&
y <= spanRect.y + spanRect.height,
);
}, []);
// Utility to convert client coordinates to CSS canvas coordinates
const getCanvasPointer = useCallback((clientX: number, clientY: number): {
cssX: number;
cssY: number;
cssWidth: number;
} | null => {
const canvas = canvasRef.current;
if (!canvas) {
return null;
}
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const cssWidth = canvas.width / dpr;
const cssX = (clientX - rect.left) * (cssWidth / rect.width);
const cssY = clientY - rect.top + scrollTopRef.current;
return { cssX, cssY, cssWidth };
}, []);
// Handle mouse move for hover and dragging
const handleMouseMove = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const rect = canvas.getBoundingClientRect();
console.log('event', { clientX: event.clientX, clientY: event.clientY });
// ---- Dragging (pan in time space) ----
if (isDraggingRef.current && dragStartRef.current) {
const deltaX = event.clientX - dragStartRef.current.x;
const deltaY = event.clientY - dragStartRef.current.y;
console.log('delta', { deltaY, deltaX });
const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
dragDistanceRef.current = totalDistance;
const timeSpan = viewEndRef.current - viewStartRef.current;
const deltaTime = (deltaX / rect.width) * timeSpan;
const newStart = viewStartRef.current - deltaTime;
const clampedStart = Math.max(
traceMetadata.startTime,
Math.min(newStart, traceMetadata.endTime - timeSpan),
);
const clampedEnd = clampedStart + timeSpan;
setViewStartTs(clampedStart);
setViewEndTs(clampedEnd);
dragStartRef.current = {
x: event.clientX,
y: event.clientY,
};
return;
}
// ---- Hover ----
const pointer = getCanvasPointer(event.clientX, event.clientY);
if (!pointer) {
return;
}
const { cssX, cssY } = pointer;
const hoveredSpan = findSpanAtPosition(cssX, cssY);
if (hoveredSpan) {
setHoveredSpanId(hoveredSpan.span.spanId);
setTooltipContent({
content: hoveredSpan.span.name,
x: event.clientX,
y: event.clientY,
});
canvas.style.cursor = 'pointer';
} else {
setHoveredSpanId('');
setTooltipContent(null);
// Set cursor based on space key state when not hovering
canvas.style.cursor = isSpacePressed ? 'grab' : 'default';
}
},
[findSpanAtPosition, traceMetadata, getCanvasPointer, isSpacePressed],
);
// Handle key down for space key
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.code === 'Space') {
event.preventDefault();
setIsSpacePressed(true);
}
};
const handleKeyUp = (event: KeyboardEvent): void => {
if (event.code === 'Space') {
event.preventDefault();
setIsSpacePressed(false);
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
const handleMouseDown = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
if (event.button !== 0) {
return;
} // left click only
event.preventDefault();
isDraggingRef.current = true;
dragStartRef.current = {
x: event.clientX,
y: event.clientY,
};
dragDistanceRef.current = 0;
const canvas = canvasRef.current;
if (canvas) {
canvas.style.cursor = 'grabbing';
}
},
[],
);
const handleMouseUp = useCallback(() => {
const wasDrag = dragDistanceRef.current > 5;
suppressClickRef.current = wasDrag; // 👈 key fix: suppress click after drag
isDraggingRef.current = false;
dragStartRef.current = null;
dragDistanceRef.current = 0;
const canvas = canvasRef.current;
if (canvas) {
canvas.style.cursor = 'grab';
}
return wasDrag;
}, []);
const handleMouseLeave = useCallback(() => {
isOverFlamegraphRef.current = false;
setHoveredSpanId('');
setTooltipContent(null);
isDraggingRef.current = false;
dragStartRef.current = null;
dragDistanceRef.current = 0;
const canvas = canvasRef.current;
if (canvas) {
canvas.style.cursor = 'grab';
}
}, []);
const handleClick = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
// Prevent click after drag
if (suppressClickRef.current) {
suppressClickRef.current = false; // reset after suppressing once
return;
}
const pointer = getCanvasPointer(event.clientX, event.clientY);
if (!pointer) {
return;
}
const { cssX, cssY } = pointer;
const clickedSpan = findSpanAtPosition(cssX, cssY);
if (!clickedSpan) {
return;
}
const searchParams = new URLSearchParams(search);
const currentSpanId = searchParams.get('spanId');
if (currentSpanId !== clickedSpan.span.spanId) {
searchParams.set('spanId', clickedSpan.span.spanId);
history.replace({ search: searchParams.toString() });
}
},
[search, history, findSpanAtPosition, getCanvasPointer],
);
const isOverFlamegraphRef = useRef(false);
useEffect(() => {
const onWheel = (e: WheelEvent) => {
// Pinch zoom on trackpads often comes as ctrl+wheel
if (isOverFlamegraphRef.current && e.ctrlKey) {
e.preventDefault(); // stops browser zoom
}
};
// capture:true ensures we intercept early
window.addEventListener('wheel', onWheel, { passive: false, capture: true });
return () => {
window.removeEventListener(
'wheel',
onWheel as any,
{ capture: true } as any,
);
};
}, []);
const wheelDeltaRef = useRef(0);
const rafRef = useRef<number | null>(null);
const lastCursorXRef = useRef(0);
const lastCssWidthRef = useRef(1);
const lastIsPinchRef = useRef(false);
const applyWheelZoom = useCallback(() => {
rafRef.current = null;
const cssWidth = lastCssWidthRef.current || 1;
const cursorX = lastCursorXRef.current;
const fullSpan = traceMetadata.endTime - traceMetadata.startTime;
const oldSpan = viewEndRef.current - viewStartRef.current;
// ✅ Different intensity for pinch vs scroll
const zoomIntensityScroll = 0.0015;
const zoomIntensityPinch = 0.01; // pinch needs stronger response
const zoomIntensity = lastIsPinchRef.current
? zoomIntensityPinch
: zoomIntensityScroll;
const deltaY = wheelDeltaRef.current;
wheelDeltaRef.current = 0;
// ✅ Smooth zoom using delta magnitude
const zoomFactor = Math.exp(deltaY * zoomIntensity);
const newSpan = oldSpan * zoomFactor;
console.log('newSpan', { cssWidth, newSpan, zoomFactor, oldSpan });
// ✅ Better minSpan clamp (absolute + pixel-based)
const absoluteMinSpan = 5; // ms
const pixelMinSpan = fullSpan / cssWidth; // ~1px of time
const minSpan = Math.max(absoluteMinSpan, pixelMinSpan);
const maxSpan = fullSpan;
const clampedSpan = Math.max(minSpan, Math.min(maxSpan, newSpan));
// ✅ Anchor preserving zoom (same as your original logic)
const cursorRatio = Math.max(0, Math.min(cursorX / cssWidth, 1));
const anchorTs = viewStartRef.current + cursorRatio * oldSpan;
const newViewStart = anchorTs - cursorRatio * clampedSpan;
const finalStart = Math.max(
traceMetadata.startTime,
Math.min(newViewStart, traceMetadata.endTime - clampedSpan),
);
const finalEnd = finalStart + clampedSpan;
console.log('finalStart', finalStart);
console.log('finalEnd', finalEnd);
setViewStartTs(finalStart);
setViewEndTs(finalEnd);
}, [traceMetadata]);
const handleWheel = useCallback(
(event: React.WheelEvent<HTMLCanvasElement>) => {
event.preventDefault();
const pointer = getCanvasPointer(event.clientX, event.clientY);
if (!pointer) {
return;
}
console.log('pointer', pointer);
const { cssX: cursorX, cssWidth } = pointer;
// ✅ Detect pinch on Chrome/Edge: ctrlKey true for trackpad pinch
lastIsPinchRef.current = event.ctrlKey;
lastCssWidthRef.current = cssWidth;
lastCursorXRef.current = cursorX;
// ✅ Accumulate deltas; apply once per frame
wheelDeltaRef.current += event.deltaY;
if (rafRef.current == null) {
rafRef.current = requestAnimationFrame(applyWheelZoom);
}
},
[applyWheelZoom, getCanvasPointer],
);
// Reset zoom and pan
const handleResetZoom = useCallback(() => {
setViewStartTs(traceMetadata.startTime);
setViewEndTs(traceMetadata.endTime);
}, [traceMetadata]);
// Handle scroll for pagination
const handleScroll = useCallback(
(event: React.UIEvent<HTMLDivElement>): void => {
const target = event.currentTarget;
setScrollTop(target.scrollTop);
// Pagination logic
if (spans.length < 50) {
return;
}
const scrollPercentage = target.scrollTop / target.scrollHeight;
const totalLevels = spans.length;
if (scrollPercentage === 0 && spans[0]?.[0]?.level !== 0) {
setFirstSpanAtFetchLevel(spans[0][0].spanId);
}
if (scrollPercentage >= 0.95 && spans[totalLevels - 1]?.[0]?.spanId) {
setFirstSpanAtFetchLevel(spans[totalLevels - 1][0].spanId);
}
},
[spans, setFirstSpanAtFetchLevel],
);
// Auto-scroll to selected span
useEffect(() => {
if (!firstSpanAtFetchLevel || !containerRef.current) {
return;
}
const levelIndex = spans.findIndex(
(level) => level[0]?.spanId === firstSpanAtFetchLevel,
);
if (levelIndex !== -1) {
const targetScroll = levelIndex * ROW_HEIGHT;
containerRef.current.scrollTop = targetScroll;
setScrollTop(targetScroll);
}
}, [firstSpanAtFetchLevel, spans]);
return (
<>
<div
ref={containerRef}
className="trace-flamegraph trace-flamegraph-canvas"
onScroll={handleScroll}
>
{(viewStartTs !== traceMetadata.startTime ||
viewEndTs !== traceMetadata.endTime) && (
<Button
className="flamegraph-reset-zoom"
size="small"
onClick={handleResetZoom}
title="Reset zoom and pan"
>
Reset View
</Button>
)}
<canvas
ref={canvasRef}
style={{
display: 'block',
width: '100%',
height: `${totalHeight}px`,
}}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={() => (isOverFlamegraphRef.current = true)}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
onWheel={handleWheel}
onContextMenu={(e): void => e.preventDefault()}
/>
{tooltipContent && (
<div
style={{
position: 'fixed',
left: `${tooltipContent.x + 10}px`,
top: `${tooltipContent.y - 10}px`,
pointerEvents: 'none',
zIndex: 1000,
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
color: isDarkMode ? '#ffffff' : '#000000',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
}}
>
{tooltipContent.content}
</div>
)}
</div>
<TimelineV2
startTimestamp={viewStartTs}
endTimestamp={viewEndTs}
offsetTimestamp={viewStartTs - traceMetadata.startTime}
timelineHeight={22}
/>
</>
);
}
export default Success;
// on drag on click is getting registered as a click
// zoom and scale not matching
// check minimap logic
// use
// const scrollTopRef = useRef(scrollTop);
// useEffect(() => {
// scrollTopRef.current = scrollTop;
// }, [scrollTop]);
// fix clicks in interaction canvas
// Auto-scroll to selected span else on top(based on default span)
// time bar line vertical
// zoom handle vertical and horizontal scroll with proper defined thresholds
// timeline should be in sync with the flamegraph. test with vertical line of time on event etc.
// proper working interaction layer for clicks and hovers
// hit testing should be efficient and accurate without flat spanRect
// Final Priority Order (Clean Summary)
// ✅ Zoom (Horizontal + Vertical thresholds)
// ✅ Timeline sync + vertical time dashed line
// ✅ Minimap brush correctness
// ✅ Auto-scroll behavior
// ✅ Interaction layer separation
// ✅ Efficient hit testing

View File

@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Form } from 'antd';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import isEqual from 'lodash-es/isEqual';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -30,7 +30,7 @@ function TagFilterInput({
};
return (
<QueryBuilderSearchV2
<QueryBuilderSearch
query={query}
onChange={onQueryChange}
placeholder={placeholder}

View File

@@ -86,7 +86,7 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
}));
const BASE_URL = ENVIRONMENT.baseURL;
const attributeKeysURL = `${BASE_URL}/api/v3/filter_suggestions`;
const attributeKeysURL = `${BASE_URL}/api/v3/autocomplete/attribute_keys`;
describe('PipelinePage container test', () => {
beforeAll(() => {
@@ -333,34 +333,26 @@ describe('PipelinePage container test', () => {
ctx.json({
status: 'success',
data: {
attributes: [
attributeKeys: [
{
key: 'otelServiceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
{
key: 'service.instance.id',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
],
},

View File

@@ -973,7 +973,6 @@ function QueryBuilderSearchV2(
return (
<div className="query-builder-search-v2">
<Select
data-testid={'qb-search-select'}
ref={selectRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(hasPopupContainer ? { getPopupContainer: popupContainer } : {})}

View File

@@ -1,94 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsTextSelected } from 'hooks/useIsTextSelected';
import { ILog } from 'types/api/logs/log';
import useLogDetailHandlers from '../useLogDetailHandlers';
jest.mock('hooks/logs/useActiveLog');
jest.mock('hooks/useIsTextSelected');
const mockOnSetActiveLog = jest.fn();
const mockOnClearActiveLog = jest.fn();
const mockOnAddToQuery = jest.fn();
const mockOnGroupByAttribute = jest.fn();
const mockIsTextSelected = jest.fn();
const mockLog: ILog = {
id: 'log-1',
timestamp: '2024-01-01T00:00:00Z',
date: '2024-01-01',
body: 'test log body',
severityText: 'INFO',
severityNumber: 9,
traceFlags: 0,
traceId: '',
spanID: '',
attributesString: {},
attributesInt: {},
attributesFloat: {},
resources_string: {},
scope_string: {},
attributes_string: {},
severity_text: '',
severity_number: 0,
};
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(useIsTextSelected).mockReturnValue(mockIsTextSelected);
jest.mocked(useActiveLog).mockReturnValue({
activeLog: null,
onSetActiveLog: mockOnSetActiveLog,
onClearActiveLog: mockOnClearActiveLog,
onAddToQuery: mockOnAddToQuery,
onGroupByAttribute: mockOnGroupByAttribute,
});
});
it('should not open log detail when text is selected', () => {
mockIsTextSelected.mockReturnValue(true);
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
});
it('should open log detail when no text is selected', () => {
mockIsTextSelected.mockReturnValue(false);
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnSetActiveLog).toHaveBeenCalledWith(mockLog);
});
it('should toggle off when clicking the same active log', () => {
mockIsTextSelected.mockReturnValue(false);
jest.mocked(useActiveLog).mockReturnValue({
activeLog: mockLog,
onSetActiveLog: mockOnSetActiveLog,
onClearActiveLog: mockOnClearActiveLog,
onAddToQuery: mockOnAddToQuery,
onGroupByAttribute: mockOnGroupByAttribute,
});
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnClearActiveLog).toHaveBeenCalled();
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
});

View File

@@ -1,17 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { MetricsType } from 'container/MetricsApplication/constant';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { AppState } from 'store/reducers';
@@ -56,20 +54,6 @@ export const useActiveLog = (): UseActiveLog => {
const [activeLog, setActiveLog] = useState<ILog | null>(null);
// Close drawer/clear active log when query in URL changes
const urlQuery = useUrlQuery();
const compositeQuery = urlQuery.get(QueryParams.compositeQuery) ?? '';
const prevQueryRef = useRef<string | null>(null);
useEffect(() => {
if (
prevQueryRef.current !== null &&
prevQueryRef.current !== compositeQuery
) {
setActiveLog(null);
}
prevQueryRef.current = compositeQuery;
}, [compositeQuery]);
const onSetDetailedLogData = useCallback(
(logData: ILog) => {
dispatch({

View File

@@ -2,7 +2,6 @@ import { useCallback, useState } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import type { UseActiveLog } from 'hooks/logs/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsTextSelected } from 'hooks/useIsTextSelected';
import { ILog } from 'types/api/logs/log';
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
@@ -29,13 +28,9 @@ function useLogDetailHandlers({
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
const isTextSelected = useIsTextSelected();
const handleSetActiveLog = useCallback(
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
if (isTextSelected()) {
return;
}
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
@@ -44,7 +39,7 @@ function useLogDetailHandlers({
onSetActiveLog(log);
setSelectedTab(nextTab ?? defaultTab);
},
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog, isTextSelected],
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
);
const handleCloseLogDetail = useCallback((): void => {

View File

@@ -17,6 +17,9 @@ const useGetTraceFlamegraph = (
REACT_QUERY_KEY.GET_TRACE_V2_FLAMEGRAPH,
props.traceId,
props.selectedSpanId,
props.limit,
props.boundaryStartTsMilli,
props.boundarEndTsMilli,
],
enabled: !!props.traceId,
keepPreviousData: true,

View File

@@ -1,10 +0,0 @@
import { useCallback } from 'react';
export function useIsTextSelected(): () => boolean {
return useCallback((): boolean => {
const selection = window.getSelection();
return (
!!selection && !selection.isCollapsed && selection.toString().length > 0
);
}, []);
}

View File

@@ -5,6 +5,9 @@ export interface TraceDetailFlamegraphURLProps {
export interface GetTraceFlamegraphPayloadProps {
traceId: string;
selectedSpanId: string;
limit: number;
boundaryStartTsMilli: number;
boundarEndTsMilli: number;
}
export interface Event {
@@ -31,4 +34,6 @@ export interface GetTraceFlamegraphSuccessResponse {
spans: FlamegraphSpan[][];
startTimestampMillis: number;
endTimestampMillis: number;
durationNano: number;
hasMore: boolean;
}

View File

@@ -18,7 +18,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -47,7 +46,6 @@ type provider struct {
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
authzHandler authz.Handler
rawDataExportHandler rawdataexport.Handler
zeusHandler zeus.Handler
querierHandler querier.Handler
}
@@ -69,7 +67,6 @@ func NewFactory(
gatewayHandler gateway.Handler,
fieldsHandler fields.Handler,
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
querierHandler querier.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
@@ -94,7 +91,6 @@ func NewFactory(
gatewayHandler,
fieldsHandler,
authzHandler,
rawDataExportHandler,
zeusHandler,
querierHandler,
)
@@ -121,7 +117,6 @@ func newProvider(
gatewayHandler gateway.Handler,
fieldsHandler fields.Handler,
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
querierHandler querier.Handler,
) (apiserver.APIServer, error) {
@@ -146,7 +141,6 @@ func newProvider(
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
authzHandler: authzHandler,
rawDataExportHandler: rawDataExportHandler,
zeusHandler: zeusHandler,
querierHandler: querierHandler,
}
@@ -221,10 +215,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addRawDataExportRoutes(router); err != nil {
return err
}
if err := provider.addZeusRoutes(router); err != nil {
return err
}

View File

@@ -1,47 +0,0 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/exporttypes"
v5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/gorilla/mux"
)
func (provider *provider) addRawDataExportRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/export_raw_data", handler.New(provider.authZ.ViewAccess(provider.rawDataExportHandler.ExportRawData), handler.OpenAPIDef{
ID: "HandleExportRawDataGET",
Tags: []string{"logs", "traces"},
Summary: "Export raw data",
Description: "This endpoints allows simple query exporting raw data for traces and logs",
RequestQuery: new(exporttypes.ExportRawDataQueryParams),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/export_raw_data", handler.New(provider.authZ.ViewAccess(provider.rawDataExportHandler.ExportRawData), handler.OpenAPIDef{
ID: "HandleExportRawDataPOST",
Tags: []string{"logs", "traces"},
Summary: "Export raw data",
Description: "This endpoints allows complex query exporting raw data for traces and logs",
Request: new(v5.QueryRangeRequest),
RequestQuery: new(exporttypes.ExportRawDataFormatQueryParam),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -87,7 +87,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
}
func (m *module) listPromotedPaths(ctx context.Context) ([]string, error) {
paths, err := m.metadataStore.GetPromotedPaths(ctx)
paths, err := m.metadataStore.ListPromotedPaths(ctx)
if err != nil {
return nil, err
}
@@ -142,7 +142,7 @@ func (m *module) PromoteAndIndexPaths(
pathsStr = append(pathsStr, path.Path)
}
existingPromotedPaths, err := m.metadataStore.GetPromotedPaths(ctx, pathsStr...)
existingPromotedPaths, err := m.metadataStore.ListPromotedPaths(ctx, pathsStr...)
if err != nil {
return err
}

View File

@@ -6,18 +6,18 @@ import (
"fmt"
"io"
"net/http"
"slices"
"net/url"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/exporttypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -31,52 +31,129 @@ func NewHandler(module rawdataexport.Module) rawdataexport.Handler {
return &handler{module: module}
}
// ExportRawData handles data export requests.
//
// API Documentation:
// Endpoint: GET /api/v1/export_raw_data
//
// Query Parameters:
//
// - source (optional): Type of data to export ["logs" (default), "metrics", "traces"]
// Note: Currently only "logs" is fully supported
//
// - format (optional): Output format ["csv" (default), "jsonl"]
//
// - start (required): Start time for query (Unix timestamp in nanoseconds)
//
// - end (required): End time for query (Unix timestamp in nanoseconds)
//
// - limit (optional): Maximum number of rows to export
// Constraints: Must be positive and cannot exceed MAX_EXPORT_ROW_COUNT_LIMIT
//
// - filter (optional): Filter expression to apply to the query
//
// - columns (optional): Specific columns to include in export
// Default: all columns are returned
// Format: ["context.field:type", "context.field", "field"]
//
// - order_by (optional): Sorting specification ["column:direction" or "context.field:type:direction"]
// Direction: "asc" or "desc"
// Default: ["timestamp:desc", "id:desc"]
//
// Response Headers:
// - Content-Type: "text/csv" or "application/x-ndjson"
// - Content-Encoding: "gzip" (handled by HTTP middleware)
// - Content-Disposition: "attachment; filename=\"data_exported.[format]\""
// - Cache-Control: "no-cache"
// - Vary: "Accept-Encoding"
// - Transfer-Encoding: "chunked"
// - Trailers: X-Response-Complete
//
// Response Format:
//
// CSV: Headers in first row, data in subsequent rows
// JSONL: One JSON object per line
//
// Example Usage:
//
// Basic CSV export:
// GET /api/v1/export_raw_data?start=1693612800000000000&end=1693699199000000000
//
// Export with columns and format:
// GET /api/v1/export_raw_data?start=1693612800000000000&end=1693699199000000000&format=jsonl
// &columns=timestamp&columns=severity&columns=message
//
// Export with filter and ordering:
// GET /api/v1/export_raw_data?start=1693612800000000000&end=1693699199000000000
// &filter=severity="error"&order_by=timestamp:desc&limit=1000
func (handler *handler) ExportRawData(rw http.ResponseWriter, r *http.Request) {
var queryRangeRequest qbtypes.QueryRangeRequest
var format string
source, err := getExportQuerySource(r.URL.Query())
if err != nil {
render.Error(rw, err)
return
}
switch r.Method {
case http.MethodGet:
var params exporttypes.ExportRawDataQueryParams
if err := binding.Query.BindQuery(r.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
params.Normalize()
if err := params.Validate(); err != nil {
render.Error(rw, err)
return
}
format = params.Format
queryRangeRequest = buildQueryRangeRequest(&params)
case http.MethodPost:
var formatParam exporttypes.ExportRawDataFormatQueryParam
if err := binding.Query.BindQuery(r.URL.Query(), &formatParam); err != nil {
render.Error(rw, err)
return
}
format = formatParam.Format
if err := json.NewDecoder(r.Body).Decode(&queryRangeRequest); err != nil {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid request body: %v", err))
return
}
switch source {
case "logs":
handler.exportLogs(rw, r)
case "traces":
handler.exportTraces(rw, r)
case "metrics":
handler.exportMetrics(rw, r)
default:
render.Error(rw, errors.Newf(errors.TypeMethodNotAllowed, errors.CodeMethodNotAllowed, "method not allowed, only GET/POST supported"))
return
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid source: must be logs"))
}
}
if err := validateSpecForExport(&queryRangeRequest); err != nil {
func (handler *handler) exportMetrics(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.Newf(errors.TypeUnsupported, errors.CodeUnsupported, "metrics export is not yet supported"))
}
func (handler *handler) exportTraces(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.Newf(errors.TypeUnsupported, errors.CodeUnsupported, "traces export is not yet supported"))
}
func (handler *handler) exportLogs(rw http.ResponseWriter, r *http.Request) {
// Set up response headers
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Set("Vary", "Accept-Encoding") // Indicate that response varies based on Accept-Encoding
rw.Header().Set("Access-Control-Expose-Headers", "Content-Disposition, X-Response-Complete")
rw.Header().Set("Trailer", "X-Response-Complete")
rw.Header().Set("Transfer-Encoding", "chunked")
queryParams := r.URL.Query()
startTime, endTime, err := getExportQueryTimeRange(queryParams)
if err != nil {
render.Error(rw, err)
return
}
if err := validateAndApplyDefaultExportLimits(queryRangeRequest.CompositeQuery.Queries); err != nil {
limit, err := getExportQueryLimit(queryParams)
if err != nil {
render.Error(rw, err)
return
}
// Use default OrderBy if not specified
queryRangeRequest.UseDefaultOrderBy()
format, err := getExportQueryFormat(queryParams)
if err != nil {
render.Error(rw, err)
return
}
// Set appropriate content type and filename
filename := fmt.Sprintf("data_exported_%s.%s", time.Now().Format("2006-01-02_150405"), format)
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
filterExpression := queryParams.Get("filter")
orderByExpression, err := getExportQueryOrderBy(queryParams)
if err != nil {
render.Error(rw, err)
return
}
columns := getExportQueryColumns(queryParams)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
@@ -90,134 +167,70 @@ func (handler *handler) ExportRawData(rw http.ResponseWriter, r *http.Request) {
return
}
setExportResponseHeaders(rw, format)
queryRangeRequest := qbtypes.QueryRangeRequest{
Start: startTime,
End: endTime,
RequestType: qbtypes.RequestTypeRaw,
CompositeQuery: qbtypes.CompositeQuery{
Queries: []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: nil,
},
},
},
}
spec := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Name: "raw",
Filter: &qbtypes.Filter{
Expression: filterExpression,
},
Limit: limit,
Order: orderByExpression,
}
spec.SelectFields = columns
queryRangeRequest.CompositeQuery.Queries[0].Spec = spec
// This will signal Export module to stop sending data
doneChan := make(chan any)
defer close(doneChan)
rowChan, errChan := handler.module.ExportRawData(r.Context(), orgID, &queryRangeRequest, doneChan)
isComplete, err := handler.executeExport(rowChan, errChan, format, rw)
if err != nil {
render.Error(rw, err)
return
}
rw.Header().Set("X-Response-Complete", strconv.FormatBool(isComplete))
}
var isComplete bool
// validateSpecForExport validates query specs
func validateSpecForExport(req *qbtypes.QueryRangeRequest) error {
queries := req.CompositeQuery.Queries
// If the trace operator query is not present, and there are multiple queries, return an error
if req.TraceOperatorQueryIndex() == -1 && len(queries) > 1 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "multiple queries not allowed without a trace operator query")
}
for idx := range queries {
switch spec := queries[idx].Spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
qbtypes.QueryBuilderTraceOperator:
// Supported spec types
default:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported query at index %d type: %T", idx, spec)
}
}
err := req.Validate(qbtypes.WithSkipLimitValidation())
if err != nil {
return err
}
return nil
}
func validateAndApplyDefaultExportLimits(queries []qbtypes.QueryEnvelope) error {
for idx := range queries {
limit := queries[idx].GetLimit()
if limit == 0 {
limit = DefaultExportRowCountLimit
} else if limit < 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be positive")
} else if limit > MaxExportRowCountLimit {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit cannot be more than %d", MaxExportRowCountLimit)
}
queries[idx].SetLimit(limit)
}
return nil
}
// buildQueryEnvelope creates a QueryEnvelope with a QueryBuilderQuery for the given aggregation type.
func buildQueryEnvelope[T any](signal telemetrytypes.Signal, filter *qbtypes.Filter, limit int, order []qbtypes.OrderBy, selectFields []telemetrytypes.TelemetryFieldKey) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[T]{
Signal: signal,
Filter: filter,
Limit: limit,
Order: order,
SelectFields: selectFields,
},
}
}
// buildQueryRangeRequest builds a QueryRangeRequest from already-bound and validated GET query params.
func buildQueryRangeRequest(params *exporttypes.ExportRawDataQueryParams) qbtypes.QueryRangeRequest {
var query qbtypes.QueryEnvelope
switch params.Signal {
case telemetrytypes.SignalLogs:
query = buildQueryEnvelope[qbtypes.LogAggregation](params.Signal, &params.Filter, params.Limit, params.Order, params.SelectFields)
case telemetrytypes.SignalTraces:
query = buildQueryEnvelope[qbtypes.TraceAggregation](params.Signal, &params.Filter, params.Limit, params.Order, params.SelectFields)
}
return qbtypes.QueryRangeRequest{
Start: params.Start,
End: params.End,
RequestType: qbtypes.RequestTypeRaw,
CompositeQuery: qbtypes.CompositeQuery{
Queries: []qbtypes.QueryEnvelope{query},
},
}
}
// setExportResponseHeaders sets common HTTP headers for export responses.
func setExportResponseHeaders(rw http.ResponseWriter, format string) {
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Set("Vary", "Accept-Encoding")
rw.Header().Set("Access-Control-Expose-Headers", "Content-Disposition, X-Response-Complete")
rw.Header().Set("Trailer", "X-Response-Complete")
rw.Header().Set("Transfer-Encoding", "chunked")
filename := fmt.Sprintf("data_exported_%s.%s", time.Now().Format("2006-01-02_150405"), format)
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
}
// executeExport streams data from rowChan to the response writer in the specified format.
func (handler *handler) executeExport(rowChan <-chan *qbtypes.RawRow, errChan <-chan error, format string, rw http.ResponseWriter) (bool, error) {
switch format {
case "csv", "":
rw.Header().Set("Content-Type", "text/csv")
csvWriter := csv.NewWriter(rw)
isComplete, err := handler.exportRawDataCSV(rowChan, errChan, csvWriter)
isComplete, err = handler.exportLogsCSV(rowChan, errChan, csvWriter)
if err != nil {
return false, err
render.Error(rw, err)
return
}
csvWriter.Flush()
return isComplete, nil
case "jsonl":
rw.Header().Set("Content-Type", "application/x-ndjson")
return handler.exportRawDataJSONL(rowChan, errChan, rw)
isComplete, err = handler.exportLogsJSONL(rowChan, errChan, rw)
if err != nil {
render.Error(rw, err)
return
}
default:
return false, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid format: must be csv or jsonl")
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid format: must be csv or jsonl"))
return
}
rw.Header().Set("X-Response-Complete", strconv.FormatBool(isComplete))
}
// exportRawDataCSV is a generic CSV export function that works with any raw data (logs, traces, etc.)
func (handler *handler) exportRawDataCSV(rowChan <-chan *qbtypes.RawRow, errChan <-chan error, csvWriter *csv.Writer) (bool, error) {
func (handler *handler) exportLogsCSV(rowChan <-chan *qbtypes.RawRow, errChan <-chan error, csvWriter *csv.Writer) (bool, error) {
var header []string
headerToIndexMapping := make(map[string]int)
headerToIndexMapping := make(map[string]int, len(header))
totalBytes := uint64(0)
for {
@@ -255,8 +268,8 @@ func (handler *handler) exportRawDataCSV(rowChan <-chan *qbtypes.RawRow, errChan
}
}
// exportRawDataJSONL is a generic JSONL export function that works with any raw data (logs, traces, etc.)
func (handler *handler) exportRawDataJSONL(rowChan <-chan *qbtypes.RawRow, errChan <-chan error, writer io.Writer) (bool, error) {
func (handler *handler) exportLogsJSONL(rowChan <-chan *qbtypes.RawRow, errChan <-chan error, writer io.Writer) (bool, error) {
totalBytes := uint64(0)
for {
select {
@@ -264,11 +277,9 @@ func (handler *handler) exportRawDataJSONL(rowChan <-chan *qbtypes.RawRow, errCh
if !ok {
return true, nil
}
jsonBytes, err := json.Marshal(row.Data)
if err != nil {
return false, errors.NewUnexpectedf(errors.CodeInternal, "error marshaling JSON: %s", err)
}
totalBytes += uint64(len(jsonBytes)) + 1
// Handle JSON format (JSONL - one object per line)
jsonBytes, _ := json.Marshal(row.Data)
totalBytes += uint64(len(jsonBytes)) + 1 // +1 for newline
if _, err := writer.Write(jsonBytes); err != nil {
return false, errors.NewUnexpectedf(errors.CodeInternal, "error writing JSON: %s", err)
@@ -288,33 +299,74 @@ func (handler *handler) exportRawDataJSONL(rowChan <-chan *qbtypes.RawRow, errCh
}
}
// priorityColumns defines the columns that should appear first in the CSV output, in order.
var priorityColumns = []string{"timestamp", "id"}
func getExportQuerySource(queryParams url.Values) (string, error) {
switch queryParams.Get("source") {
case "logs", "":
return "logs", nil
case "metrics":
return "metrics", errors.NewInvalidInputf(errors.CodeInvalidInput, "metrics export not yet supported")
case "traces":
return "traces", errors.NewInvalidInputf(errors.CodeInvalidInput, "traces export not yet supported")
default:
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid source: must be logs, metrics or traces")
}
}
func getExportQueryFormat(queryParams url.Values) (string, error) {
switch queryParams.Get("format") {
case "csv", "":
return "csv", nil
case "jsonl":
return "jsonl", nil
default:
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid format: must be csv or jsonl")
}
}
func getExportQueryLimit(queryParams url.Values) (int, error) {
limitStr := queryParams.Get("limit")
if limitStr == "" {
return DefaultExportRowCountLimit, nil
} else {
limit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid limit format: %s", err.Error())
}
if limit <= 0 {
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be positive")
}
if limit > MaxExportRowCountLimit {
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "limit cannot be more than %d", MaxExportRowCountLimit)
}
return limit, nil
}
}
func getExportQueryTimeRange(queryParams url.Values) (uint64, uint64, error) {
startTimeStr := queryParams.Get("start")
endTimeStr := queryParams.Get("end")
if startTimeStr == "" || endTimeStr == "" {
return 0, 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "start and end time are required")
}
startTime, err := strconv.ParseUint(startTimeStr, 10, 64)
if err != nil {
return 0, 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid start time format: %s", err.Error())
}
endTime, err := strconv.ParseUint(endTimeStr, 10, 64)
if err != nil {
return 0, 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid end time format: %s", err.Error())
}
return startTime, endTime, nil
}
func constructCSVHeaderFromQueryResponse(data map[string]any) []string {
header := make([]string, 0, len(data))
for key := range data {
header = append(header, key)
}
// This is to ensure CSV output is consistent across multiple queries
slices.SortFunc(header, func(a, b string) int {
ai, bi := slices.Index(priorityColumns, a), slices.Index(priorityColumns, b)
switch {
case ai != -1 && bi != -1:
return ai - bi
case ai != -1:
return -1
case bi != -1:
return 1
default:
if a < b {
return -1
} else if a > b {
return 1
}
return 0
}
})
return header
}
@@ -375,12 +427,9 @@ func constructCSVRecordFromQueryResponse(data map[string]any, headerToIndexMappi
valueStr = v.String()
default:
jsonBytes, err := json.Marshal(v)
if err != nil {
valueStr = fmt.Sprintf("%v", v)
} else {
valueStr = string(jsonBytes)
}
// For all other complex types (maps, structs, etc.)
jsonBytes, _ := json.Marshal(v)
valueStr = string(jsonBytes)
}
record[index] = sanitizeForCSV(valueStr)
@@ -389,6 +438,26 @@ func constructCSVRecordFromQueryResponse(data map[string]any, headerToIndexMappi
return record
}
// getExportQueryColumns parses the "columns" query parameters and returns a slice of TelemetryFieldKey structs.
// Each column should be a valid telemetry field key in the format "context.field:type" or "context.field" or "field"
func getExportQueryColumns(queryParams url.Values) []telemetrytypes.TelemetryFieldKey {
columnParams := queryParams["columns"]
columns := make([]telemetrytypes.TelemetryFieldKey, 0, len(columnParams))
for _, columnStr := range columnParams {
// Skip empty strings
columnStr = strings.TrimSpace(columnStr)
if columnStr == "" {
continue
}
columns = append(columns, telemetrytypes.GetFieldKeyFromKeyText(columnStr))
}
return columns
}
func getsizeOfStringSlice(slice []string) uint64 {
var totalBytes uint64
for _, str := range slice {
@@ -396,3 +465,52 @@ func getsizeOfStringSlice(slice []string) uint64 {
}
return totalBytes
}
// getExportQueryOrderBy parses the "order_by" query parameters and returns a slice of OrderBy structs.
// Each "order_by" parameter should be in the format "column:direction"
// Each "column" should be a valid telemetry field key in the format "context.field:type" or "context.field" or "field"
func getExportQueryOrderBy(queryParams url.Values) ([]qbtypes.OrderBy, error) {
orderByParam := queryParams.Get("order_by")
orderByParam = strings.TrimSpace(orderByParam)
if orderByParam == "" {
return telemetrylogs.DefaultLogsV2SortingOrder, nil
}
parts := strings.Split(orderByParam, ":")
if len(parts) != 2 && len(parts) != 3 {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order_by format: %s, should be <column>:<direction>", orderByParam)
}
column := strings.Join(parts[:len(parts)-1], ":")
direction := parts[len(parts)-1]
orderDirection, ok := qbtypes.OrderDirectionMap[direction]
if !ok {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order_by direction: %s, should be one of %s, %s", direction, qbtypes.OrderDirectionAsc, qbtypes.OrderDirectionDesc)
}
orderByKey := telemetrytypes.GetFieldKeyFromKeyText(column)
orderBy := []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: orderByKey,
},
Direction: orderDirection,
},
}
// If we are ordering by the timestamp column, also order by the ID column
if orderByKey.Name == telemetrylogs.LogsV2TimestampColumn {
orderBy = append(orderBy, qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2IDColumn,
},
},
Direction: orderDirection,
})
}
return orderBy, nil
}

View File

@@ -2,85 +2,58 @@ package implrawdataexport
import (
"net/url"
"strconv"
"testing"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/types/exporttypes"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func TestExportRawDataQueryParams_BindingDefaults(t *testing.T) {
var params exporttypes.ExportRawDataQueryParams
err := binding.Query.BindQuery(url.Values{}, &params)
assert.NoError(t, err)
assert.Equal(t, "csv", params.Format)
assert.Equal(t, DefaultExportRowCountLimit, params.Limit)
}
func logQuery(limit int) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{Limit: limit},
}
}
func traceQuery(limit int) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{Limit: limit},
}
}
func traceOperatorQuery(limit int) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeTraceOperator,
Spec: qbtypes.QueryBuilderTraceOperator{Limit: limit, Expression: "A"},
}
}
func makeRequest(queries ...qbtypes.QueryEnvelope) qbtypes.QueryRangeRequest {
return qbtypes.QueryRangeRequest{
Start: 1000000000000,
End: 1000003600000,
RequestType: qbtypes.RequestTypeRaw,
CompositeQuery: qbtypes.CompositeQuery{Queries: queries},
}
}
func TestValidateSpecForExport(t *testing.T) {
func TestGetExportQuerySource(t *testing.T) {
tests := []struct {
name string
req qbtypes.QueryRangeRequest
expectedError bool
name string
queryParams url.Values
expectedSource string
expectedError bool
}{
{
name: "single log query",
req: makeRequest(logQuery(0)),
name: "default logs source",
queryParams: url.Values{},
expectedSource: "logs",
expectedError: false,
},
{
name: "single trace query",
req: makeRequest(traceQuery(0)),
name: "explicit logs source",
queryParams: url.Values{"source": {"logs"}},
expectedSource: "logs",
expectedError: false,
},
{
name: "trace operator alone",
req: makeRequest(traceOperatorQuery(0)),
name: "metrics source - not supported",
queryParams: url.Values{"source": {"metrics"}},
expectedSource: "metrics",
expectedError: true,
},
{
name: "multiple queries without trace operator",
req: makeRequest(logQuery(0), traceQuery(0)),
expectedError: true,
name: "traces source - not supported",
queryParams: url.Values{"source": {"traces"}},
expectedSource: "traces",
expectedError: true,
},
{
name: "unsupported query type",
req: makeRequest(qbtypes.QueryEnvelope{Type: qbtypes.QueryTypeBuilder, Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{}}),
expectedError: true,
name: "invalid source",
queryParams: url.Values{"source": {"invalid"}},
expectedSource: "",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateSpecForExport(&tt.req)
source, err := getExportQuerySource(tt.queryParams)
assert.Equal(t, tt.expectedSource, source)
if tt.expectedError {
assert.Error(t, err)
} else {
@@ -90,69 +63,456 @@ func TestValidateSpecForExport(t *testing.T) {
}
}
func TestValidateAndApplyDefaultExportLimits(t *testing.T) {
func TestGetExportQueryFormat(t *testing.T) {
tests := []struct {
name string
queries []qbtypes.QueryEnvelope
expectedError bool
checkQueries func(t *testing.T, queries []qbtypes.QueryEnvelope)
name string
queryParams url.Values
expectedFormat string
expectedError bool
}{
{
name: "single log query, zero limit gets default",
queries: makeRequest(logQuery(0)).CompositeQuery.Queries,
checkQueries: func(t *testing.T, q []qbtypes.QueryEnvelope) {
assert.Equal(t, DefaultExportRowCountLimit, q[0].GetLimit())
},
name: "default csv format",
queryParams: url.Values{},
expectedFormat: "csv",
expectedError: false,
},
{
name: "single log query, valid limit kept",
queries: makeRequest(logQuery(1000)).CompositeQuery.Queries,
checkQueries: func(t *testing.T, q []qbtypes.QueryEnvelope) {
assert.Equal(t, 1000, q[0].GetLimit())
},
name: "explicit csv format",
queryParams: url.Values{"format": {"csv"}},
expectedFormat: "csv",
expectedError: false,
},
{
name: "single log query, max limit kept",
queries: makeRequest(logQuery(MaxExportRowCountLimit)).CompositeQuery.Queries,
checkQueries: func(t *testing.T, q []qbtypes.QueryEnvelope) {
assert.Equal(t, MaxExportRowCountLimit, q[0].GetLimit())
},
name: "jsonl format",
queryParams: url.Values{"format": {"jsonl"}},
expectedFormat: "jsonl",
expectedError: false,
},
{
name: "single log query, limit exceeds max",
queries: makeRequest(logQuery(MaxExportRowCountLimit + 1)).CompositeQuery.Queries,
name: "invalid format",
queryParams: url.Values{"format": {"xml"}},
expectedFormat: "",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
format, err := getExportQueryFormat(tt.queryParams)
assert.Equal(t, tt.expectedFormat, format)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestGetExportQueryLimit(t *testing.T) {
tests := []struct {
name string
queryParams url.Values
expectedLimit int
expectedError bool
}{
{
name: "default limit",
queryParams: url.Values{},
expectedLimit: DefaultExportRowCountLimit,
expectedError: false,
},
{
name: "valid limit",
queryParams: url.Values{"limit": {"5000"}},
expectedLimit: 5000,
expectedError: false,
},
{
name: "maximum limit",
queryParams: url.Values{"limit": {strconv.Itoa(MaxExportRowCountLimit)}},
expectedLimit: MaxExportRowCountLimit,
expectedError: false,
},
{
name: "limit exceeds maximum",
queryParams: url.Values{"limit": {"100000"}},
expectedLimit: 0,
expectedError: true,
},
{
name: "single log query, negative limit",
queries: makeRequest(logQuery(-1)).CompositeQuery.Queries,
name: "invalid limit format",
queryParams: url.Values{"limit": {"invalid"}},
expectedLimit: 0,
expectedError: true,
},
{
name: "single trace query, zero limit gets default",
queries: makeRequest(traceQuery(0)).CompositeQuery.Queries,
checkQueries: func(t *testing.T, q []qbtypes.QueryEnvelope) {
assert.Equal(t, DefaultExportRowCountLimit, q[0].GetLimit())
name: "negative limit",
queryParams: url.Values{"limit": {"-100"}},
expectedLimit: 0,
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
limit, err := getExportQueryLimit(tt.queryParams)
assert.Equal(t, tt.expectedLimit, limit)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestGetExportQueryTimeRange(t *testing.T) {
tests := []struct {
name string
queryParams url.Values
expectedStartTime uint64
expectedEndTime uint64
expectedError bool
}{
{
name: "valid time range",
queryParams: url.Values{
"start": {"1640995200"},
"end": {"1641081600"},
},
expectedStartTime: 1640995200,
expectedEndTime: 1641081600,
expectedError: false,
},
{
name: "missing start time",
queryParams: url.Values{"end": {"1641081600"}},
expectedError: true,
},
{
name: "missing end time",
queryParams: url.Values{"start": {"1640995200"}},
expectedError: true,
},
{
name: "missing both times",
queryParams: url.Values{},
expectedError: true,
},
{
name: "invalid start time format",
queryParams: url.Values{
"start": {"invalid"},
"end": {"1641081600"},
},
expectedError: true,
},
{
name: "invalid end time format",
queryParams: url.Values{
"start": {"1640995200"},
"end": {"invalid"},
},
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
startTime, endTime, err := getExportQueryTimeRange(tt.queryParams)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedStartTime, startTime)
assert.Equal(t, tt.expectedEndTime, endTime)
}
})
}
}
func TestGetExportQueryColumns(t *testing.T) {
tests := []struct {
name string
queryParams url.Values
expectedColumns []telemetrytypes.TelemetryFieldKey
}{
{
name: "no columns specified",
queryParams: url.Values{},
expectedColumns: []telemetrytypes.TelemetryFieldKey{},
},
{
name: "single column",
queryParams: url.Values{
"columns": {"timestamp"},
},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
},
},
{
name: "trace operator alone, zero limit gets default",
queries: makeRequest(traceOperatorQuery(0)).CompositeQuery.Queries,
checkQueries: func(t *testing.T, q []qbtypes.QueryEnvelope) {
assert.Equal(t, DefaultExportRowCountLimit, q[0].GetLimit())
name: "multiple columns",
queryParams: url.Values{
"columns": {"timestamp", "message", "level"},
},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "message"},
{Name: "level"},
},
},
{
name: "empty column name (should be skipped)",
queryParams: url.Values{
"columns": {"timestamp", "", "level"},
},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "level"},
},
},
{
name: "whitespace column name (should be skipped)",
queryParams: url.Values{
"columns": {"timestamp", " ", "level"},
},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "level"},
},
},
{
name: "valid column name with data type",
queryParams: url.Values{
"columns": {"timestamp", "attribute.user:string", "level"},
},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "user", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "level"},
},
},
{
name: "valid column name with dot notation",
queryParams: url.Values{
"columns": {"timestamp", "attribute.user.string", "level"},
},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "user.string", FieldContext: telemetrytypes.FieldContextAttribute},
{Name: "level"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateAndApplyDefaultExportLimits(tt.queries)
columns := getExportQueryColumns(tt.queryParams)
assert.Equal(t, len(tt.expectedColumns), len(columns))
for i, expectedCol := range tt.expectedColumns {
assert.Equal(t, expectedCol, columns[i])
}
})
}
}
func TestGetExportQueryOrderBy(t *testing.T) {
tests := []struct {
name string
queryParams url.Values
expectedOrder []qbtypes.OrderBy
expectedError bool
}{
{
name: "no order specified",
queryParams: url.Values{},
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionDesc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2TimestampColumn,
},
},
},
{
Direction: qbtypes.OrderDirectionDesc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2IDColumn,
},
},
},
},
expectedError: false,
},
{
name: "single order error, direction not specified",
queryParams: url.Values{
"order_by": {"timestamp"},
},
expectedOrder: nil,
expectedError: true,
},
{
name: "single order no error",
queryParams: url.Values{
"order_by": {"timestamp:asc"},
},
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2TimestampColumn,
},
},
},
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2IDColumn,
},
},
},
},
expectedError: false,
},
{
name: "multiple orders",
queryParams: url.Values{
"order_by": {"timestamp:asc", "body:desc", "id:asc"},
},
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2TimestampColumn,
},
},
},
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2IDColumn,
},
},
},
},
expectedError: false,
},
{
name: "empty order name (should be skipped)",
queryParams: url.Values{
"order_by": {"timestamp:asc", "", "id:asc"},
},
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2TimestampColumn,
},
},
},
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2IDColumn,
},
},
},
},
expectedError: false,
},
{
name: "whitespace order name (should be skipped)",
queryParams: url.Values{
"order_by": {"timestamp:asc", " ", "id:asc"},
},
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2TimestampColumn,
},
},
},
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: telemetrylogs.LogsV2IDColumn,
},
},
},
},
expectedError: false,
},
{
name: "invalid order name (should error out)",
queryParams: url.Values{
"order_by": {"attributes.user:", "id:asc"},
},
expectedOrder: nil,
expectedError: true,
},
{
name: "valid order name (should be included)",
queryParams: url.Values{
"order_by": {"attribute.user:string:desc", "id:asc"},
},
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionDesc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "user",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
},
},
expectedError: false,
},
{
name: "valid order name (should be included)",
queryParams: url.Values{
"order_by": {"attribute.user.string:desc", "id:asc"},
},
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionDesc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "user.string",
FieldContext: telemetrytypes.FieldContextAttribute,
},
},
},
},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
order, err := getExportQueryOrderBy(tt.queryParams)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tt.checkQueries != nil {
tt.checkQueries(t, tt.queries)
assert.Equal(t, len(tt.expectedOrder), len(order))
for i, expectedOrd := range tt.expectedOrder {
assert.Equal(t, expectedOrd, order[i])
}
}
})
@@ -169,8 +529,13 @@ func TestConstructCSVHeaderFromQueryResponse(t *testing.T) {
header := constructCSVHeaderFromQueryResponse(data)
// Priority columns come first in order, then the rest alphabetically.
assert.Equal(t, []string{"timestamp", "id", "level", "message"}, header)
// Since map iteration order is not guaranteed, check that all expected keys are present
expectedKeys := []string{"timestamp", "message", "level", "id"}
assert.Equal(t, len(expectedKeys), len(header))
for _, key := range expectedKeys {
assert.Contains(t, header, key)
}
}
func TestConstructCSVRecordFromQueryResponse(t *testing.T) {

View File

@@ -23,18 +23,8 @@ func NewModule(querier querier.Querier) rawdataexport.Module {
func (m *Module) ExportRawData(ctx context.Context, orgID valuer.UUID, rangeRequest *qbtypes.QueryRangeRequest, doneChan chan any) (chan *qbtypes.RawRow, chan error) {
traceOperatorQueryIndex := rangeRequest.TraceOperatorQueryIndex()
queries := rangeRequest.CompositeQuery.Queries
// If the trace operator query is present, mark the queries other than trace operator as disabled
if traceOperatorQueryIndex > -1 {
for idx := range len(queries) {
if idx != traceOperatorQueryIndex {
queries[idx].SetDisabled(true)
}
}
}
spec := rangeRequest.CompositeQuery.Queries[0].Spec.(qbtypes.QueryBuilderQuery[qbtypes.LogAggregation])
rowCountLimit := spec.Limit
rowChan := make(chan *qbtypes.RawRow, 1)
errChan := make(chan error, 1)
@@ -48,62 +38,52 @@ func (m *Module) ExportRawData(ctx context.Context, orgID valuer.UUID, rangeRequ
defer close(errChan)
defer close(rowChan)
if traceOperatorQueryIndex > -1 {
// If the trace operator query is present, we need to export the data for the trace operator query only
exportRawDataForSingleQuery(m.querier, contextWithTimeout, orgID, rangeRequest, rowChan, errChan, doneChan, traceOperatorQueryIndex)
} else {
// If the trace operator query is not present, we need to export the data for the first query only
exportRawDataForSingleQuery(m.querier, contextWithTimeout, orgID, rangeRequest, rowChan, errChan, doneChan, 0)
rowCount := 0
for rowCount < rowCountLimit {
spec.Limit = min(ChunkSize, rowCountLimit-rowCount)
spec.Offset = rowCount
rangeRequest.CompositeQuery.Queries[0].Spec = spec
response, err := m.querier.QueryRange(contextWithTimeout, orgID, rangeRequest)
if err != nil {
errChan <- err
return
}
newRowsCount := 0
for _, result := range response.Data.Results {
resultData, ok := result.(*qbtypes.RawData)
if !ok {
errChan <- errors.NewInternalf(errors.CodeInternal, "expected RawData, got %T", result)
return
}
newRowsCount += len(resultData.Rows)
for _, row := range resultData.Rows {
select {
case rowChan <- row:
case <-doneChan:
return
case <-ctx.Done():
errChan <- ctx.Err()
return
}
}
}
// Break if we did not receive any new rows
if newRowsCount == 0 {
return
}
rowCount += newRowsCount
}
}()
return rowChan, errChan
}
func exportRawDataForSingleQuery(querier querier.Querier, ctx context.Context, orgID valuer.UUID, rangeRequest *qbtypes.QueryRangeRequest, rowChan chan *qbtypes.RawRow, errChan chan error, doneChan chan any, queryIndex int) {
queries := rangeRequest.CompositeQuery.Queries
rowCountLimit := queries[queryIndex].GetLimit()
rowCount := 0
for rowCount < rowCountLimit {
chunkSize := min(ChunkSize, rowCountLimit-rowCount)
queries[queryIndex].SetLimit(chunkSize)
queries[queryIndex].SetOffset(rowCount)
response, err := querier.QueryRange(ctx, orgID, rangeRequest)
if err != nil {
errChan <- err
return
}
newRowsCount := 0
for _, result := range response.Data.Results {
resultData, ok := result.(*qbtypes.RawData)
if !ok {
errChan <- errors.NewInternalf(errors.CodeInternal, "expected RawData, got %T", result)
return
}
newRowsCount += len(resultData.Rows)
for _, row := range resultData.Rows {
select {
case rowChan <- row:
case <-doneChan:
return
case <-ctx.Done():
errChan <- ctx.Err()
return
}
}
}
rowCount += newRowsCount
// Stop if we received fewer rows than requested — no more data available
if newRowsCount < chunkSize {
return
}
}
}

View File

@@ -80,16 +80,11 @@ func (q *builderQuery[T]) Fingerprint() string {
case qbtypes.LogAggregation:
aggParts = append(aggParts, a.Expression)
case qbtypes.MetricAggregation:
var spaceAggParamStr string
if a.ComparisonSpaceAggregationParam != nil {
spaceAggParamStr = a.ComparisonSpaceAggregationParam.StringValue()
}
aggParts = append(aggParts, fmt.Sprintf("%s:%s:%s:%s:%s",
aggParts = append(aggParts, fmt.Sprintf("%s:%s:%s:%s",
a.MetricName,
a.Temporality.StringValue(),
a.TimeAggregation.StringValue(),
a.SpaceAggregation.StringValue(),
spaceAggParamStr,
))
}
}

View File

@@ -276,17 +276,15 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
// Fetch temporality for all metrics at once
var metricTemporality map[string]metrictypes.Temporality
var metricTypes map[string]metrictypes.Type
if len(metricNames) > 0 {
var err error
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
metricTemporality, err = q.metadataStore.FetchTemporalityMulti(ctx, req.Start, req.End, metricNames...)
if err != nil {
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
// Continue without temporality - statement builder will handle unspecified
metricTemporality = make(map[string]metrictypes.Temporality)
metricTypes = make(map[string]metrictypes.Type)
}
q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes)
q.logger.DebugContext(ctx, "fetched metric temporalities", "metric_temporality", metricTemporality)
}
queries := make(map[string]qbtypes.Query)
@@ -382,12 +380,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
if spec.Aggregations[i].Temporality == metrictypes.Unknown {
spec.Aggregations[i].Temporality = metrictypes.Unspecified
}
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
if foundMetricType, ok := metricTypes[spec.Aggregations[i].MetricName]; ok && foundMetricType != metrictypes.UnspecifiedType {
spec.Aggregations[i].Type = foundMetricType
}
}
}
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)

View File

@@ -1046,7 +1046,19 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
}
processingPostCache := time.Now()
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint := tracedetail.GetSelectedSpans(req.UncollapsedSpans, req.SelectedSpanID, traceRoots, spanIdToSpanNodeMap, req.IsSelectedSpanIDUnCollapsed)
limit := min(req.Limit, tracedetail.MaxLimitToSelectAllSpans)
selectAllSpans := totalSpans <= uint64(limit)
var (
selectedSpans []*model.Span
uncollapsedSpans []string
rootServiceName, rootServiceEntryPoint string
)
if selectAllSpans {
selectedSpans, rootServiceName, rootServiceEntryPoint = tracedetail.GetAllSpans(traceRoots)
} else {
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint = tracedetail.GetSelectedSpans(req.UncollapsedSpans, req.SelectedSpanID, traceRoots, spanIdToSpanNodeMap, req.IsSelectedSpanIDUnCollapsed)
}
zap.L().Info("getWaterfallSpansForTraceWithMetadata: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID))
// convert start timestamp to millis because right now frontend is expecting it in millis
@@ -1059,7 +1071,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
}
response.Spans = selectedSpans
response.UncollapsedSpans = uncollapsedSpans
response.UncollapsedSpans = uncollapsedSpans // ignoring if all spans are returning
response.StartTimestampMillis = startTime / 1000000
response.EndTimestampMillis = endTime / 1000000
response.TotalSpansCount = totalSpans
@@ -1068,6 +1080,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
response.RootServiceEntryPoint = rootServiceEntryPoint
response.ServiceNameToTotalDurationMap = serviceNameToTotalDurationMap
response.HasMissingSpans = hasMissingSpans
response.HasMore = !selectAllSpans
return response, nil
}
@@ -1199,7 +1212,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID
}
}
selectedSpans = tracedetail.GetSelectedSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap)
selectedSpans = tracedetail.GetAllSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap)
traceCache := model.GetFlamegraphSpansForTraceCache{
StartTime: startTime,
EndTime: endTime,
@@ -1216,12 +1229,20 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID
}
processingPostCache := time.Now()
selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, startTime, endTime)
zap.L().Info("getFlamegraphSpansForTrace: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID))
selectedSpansForRequest := selectedSpans
limit := min(req.Limit, tracedetail.MaxLimitWithoutSampling)
totalSpanCount := tracedetail.GetTotalSpanCount(selectedSpans)
if totalSpanCount > uint64(limit) {
boundaryStart, boundaryEnd := utils.MilliToNano(req.BoundaryStartTS), utils.MilliToNano(req.BoundaryEndTS)
selectedSpansForRequest = tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, boundaryStart, boundaryEnd)
}
zap.L().Info("getFlamegraphSpansForTrace: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID),
zap.Uint64("totalSpanCount", totalSpanCount))
trace.Spans = selectedSpansForRequest
trace.StartTimestampMillis = startTime / 1000000
trace.EndTimestampMillis = endTime / 1000000
trace.HasMore = totalSpanCount > uint64(limit)
return trace, nil
}

View File

@@ -572,6 +572,9 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
aH.LicensingAPI.Activate(rw, req)
})).Methods(http.MethodGet)
// Export
router.HandleFunc("/api/v1/export_raw_data", am.ViewAccess(aH.Signoz.Handlers.RawDataExport.ExportRawData)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/span_percentile", am.ViewAccess(aH.Signoz.Handlers.SpanPercentile.GetSpanPercentileDetails)).Methods(http.MethodPost)
// Query Filter Analyzer api used to extract metric names and grouping columns from a query

View File

@@ -7,9 +7,11 @@ import (
)
var (
SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH float64 = 50
SPAN_LIMIT_PER_LEVEL int = 100
TIMESTAMP_SAMPLING_BUCKET_COUNT int = 50
flamegraphSpanLevelLimit float64 = 50
flamegraphSpanLimitPerLevel int = 1000
flamegraphSamplingBucketCount int = 500
MaxLimitWithoutSampling uint = 120_000
)
func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool {
@@ -52,7 +54,8 @@ func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId st
return selectedSpanLevel
}
func GetSelectedSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan {
// GetAllSpansForFlamegraph groups all spans as per their level
func GetAllSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan {
var traceIdLevelledFlamegraph = map[string]map[int64][]*model.FlamegraphSpan{}
selectedSpans := [][]*model.FlamegraphSpan{}
@@ -100,7 +103,7 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected
})
// pick the top 5 latency spans
for idx := range 5 {
for idx := range 100 {
sampledSpans = append(sampledSpans, spans[idx])
}
@@ -110,6 +113,7 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected
for _idx, span := range spans {
if span.SpanID == selectedSpanID {
idx = _idx
break
}
}
if idx != -1 {
@@ -117,17 +121,17 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected
}
}
bucketSize := (endTime - startTime) / uint64(TIMESTAMP_SAMPLING_BUCKET_COUNT)
bucketSize := (endTime - startTime) / uint64(flamegraphSamplingBucketCount)
if bucketSize == 0 {
bucketSize = 1
}
bucketedSpans := make([][]*model.FlamegraphSpan, 50)
bucketedSpans := make([][]*model.FlamegraphSpan, flamegraphSamplingBucketCount)
for _, span := range spans {
if span.TimeUnixNano >= startTime && span.TimeUnixNano <= endTime {
bucketIndex := int((span.TimeUnixNano - startTime) / bucketSize)
if bucketIndex >= 0 && bucketIndex < 50 {
if bucketIndex >= 0 && bucketIndex < flamegraphSamplingBucketCount {
bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span)
}
}
@@ -156,8 +160,8 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
selectedIndex = FindIndexForSelectedSpan(selectedSpans, selectedSpanID)
}
lowerLimit := selectedIndex - int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.4)
upperLimit := selectedIndex + int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.6)
lowerLimit := selectedIndex - int(flamegraphSpanLevelLimit*0.4)
upperLimit := selectedIndex + int(flamegraphSpanLevelLimit*0.6)
if lowerLimit < 0 {
upperLimit = upperLimit - lowerLimit
@@ -174,7 +178,7 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
}
for i := lowerLimit; i < upperLimit; i++ {
if len(selectedSpans[i]) > SPAN_LIMIT_PER_LEVEL {
if len(selectedSpans[i]) > flamegraphSpanLimitPerLevel {
_spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime)
selectedSpansForRequest = append(selectedSpansForRequest, _spans)
} else {
@@ -184,3 +188,12 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
return selectedSpansForRequest
}
func GetTotalSpanCount(spans [][]*model.FlamegraphSpan) uint64 {
levelCount := len(spans)
spanCount := uint64(0)
for i := range levelCount {
spanCount += uint64(len(spans[i]))
}
return spanCount
}

View File

@@ -9,6 +9,9 @@ import (
var (
SPAN_LIMIT_PER_REQUEST_FOR_WATERFALL float64 = 500
maxDepthForSelectedSpanChildren int = 5
MaxLimitToSelectAllSpans uint = 10_000
)
type Interval struct {
@@ -88,8 +91,11 @@ func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, un
return isPresentInSubtreeForTheNode, spansFromRootToNode
}
func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string) []*model.Span {
func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string,
depthFromSelectedSpan int, isSelectedSpanIDUnCollapsed bool, selectAllSpan bool) ([]*model.Span, []string) {
preOrderTraversal := []*model.Span{}
autoExpandedSpans := []string{}
// sort the children to maintain the order across requests
sort.Slice(span.Children, func(i, j int) bool {
@@ -126,15 +132,40 @@ func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, is
preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren)
}
nextDepthFromSelectedSpan := -1
if span.SpanID == selectedSpanId && isSelectedSpanIDUnCollapsed {
nextDepthFromSelectedSpan = 1
} else if depthFromSelectedSpan >= 1 && depthFromSelectedSpan < maxDepthForSelectedSpanChildren {
nextDepthFromSelectedSpan = depthFromSelectedSpan + 1
}
for index, child := range span.Children {
_childTraversal := traverseTrace(child, uncollapsedSpans, level+1, isPartOfPreOrder && slices.Contains(uncollapsedSpans, span.SpanID), index != (len(span.Children)-1), selectedSpanId)
// A child is included in the pre-order output if its parent is uncollapsed
// OR if the child falls within MAX_DEPTH_FOR_SELECTED_SPAN_CHILDREN levels
// below the selected span.
isChildWithinMaxDepth := nextDepthFromSelectedSpan >= 1
isAlreadyUncollapsed := slices.Contains(uncollapsedSpans, span.SpanID)
childIsPartOfPreOrder := isPartOfPreOrder && (isAlreadyUncollapsed || isChildWithinMaxDepth)
if selectAllSpan {
childIsPartOfPreOrder = true
}
if isPartOfPreOrder && isChildWithinMaxDepth && !isAlreadyUncollapsed {
if !slices.Contains(autoExpandedSpans, span.SpanID) {
autoExpandedSpans = append(autoExpandedSpans, span.SpanID)
}
}
_childTraversal, _autoExpanded := traverseTrace(child, uncollapsedSpans, level+1, childIsPartOfPreOrder, index != (len(span.Children)-1), selectedSpanId,
nextDepthFromSelectedSpan, isSelectedSpanIDUnCollapsed, selectAllSpan)
preOrderTraversal = append(preOrderTraversal, _childTraversal...)
autoExpandedSpans = append(autoExpandedSpans, _autoExpanded...)
nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
span.SubTreeNodeCount += child.SubTreeNodeCount + 1
}
nodeWithoutChildren.SubTreeNodeCount += 1
return preOrderTraversal
return preOrderTraversal, autoExpandedSpans
}
@@ -168,7 +199,13 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
_, spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, selectedSpanID, updatedUncollapsedSpans, isSelectedSpanIDUnCollapsed)
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spansFromRootToNode...)
_preOrderTraversal := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID)
_preOrderTraversal, _autoExpanded := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID, -1, isSelectedSpanIDUnCollapsed, false)
// Merge auto-expanded spans into updatedUncollapsedSpans for returning in response
for _, spanID := range _autoExpanded {
if !slices.Contains(updatedUncollapsedSpans, spanID) {
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spanID)
}
}
_selectedSpanIndex := findIndexForSelectedSpanFromPreOrder(_preOrderTraversal, selectedSpanID)
if _selectedSpanIndex != -1 {
@@ -212,3 +249,17 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
return preOrderTraversal[startIndex:endIndex], updatedUncollapsedSpans, rootServiceName, rootServiceEntryPoint
}
func GetAllSpans(traceRoots []*model.Span) (spans []*model.Span, rootServiceName, rootEntryPoint string) {
for _, root := range traceRoots {
childSpans, _ := traverseTrace(root, nil, 0, true, false, "", -1, false, true)
spans = append(spans, childSpans...)
if rootServiceName == "" {
rootServiceName = root.ServiceName
}
if rootEntryPoint == "" {
rootEntryPoint = root.Name
}
}
return
}

View File

@@ -333,10 +333,14 @@ type GetWaterfallSpansForTraceWithMetadataParams struct {
SelectedSpanID string `json:"selectedSpanId"`
IsSelectedSpanIDUnCollapsed bool `json:"isSelectedSpanIDUnCollapsed"`
UncollapsedSpans []string `json:"uncollapsedSpans"`
Limit uint `json:"limit"`
}
type GetFlamegraphSpansForTraceParams struct {
SelectedSpanID string `json:"selectedSpanId"`
SelectedSpanID string `json:"selectedSpanId"`
Limit uint `json:"limit"`
BoundaryStartTS uint64 `json:"boundaryStartTsMilli"`
BoundaryEndTS uint64 `json:"boundarEndTsMilli"`
}
type SpanFilterParams struct {

View File

@@ -329,6 +329,7 @@ type GetWaterfallSpansForTraceWithMetadataResponse struct {
HasMissingSpans bool `json:"hasMissingSpans"`
// this is needed for frontend and query service sync
UncollapsedSpans []string `json:"uncollapsedSpans"`
HasMore bool `json:"hasMore"`
}
type GetFlamegraphSpansForTraceResponse struct {
@@ -336,6 +337,7 @@ type GetFlamegraphSpansForTraceResponse struct {
EndTimestampMillis uint64 `json:"endTimestampMillis"`
DurationNano uint64 `json:"durationNano"`
Spans [][]*FlamegraphSpan `json:"spans"`
HasMore bool `json:"hasMore"`
}
type OtelSpanRef struct {

View File

@@ -17,3 +17,7 @@ func Elapsed(funcName string, args map[string]interface{}) func() {
zap.L().Info("Elapsed time", zapFields...)
}
}
func MilliToNano(milliTS uint64) uint64 {
return milliTS * 1000_000
}

View File

@@ -22,7 +22,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -58,7 +57,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ gateway.Handler }{},
struct{ fields.Handler }{},
struct{ authz.Handler }{},
struct{ rawdataexport.Handler }{},
struct{ zeus.Handler }{},
struct{ querier.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})

View File

@@ -169,7 +169,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddAnonymousPublicDashboardTransactionFactory(sqlstore),
sqlmigration.NewAddRootUserFactory(sqlstore, sqlschema),
sqlmigration.NewAddUserEmailOrgIDIndexFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateRulesV4ToV5Factory(sqlstore, telemetryStore),
)
}
@@ -253,7 +252,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.GatewayHandler,
handlers.Fields,
handlers.AuthzHandler,
handlers.RawDataExport,
handlers.ZeusHandler,
handlers.QuerierHandler,
),

View File

@@ -1,209 +0,0 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"log/slog"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type migrateRulesV4ToV5 struct {
store sqlstore.SQLStore
telemetryStore telemetrystore.TelemetryStore
logger *slog.Logger
}
func NewMigrateRulesV4ToV5Factory(
store sqlstore.SQLStore,
telemetryStore telemetrystore.TelemetryStore,
) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("migrate_rules_post_deprecation"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &migrateRulesV4ToV5{
store: store,
telemetryStore: telemetryStore,
logger: ps.Logger,
}, nil
})
}
func (migration *migrateRulesV4ToV5) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *migrateRulesV4ToV5) getLogDuplicateKeys(ctx context.Context) ([]string, error) {
query := `
SELECT name
FROM (
SELECT DISTINCT name FROM signoz_logs.distributed_logs_attribute_keys
INTERSECT
SELECT DISTINCT name FROM signoz_logs.distributed_logs_resource_keys
)
ORDER BY name
`
rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query)
if err != nil {
migration.logger.WarnContext(ctx, "failed to query log duplicate keys", "error", err)
return nil, nil
}
defer rows.Close()
var keys []string
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
migration.logger.WarnContext(ctx, "failed to scan log duplicate key", "error", err)
continue
}
keys = append(keys, key)
}
return keys, nil
}
func (migration *migrateRulesV4ToV5) getTraceDuplicateKeys(ctx context.Context) ([]string, error) {
query := `
SELECT tagKey
FROM signoz_traces.distributed_span_attributes_keys
WHERE tagType IN ('tag', 'resource')
GROUP BY tagKey
HAVING COUNT(DISTINCT tagType) > 1
ORDER BY tagKey
`
rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query)
if err != nil {
migration.logger.WarnContext(ctx, "failed to query trace duplicate keys", "error", err)
return nil, nil
}
defer rows.Close()
var keys []string
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
migration.logger.WarnContext(ctx, "failed to scan trace duplicate key", "error", err)
continue
}
keys = append(keys, key)
}
return keys, nil
}
func (migration *migrateRulesV4ToV5) Up(ctx context.Context, db *bun.DB) error {
logsKeys, err := migration.getLogDuplicateKeys(ctx)
if err != nil {
return err
}
tracesKeys, err := migration.getTraceDuplicateKeys(ctx)
if err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
var rules []struct {
ID string `bun:"id"`
Data map[string]any `bun:"data"`
}
err = tx.NewSelect().
Table("rule").
Column("id", "data").
Scan(ctx, &rules)
if err != nil {
if err == sql.ErrNoRows {
return nil
}
return err
}
alertsMigrator := transition.NewAlertMigrateV5(migration.logger, logsKeys, tracesKeys)
count := 0
for _, rule := range rules {
version, _ := rule.Data["version"].(string)
if version == "v5" {
continue
}
if version == "" {
migration.logger.WarnContext(ctx, "unexpected empty version for rule", "rule_id", rule.ID)
}
migration.logger.InfoContext(ctx, "migrating rule v4 to v5", "rule_id", rule.ID, "current_version", version)
// Check if the queries envelope already exists and is non-empty
hasQueriesEnvelope := false
if condition, ok := rule.Data["condition"].(map[string]any); ok {
if compositeQuery, ok := condition["compositeQuery"].(map[string]any); ok {
if queries, ok := compositeQuery["queries"].([]any); ok && len(queries) > 0 {
hasQueriesEnvelope = true
}
}
}
if hasQueriesEnvelope {
// already has queries envelope, just bump version
// this is because user made a mistake of choosing version
migration.logger.InfoContext(ctx, "rule already has queries envelope, bumping version", "rule_id", rule.ID)
rule.Data["version"] = "v5"
} else {
// old format, run full migration
migration.logger.InfoContext(ctx, "rule has old format, running full migration", "rule_id", rule.ID)
updated := alertsMigrator.Migrate(ctx, rule.Data)
if !updated {
migration.logger.WarnContext(ctx, "expected updated to be true but got false", "rule_id", rule.ID)
continue
}
rule.Data["version"] = "v5"
}
dataJSON, err := json.Marshal(rule.Data)
if err != nil {
return err
}
_, err = tx.NewUpdate().
Table("rule").
Set("data = ?", string(dataJSON)).
Where("id = ?", rule.ID).
Exec(ctx)
if err != nil {
return err
}
count++
}
if count != 0 {
migration.logger.InfoContext(ctx, "migrate v4 alerts", "count", count)
}
return tx.Commit()
}
func (migration *migrateRulesV4ToV5) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz-otel-collector/utils"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
@@ -112,7 +113,7 @@ func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
for _, fieldKey := range fieldKeys {
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
fieldKey.Materialized = promoted[promotedKey]
fieldKey.Materialized = promoted.Contains(promotedKey)
fieldKey.Indexes = indexes[fieldKey.Name]
}
@@ -294,6 +295,33 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
return indexes, nil
}
func (t *telemetryMetaStore) ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error) {
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
pathConditions := []string{}
for _, path := range paths {
pathConditions = append(pathConditions, sb.Equal("path", path))
}
sb.Where(sb.Or(pathConditions...))
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, CodeFailLoadPromotedPaths, "failed to load promoted paths")
}
defer rows.Close()
next := make(map[string]struct{})
for rows.Next() {
var path string
if err := rows.Scan(&path); err != nil {
return nil, errors.WrapInternalf(err, CodeFailLoadPromotedPaths, "failed to scan promoted path")
}
next[path] = struct{}{}
}
return next, nil
}
// TODO(Piyush): Remove this if not used in future
func (t *telemetryMetaStore) ListJSONValues(ctx context.Context, path string, limit int) (*telemetrytypes.TelemetryFieldValues, bool, error) {
path = CleanPathPrefixes(path)
@@ -456,12 +484,11 @@ func derefValue(v any) any {
return val.Interface()
}
// IsPathPromoted checks if a specific path is promoted (Column Evolution table: field_name for logs body).
// IsPathPromoted checks if a specific path is promoted
func (t *telemetryMetaStore) IsPathPromoted(ctx context.Context, path string) (bool, error) {
split := strings.Split(path, telemetrytypes.ArraySep)
pathSegment := split[0]
query := fmt.Sprintf("SELECT 1 FROM %s.%s WHERE signal = ? AND column_name = ? AND field_context = ? AND field_name = ? LIMIT 1", DBName, PromotedPathsTableName)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, telemetrytypes.FieldContextBody, pathSegment)
query := fmt.Sprintf("SELECT 1 FROM %s.%s WHERE path = ? LIMIT 1", DBName, PromotedPathsTableName)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, split[0])
if err != nil {
return false, errors.WrapInternalf(err, CodeFailCheckPathPromoted, "failed to check if path %s is promoted", path)
}
@@ -470,24 +497,15 @@ func (t *telemetryMetaStore) IsPathPromoted(ctx context.Context, path string) (b
return rows.Next(), nil
}
// GetPromotedPaths returns promoted paths from the Column Evolution table (field_name for logs body).
func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error) {
sb := sqlbuilder.Select("field_name").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
conditions := []string{
sb.Equal("signal", telemetrytypes.SignalLogs),
sb.Equal("column_name", telemetrylogs.LogsV2BodyPromotedColumn),
sb.Equal("field_context", telemetrytypes.FieldContextBody),
sb.NotEqual("field_name", "__all__"),
// GetPromotedPaths checks if a specific path is promoted
func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...string) (*utils.ConcurrentSet[string], error) {
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
pathConditions := []string{}
for _, path := range paths {
split := strings.Split(path, telemetrytypes.ArraySep)
pathConditions = append(pathConditions, sb.Equal("path", split[0]))
}
if len(paths) > 0 {
pathArgs := make([]interface{}, len(paths))
for i, path := range paths {
split := strings.Split(path, telemetrytypes.ArraySep)
pathArgs[i] = split[0]
}
conditions = append(conditions, sb.In("field_name", pathArgs))
}
sb.Where(sb.And(conditions...))
sb.Where(sb.Or(pathConditions...))
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
@@ -496,13 +514,13 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
}
defer rows.Close()
promotedPaths := make(map[string]bool)
promotedPaths := utils.NewConcurrentSet[string]()
for rows.Next() {
var fieldName string
if err := rows.Scan(&fieldName); err != nil {
var path string
if err := rows.Scan(&path); err != nil {
return nil, errors.WrapInternalf(err, CodeFailCheckPathPromoted, "failed to scan promoted path")
}
promotedPaths[fieldName] = true
promotedPaths.Insert(path)
}
return promotedPaths, nil
@@ -516,22 +534,21 @@ func CleanPathPrefixes(path string) string {
return path
}
// PromotePaths inserts promoted paths into the Column Evolution table (same schema as signoz-otel-collector metadata_migrations).
func (t *telemetryMetaStore) PromotePaths(ctx context.Context, paths ...string) error {
batch, err := t.telemetrystore.ClickhouseDB().PrepareBatch(ctx,
fmt.Sprintf("INSERT INTO %s.%s (signal, column_name, column_type, field_context, field_name, version, release_time) VALUES", DBName,
fmt.Sprintf("INSERT INTO %s.%s (path, created_at) VALUES", DBName,
PromotedPathsTableName))
if err != nil {
return errors.WrapInternalf(err, CodeFailedToPrepareBatch, "failed to prepare batch")
}
releaseTime := time.Now().UnixNano()
nowMs := uint64(time.Now().UnixMilli())
for _, p := range paths {
trimmed := strings.TrimSpace(p)
if trimmed == "" {
continue
}
if err := batch.Append(telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, "JSON()", telemetrytypes.FieldContextBody, trimmed, 0, releaseTime); err != nil {
if err := batch.Append(trimmed, nowMs); err != nil {
_ = batch.Abort()
return errors.WrapInternalf(err, CodeFailedToAppendPath, "failed to append path")
}

View File

@@ -1616,52 +1616,40 @@ func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, queryTimeRang
}
func (t *telemetryMetaStore) FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
temporalities, _, err := t.FetchTemporalityAndTypeMulti(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
return temporalities, err
}
func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
if len(metricNames) == 0 {
return make(map[string]metrictypes.Temporality), make(map[string]metrictypes.Type), nil
return make(map[string]metrictypes.Temporality), nil
}
temporalities := make(map[string]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
metricsTemporality, metricTypes, err := t.fetchMetricsTemporalityAndType(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
result := make(map[string]metrictypes.Temporality)
metricsTemporality, err := t.fetchMetricsTemporality(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
if err != nil {
return nil, nil, err
return nil, err
}
// TODO: return error after table migration are run
meterMetricsTemporality, meterMetricsTypes, _ := t.fetchMeterSourceMetricsTemporalityAndType(ctx, metricNames...)
meterMetricsTemporality, _ := t.fetchMeterSourceMetricsTemporality(ctx, metricNames...)
// For metrics not found in the database, set to Unknown
for _, metricName := range metricNames {
if temporality, exists := metricsTemporality[metricName]; exists && len(temporality) > 0 {
if len(temporality) > 1 {
temporalities[metricName] = metrictypes.Multiple
result[metricName] = metrictypes.Multiple
} else {
temporalities[metricName] = temporality[0]
result[metricName] = temporality[0]
}
} else if temporality, exists := meterMetricsTemporality[metricName]; exists {
temporalities[metricName] = temporality
} else {
temporalities[metricName] = metrictypes.Unknown
continue
}
if metricType, exists := metricTypes[metricName]; exists {
types[metricName] = metricType
} else if meterMetricType, exists := meterMetricsTypes[metricName]; exists {
types[metricName] = meterMetricType
} else {
types[metricName] = metrictypes.UnspecifiedType
if temporality, exists := meterMetricsTemporality[metricName]; exists {
result[metricName] = temporality
continue
}
result[metricName] = metrictypes.Unknown
}
return temporalities, types, nil
return result, nil
}
func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, map[string]metrictypes.Type, error) {
temporalities := make(map[string][]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
func (t *telemetryMetaStore) fetchMetricsTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, error) {
result := make(map[string][]metrictypes.Temporality)
adjustedStartTs, adjustedEndTs, tsTableName, _ := telemetrymetrics.WhichTSTableToUse(queryTimeRangeStartTs, queryTimeRangeEndTs, nil)
@@ -1672,8 +1660,6 @@ func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context,
sb := sqlbuilder.Select(
"metric_name",
"temporality",
"any(type) AS type",
"any(is_monotonic) as is_monotonic",
).
From(t.metricsDBName + "." + tsTableName)
@@ -1692,42 +1678,47 @@ func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context,
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
}
defer rows.Close()
// Process results
for rows.Next() {
var metricName string
var metricName, temporalityStr string
if err := rows.Scan(&metricName, &temporalityStr); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
}
// Convert string to Temporality type
var temporality metrictypes.Temporality
var metricType metrictypes.Type
var isMonotonic bool
if err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
switch temporalityStr {
case "Delta":
temporality = metrictypes.Delta
case "Cumulative":
temporality = metrictypes.Cumulative
case "Unspecified":
temporality = metrictypes.Unspecified
default:
// Unknown or empty temporality
temporality = metrictypes.Unknown
}
if temporality != metrictypes.Unknown {
temporalities[metricName] = append(temporalities[metricName], temporality)
result[metricName] = append(result[metricName], temporality)
}
if metricType == metrictypes.SumType && !isMonotonic {
metricType = metrictypes.GaugeType
}
types[metricName] = metricType
}
if err := rows.Err(); err != nil {
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
}
return temporalities, types, nil
return result, nil
}
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
temporalities := make(map[string]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporality(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, error) {
result := make(map[string]metrictypes.Temporality)
sb := sqlbuilder.Select(
"metric_name",
"argMax(temporality, unix_milli) as temporality",
"any(type) AS type",
).From(t.meterDBName + "." + t.meterFieldsTblName)
// Filter by metric names (in the temporality column due to data mix-up)
@@ -1742,27 +1733,35 @@ func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx conte
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch meter metric temporality")
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch meter metric temporality")
}
defer rows.Close()
// Process results
for rows.Next() {
var metricName string
var metricName, temporalityStr string
if err := rows.Scan(&metricName, &temporalityStr); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
}
// Convert string to Temporality type
var temporality metrictypes.Temporality
var metricType metrictypes.Type
var isMonotonic bool
if err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
switch temporalityStr {
case "Delta":
temporality = metrictypes.Delta
case "Cumulative":
temporality = metrictypes.Cumulative
case "Unspecified":
temporality = metrictypes.Unspecified
default:
// Unknown or empty temporality
temporality = metrictypes.Unknown
}
if metricType == metrictypes.SumType && !isMonotonic {
metricType = metrictypes.GaugeType
}
temporalities[metricName] = temporality
types[metricName] = metricType
result[metricName] = temporality
}
return temporalities, types, nil
return result, nil
}
// chunkSizeFirstSeenMetricMetadata limits the number of tuples per SQL query to avoid hitting the max_query_size limit.

View File

@@ -7,7 +7,6 @@ const (
AttributesMetadataTableName = "distributed_attributes_metadata"
AttributesMetadataLocalTableName = "attributes_metadata"
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"
PromotedPathsTableName = otelcollectorconst.DistributedPromotedPathsTable
SkipIndexTableName = "system.data_skipping_indices"
)

View File

@@ -123,7 +123,8 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
origTimeAgg := query.Aggregations[0].TimeAggregation
origGroupBy := slices.Clone(query.GroupBy)
if query.Aggregations[0].Type == metrictypes.HistogramType {
if query.Aggregations[0].SpaceAggregation.IsPercentile() &&
query.Aggregations[0].Type != metrictypes.ExpHistogramType {
// add le in the group by if doesn't exist
leExists := false
for _, g := range query.GroupBy {
@@ -153,11 +154,7 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
}
// make the time aggregation rate and space aggregation sum
if query.Aggregations[0].SpaceAggregation.IsPercentile() {
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationRate
} else {
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationIncrease
}
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationRate
query.Aggregations[0].SpaceAggregation = metrictypes.SpaceAggregationSum
}
@@ -554,9 +551,6 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
cteArgs [][]any,
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
) (*qbtypes.Statement, error) {
metricType := query.Aggregations[0].Type
spaceAgg := query.Aggregations[0].SpaceAggregation
combined := querybuilder.CombineCTEs(cteFragments)
var args []any
@@ -566,8 +560,12 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
sb := sqlbuilder.NewSelectBuilder()
if metricType == metrictypes.HistogramType && spaceAgg.IsPercentile() {
quantile := query.Aggregations[0].SpaceAggregation.Percentile()
var quantile float64
if query.Aggregations[0].SpaceAggregation.IsPercentile() {
quantile = query.Aggregations[0].SpaceAggregation.Percentile()
}
if quantile != 0 && query.Aggregations[0].Type != metrictypes.ExpHistogramType {
sb.Select("ts")
for _, g := range query.GroupBy {
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
@@ -579,36 +577,12 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
sb.From("__spatial_aggregation_cte")
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
sb.GroupBy("ts")
if query.Having != nil && query.Having.Expression != "" {
rewriter := querybuilder.NewHavingExpressionRewriter()
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
sb.Having(rewrittenExpr)
}
} else if metricType == metrictypes.HistogramType && spaceAgg == metrictypes.SpaceAggregationCount && query.Aggregations[0].ComparisonSpaceAggregationParam != nil {
sb.Select("ts")
for _, g := range query.GroupBy {
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
aggQuery, err := AggregationQueryForHistogramCountWithParams(query.Aggregations[0].ComparisonSpaceAggregationParam)
if err != nil {
return nil, err
}
sb.SelectMore(aggQuery)
sb.From("__spatial_aggregation_cte")
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
sb.GroupBy("ts")
if query.Having != nil && query.Having.Expression != "" {
rewriter := querybuilder.NewHavingExpressionRewriter()
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
sb.Having(rewrittenExpr)
}
} else {
// for count aggregation on histograms with no params, the exact result of spatial aggregation can be sent forward
sb.Select("*")
sb.From("__spatial_aggregation_cte")
if query.Having != nil && query.Having.Expression != "" {
@@ -619,9 +593,6 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
}
sb.OrderBy(querybuilder.GroupByKeys(query.GroupBy)...)
sb.OrderBy("ts")
if metricType == metrictypes.HistogramType && spaceAgg == metrictypes.SpaceAggregationCount && query.Aggregations[0].ComparisonSpaceAggregationParam == nil {
sb.OrderBy("toFloat64(le)")
}
q, a := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return &qbtypes.Statement{Query: combined + q, Args: append(args, a...)}, nil

View File

@@ -1,7 +1,6 @@
package telemetrymetrics
import (
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -309,20 +308,3 @@ func AggregationColumnForSamplesTable(
}
return aggregationColumn, nil
}
func AggregationQueryForHistogramCountWithParams(param *metrictypes.ComparisonSpaceAggregationParam) (string, error) {
if param == nil {
return "", errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no aggregation param provided for histogram count")
}
histogramCountThreshold := param.Threshold
switch param.Operater {
case "<=":
return fmt.Sprintf("argMaxIf(value, toFloat64(le), toFloat64(le) <= %f) + (argMinIf(value, toFloat64(le), toFloat64(le) > %f) - argMaxIf(value, toFloat64(le), toFloat64(le) <= %f)) * (%f - maxIf(toFloat64(le), toFloat64(le) <= %f)) / (minIf(toFloat64(le), toFloat64(le) > %f) - maxIf(toFloat64(le), toFloat64(le) <= %f)) AS value", histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold), nil
case ">":
return fmt.Sprintf("argMax(value, toFloat64(le)) - (argMaxIf(value, toFloat64(le), toFloat64(le) <= %f) + (argMinIf(value, toFloat64(le), toFloat64(le) > %f) - argMaxIf(value, toFloat64(le), toFloat64(le) <= %f)) * (%f - maxIf(toFloat64(le), toFloat64(le) <= %f)) / (minIf(toFloat64(le), toFloat64(le) > %f) - maxIf(toFloat64(le), toFloat64(le) <= %f))) AS value", histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold), nil
default:
return "", errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid space aggregation operator, should be one of the following: [`<=`, `>`]")
}
}

View File

@@ -1,120 +0,0 @@
package exporttypes
import (
"strings"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// ExportRawDataQueryParams represents the query parameters for the export raw data endpoint
type ExportRawDataQueryParams struct {
ExportRawDataFormatQueryParam
// Signal specifies the type of data to export: "logs" or "traces"
Signal telemetrytypes.Signal `query:"signal" enum:"logs,traces" required:"true"`
// Source specifies the type of data to export: "logs" or "traces"
// Deprecated: Use Signal instead.
Source string `query:"source" deprecated:"true"`
// Start is the start time for the query (Unix timestamp in nanoseconds)
Start uint64 `query:"start"`
// End is the end time for the query (Unix timestamp in nanoseconds)
End uint64 `query:"end"`
// Limit specifies the maximum number of rows to export
Limit int `query:"limit,default=10000" default:"10000" minimum:"1" maximum:"50000"`
// Filter is a filter expression to apply to the query
FilterString string `query:"filter" deprecated:"true"`
Filter qbtypes.Filter `query:"filterExpression"`
// Columns specifies the columns to include in the export
// Format: ["context.field:type", "context.field", "field"]
Columns []string `query:"columns" deprecated:"true"`
// SelectFields specifies the columns to include in the export
SelectFields []telemetrytypes.TelemetryFieldKey `query:"selectFields"`
// OrderBy specifies the sorting order
// Format: "column:direction" or "context.field:type:direction"
// Direction can be "asc" or "desc"
// ** Deprecated **
OrderBy string `query:"order_by" deprecated:"true"`
// order by keys and directions
Order []qbtypes.OrderBy `query:"order"`
}
type ExportRawDataFormatQueryParam struct {
// Format specifies the output format: "csv" or "jsonl"
Format string `query:"format,default=csv" default:"csv" enum:"csv,jsonl"`
}
func (p *ExportRawDataQueryParams) Normalize() {
if len(p.Order) == 0 && len(p.OrderBy) > 0 {
p.Order = parseExportQueryOrderBy(p.OrderBy)
}
if len(p.SelectFields) == 0 && len(p.Columns) != 0 {
p.SelectFields = parseExportQueryColumns(p.Columns)
}
if len(p.Filter.Expression) == 0 && len(p.FilterString) > 0 {
p.Filter = qbtypes.Filter{Expression: p.FilterString}
}
if p.Signal == telemetrytypes.SignalUnspecified && p.Source != "" {
p.Signal = telemetrytypes.Signal{String: valuer.NewString(p.Source)}
}
}
func (p *ExportRawDataQueryParams) Validate() error {
if p.Signal != telemetrytypes.SignalLogs && p.Signal != telemetrytypes.SignalTraces {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid signal %s", p.Signal).WithAdditional("Allowed values: [logs, traces]")
}
return nil
}
// parseExportQueryColumns converts bound column strings to TelemetryFieldKey structs.
// Each column should be in the format "context.field:type" or "context.field" or "field"
func parseExportQueryColumns(columnParams []string) []telemetrytypes.TelemetryFieldKey {
columns := make([]telemetrytypes.TelemetryFieldKey, 0, len(columnParams))
for _, columnStr := range columnParams {
columnStr = strings.TrimSpace(columnStr)
if columnStr == "" {
continue
}
columns = append(columns, telemetrytypes.GetFieldKeyFromKeyText(columnStr))
}
return columns
}
// parseExportQueryOrderBy converts a bound order_by string to an OrderBy slice.
// The string should be in the format "column:direction" and is assumed already validated.
func parseExportQueryOrderBy(orderByParam string) []qbtypes.OrderBy {
orderByParam = strings.TrimSpace(orderByParam)
if orderByParam == "" {
return []qbtypes.OrderBy{}
}
parts := strings.Split(orderByParam, ":")
column := strings.Join(parts[:len(parts)-1], ":")
direction := parts[len(parts)-1]
return []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.GetFieldKeyFromKeyText(column),
},
Direction: qbtypes.OrderDirectionMap[direction],
},
}
}

View File

@@ -1,174 +0,0 @@
package exporttypes
import (
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func TestParseExportQueryColumns(t *testing.T) {
tests := []struct {
name string
input []string
expectedColumns []telemetrytypes.TelemetryFieldKey
}{
{
name: "empty input",
input: []string{},
expectedColumns: []telemetrytypes.TelemetryFieldKey{},
},
{
name: "single column",
input: []string{"timestamp"},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
},
},
{
name: "multiple columns",
input: []string{"timestamp", "message", "level"},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "message"},
{Name: "level"},
},
},
{
name: "empty entry is skipped",
input: []string{"timestamp", "", "level"},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "level"},
},
},
{
name: "whitespace-only entry is skipped",
input: []string{"timestamp", " ", "level"},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "timestamp"},
{Name: "level"},
},
},
{
name: "column with context and type",
input: []string{"attribute.user:string"},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "user", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
},
},
{
name: "column with context, dot-notation name",
input: []string{"attribute.user.string"},
expectedColumns: []telemetrytypes.TelemetryFieldKey{
{Name: "user.string", FieldContext: telemetrytypes.FieldContextAttribute},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
columns := parseExportQueryColumns(tt.input)
assert.Equal(t, len(tt.expectedColumns), len(columns))
for i, expected := range tt.expectedColumns {
assert.Equal(t, expected, columns[i])
}
})
}
}
func TestParseExportQueryOrderBy(t *testing.T) {
tests := []struct {
name string
input string
expectedOrder []qbtypes.OrderBy
}{
{
name: "empty string returns empty slice",
input: "",
expectedOrder: []qbtypes.OrderBy{},
},
{
name: "simple column asc",
input: "timestamp:asc",
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "timestamp"},
},
},
},
},
{
name: "simple column desc",
input: "timestamp:desc",
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionDesc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "timestamp"},
},
},
},
},
{
name: "column with context and type qualifier",
input: "attribute.user:string:desc",
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionDesc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "user",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
},
},
},
{
name: "column with context, dot-notation name",
input: "attribute.user.string:desc",
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionDesc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "user.string",
FieldContext: telemetrytypes.FieldContextAttribute,
},
},
},
},
},
{
name: "resource with context and type",
input: "resource.service.name:string:asc",
expectedOrder: []qbtypes.OrderBy{
{
Direction: qbtypes.OrderDirectionAsc,
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
order := parseExportQueryOrderBy(tt.input)
assert.Equal(t, len(tt.expectedOrder), len(order))
for i, expected := range tt.expectedOrder {
assert.Equal(t, expected, order[i])
}
})
}
}

View File

@@ -2,7 +2,6 @@ package metrictypes
import (
"database/sql/driver"
"fmt"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
@@ -257,12 +256,3 @@ type MetricTableHints struct {
type MetricValueFilter struct {
Value float64
}
type ComparisonSpaceAggregationParam struct {
Operater string `json:"operator" required:"true"`
Threshold float64 `json:"threshold" required:"true"`
}
func (param ComparisonSpaceAggregationParam) StringValue() string {
return fmt.Sprintf("operator=%s:threshold=%f", param.Operater, param.Threshold)
}

View File

@@ -446,8 +446,6 @@ type MetricAggregation struct {
TimeAggregation metrictypes.TimeAggregation `json:"timeAggregation"`
// space aggregation to apply to the query
SpaceAggregation metrictypes.SpaceAggregation `json:"spaceAggregation"`
// param for space aggregation if needed
ComparisonSpaceAggregationParam *metrictypes.ComparisonSpaceAggregationParam `json:"comparisonSpaceAggregationParam"`
// table hints to use for the query
TableHints *metrictypes.MetricTableHints `json:"-"`
// value filter to apply to the query

View File

@@ -393,59 +393,6 @@ func (r *QueryRangeRequest) HasOrderSpecified() bool {
return false
}
// UseDefaultOrderBy applies a default order unless query has an explicit order provided.
func (r *QueryRangeRequest) UseDefaultOrderBy() {
queries := r.CompositeQuery.Queries
for idx := range queries {
switch queries[idx].Spec.(type) {
case QueryBuilderQuery[TraceAggregation],
QueryBuilderTraceOperator:
if len(queries[idx].GetOrder()) == 0 {
queries[idx].SetOrder(
[]OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "timestamp",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextSpan,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
},
},
Direction: OrderDirectionDesc,
},
},
)
}
case QueryBuilderQuery[LogAggregation]:
if len(queries[idx].GetOrder()) == 0 {
queries[idx].SetOrder(
[]OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "timestamp",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextLog,
FieldDataType: telemetrytypes.FieldDataTypeNumber},
},
Direction: OrderDirectionDesc,
},
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "id",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextLog,
FieldDataType: telemetrytypes.FieldDataTypeString},
},
Direction: OrderDirectionDesc,
},
},
)
}
}
}
}
func (r *QueryRangeRequest) FuncsForQuery(name string) []Function {
funcs := []Function{}
for _, query := range r.CompositeQuery.Queries {
@@ -490,16 +437,6 @@ func (r *QueryRangeRequest) IsAnomalyRequest() (*QueryBuilderQuery[MetricAggrega
return &q, hasAnomaly
}
func (r *QueryRangeRequest) TraceOperatorQueryIndex() int {
for idx, query := range r.CompositeQuery.Queries {
switch query.Spec.(type) {
case TraceOperatorType:
return idx
}
}
return -1
}
// We do not support fill gaps for these queries. Maybe support in future?
func (r *QueryRangeRequest) SkipFillGaps(name string) bool {
for _, query := range r.CompositeQuery.Queries {

View File

@@ -1,379 +0,0 @@
package querybuildertypesv5
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
// GetExpression returns the expression string.
func (q *QueryEnvelope) GetExpression() string {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Expression
case QueryBuilderFormula:
return spec.Expression
}
return ""
}
// GetReturnSpansFrom returns the return-spans-from value.
func (q *QueryEnvelope) GetReturnSpansFrom() string {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.ReturnSpansFrom
}
return ""
}
// GetSignal returns the signal.
func (q *QueryEnvelope) GetSignal() telemetrytypes.Signal {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
return spec.Signal
case QueryBuilderQuery[LogAggregation]:
return spec.Signal
case QueryBuilderQuery[MetricAggregation]:
return spec.Signal
}
return telemetrytypes.SignalUnspecified
}
// GetSource returns the source.
func (q *QueryEnvelope) GetSource() telemetrytypes.Source {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
return spec.Source
case QueryBuilderQuery[LogAggregation]:
return spec.Source
case QueryBuilderQuery[MetricAggregation]:
return spec.Source
}
return telemetrytypes.SourceUnspecified
}
// GetQuery returns the raw query string.
func (q *QueryEnvelope) GetQuery() string {
switch spec := q.Spec.(type) {
case PromQuery:
return spec.Query
case ClickHouseQuery:
return spec.Query
}
return ""
}
// GetStats returns the PromQL stats flag.
func (q *QueryEnvelope) GetStats() bool {
switch spec := q.Spec.(type) {
case PromQuery:
return spec.Stats
}
return false
}
// GetLeft returns the left query reference of a join.
func (q *QueryEnvelope) GetLeft() QueryRef {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
return spec.Left
}
return QueryRef{}
}
// GetRight returns the right query reference of a join.
func (q *QueryEnvelope) GetRight() QueryRef {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
return spec.Right
}
return QueryRef{}
}
// GetJoinType returns the join type.
func (q *QueryEnvelope) GetJoinType() JoinType {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
return spec.Type
}
return JoinType{}
}
// GetOn returns the join ON condition.
func (q *QueryEnvelope) GetOn() string {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
return spec.On
}
return ""
}
// GetQueryName returns the name of the spec.
func (q *QueryEnvelope) GetQueryName() string {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Name
case QueryBuilderQuery[TraceAggregation]:
return spec.Name
case QueryBuilderQuery[LogAggregation]:
return spec.Name
case QueryBuilderQuery[MetricAggregation]:
return spec.Name
case QueryBuilderFormula:
return spec.Name
case QueryBuilderJoin:
return spec.Name
case PromQuery:
return spec.Name
case ClickHouseQuery:
return spec.Name
}
return ""
}
// IsDisabled returns whether the spec is disabled.
func (q *QueryEnvelope) IsDisabled() bool {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Disabled
case QueryBuilderQuery[TraceAggregation]:
return spec.Disabled
case QueryBuilderQuery[LogAggregation]:
return spec.Disabled
case QueryBuilderQuery[MetricAggregation]:
return spec.Disabled
case QueryBuilderFormula:
return spec.Disabled
case QueryBuilderJoin:
return spec.Disabled
case PromQuery:
return spec.Disabled
case ClickHouseQuery:
return spec.Disabled
}
return false
}
// GetLimit returns the row limit.
func (q *QueryEnvelope) GetLimit() int {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Limit
case QueryBuilderQuery[TraceAggregation]:
return spec.Limit
case QueryBuilderQuery[LogAggregation]:
return spec.Limit
case QueryBuilderQuery[MetricAggregation]:
return spec.Limit
case QueryBuilderFormula:
return spec.Limit
case QueryBuilderJoin:
return spec.Limit
}
return 0
}
// GetOffset returns the row offset.
func (q *QueryEnvelope) GetOffset() int {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Offset
case QueryBuilderQuery[TraceAggregation]:
return spec.Offset
case QueryBuilderQuery[LogAggregation]:
return spec.Offset
case QueryBuilderQuery[MetricAggregation]:
return spec.Offset
}
return 0
}
// GetType returns the QueryType of the envelope.
func (q *QueryEnvelope) GetType() QueryType {
return q.Type
}
// GetOrder returns the order-by clauses.
func (q *QueryEnvelope) GetOrder() []OrderBy {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Order
case QueryBuilderQuery[TraceAggregation]:
return spec.Order
case QueryBuilderQuery[LogAggregation]:
return spec.Order
case QueryBuilderQuery[MetricAggregation]:
return spec.Order
case QueryBuilderFormula:
return spec.Order
case QueryBuilderJoin:
return spec.Order
}
return nil
}
// GetGroupBy returns the group-by keys.
func (q *QueryEnvelope) GetGroupBy() []GroupByKey {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.GroupBy
case QueryBuilderQuery[TraceAggregation]:
return spec.GroupBy
case QueryBuilderQuery[LogAggregation]:
return spec.GroupBy
case QueryBuilderQuery[MetricAggregation]:
return spec.GroupBy
case QueryBuilderJoin:
return spec.GroupBy
}
return nil
}
// GetFilter returns the filter.
func (q *QueryEnvelope) GetFilter() *Filter {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Filter
case QueryBuilderQuery[TraceAggregation]:
return spec.Filter
case QueryBuilderQuery[LogAggregation]:
return spec.Filter
case QueryBuilderQuery[MetricAggregation]:
return spec.Filter
case QueryBuilderJoin:
return spec.Filter
}
return nil
}
// GetHaving returns the having clause.
func (q *QueryEnvelope) GetHaving() *Having {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Having
case QueryBuilderQuery[TraceAggregation]:
return spec.Having
case QueryBuilderQuery[LogAggregation]:
return spec.Having
case QueryBuilderQuery[MetricAggregation]:
return spec.Having
case QueryBuilderFormula:
return spec.Having
case QueryBuilderJoin:
return spec.Having
}
return nil
}
// GetFunctions returns the post-processing functions.
func (q *QueryEnvelope) GetFunctions() []Function {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Functions
case QueryBuilderQuery[TraceAggregation]:
return spec.Functions
case QueryBuilderQuery[LogAggregation]:
return spec.Functions
case QueryBuilderQuery[MetricAggregation]:
return spec.Functions
case QueryBuilderFormula:
return spec.Functions
case QueryBuilderJoin:
return spec.Functions
}
return nil
}
// GetSelectFields returns the selected fields.
func (q *QueryEnvelope) GetSelectFields() []telemetrytypes.TelemetryFieldKey {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.SelectFields
case QueryBuilderQuery[TraceAggregation]:
return spec.SelectFields
case QueryBuilderQuery[LogAggregation]:
return spec.SelectFields
case QueryBuilderQuery[MetricAggregation]:
return spec.SelectFields
case QueryBuilderJoin:
return spec.SelectFields
}
return nil
}
// GetLegend returns the legend label.
func (q *QueryEnvelope) GetLegend() string {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Legend
case QueryBuilderQuery[TraceAggregation]:
return spec.Legend
case QueryBuilderQuery[LogAggregation]:
return spec.Legend
case QueryBuilderQuery[MetricAggregation]:
return spec.Legend
case QueryBuilderFormula:
return spec.Legend
case PromQuery:
return spec.Legend
case ClickHouseQuery:
return spec.Legend
}
return ""
}
// GetCursor returns the pagination cursor.
func (q *QueryEnvelope) GetCursor() string {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.Cursor
case QueryBuilderQuery[TraceAggregation]:
return spec.Cursor
case QueryBuilderQuery[LogAggregation]:
return spec.Cursor
case QueryBuilderQuery[MetricAggregation]:
return spec.Cursor
}
return ""
}
// GetStepInterval returns the step interval.
func (q *QueryEnvelope) GetStepInterval() Step {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
return spec.StepInterval
case QueryBuilderQuery[TraceAggregation]:
return spec.StepInterval
case QueryBuilderQuery[LogAggregation]:
return spec.StepInterval
case QueryBuilderQuery[MetricAggregation]:
return spec.StepInterval
case PromQuery:
return spec.Step
}
return Step{}
}
// GetSecondaryAggregations returns the secondary aggregations.
func (q *QueryEnvelope) GetSecondaryAggregations() []SecondaryAggregation {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
return spec.SecondaryAggregations
case QueryBuilderQuery[LogAggregation]:
return spec.SecondaryAggregations
case QueryBuilderQuery[MetricAggregation]:
return spec.SecondaryAggregations
case QueryBuilderJoin:
return spec.SecondaryAggregations
}
return nil
}
// GetLimitBy returns the limit-by configuration.
func (q *QueryEnvelope) GetLimitBy() *LimitBy {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
return spec.LimitBy
case QueryBuilderQuery[LogAggregation]:
return spec.LimitBy
case QueryBuilderQuery[MetricAggregation]:
return spec.LimitBy
}
return nil
}

View File

@@ -1,452 +0,0 @@
package querybuildertypesv5
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
// SetExpression sets the expression string of the spec, if applicable.
func (q *QueryEnvelope) SetExpression(expression string) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Expression = expression
q.Spec = spec
case QueryBuilderFormula:
spec.Expression = expression
q.Spec = spec
}
}
// SetReturnSpansFrom sets the return-spans-from value, if applicable.
func (q *QueryEnvelope) SetReturnSpansFrom(returnSpansFrom string) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.ReturnSpansFrom = returnSpansFrom
q.Spec = spec
}
}
// SetSignal sets the signal of the spec, if applicable.
func (q *QueryEnvelope) SetSignal(signal telemetrytypes.Signal) {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
spec.Signal = signal
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Signal = signal
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Signal = signal
q.Spec = spec
}
}
// SetSource sets the source of the spec, if applicable.
func (q *QueryEnvelope) SetSource(source telemetrytypes.Source) {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
spec.Source = source
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Source = source
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Source = source
q.Spec = spec
}
}
// SetQuery sets the raw query string of the spec, if applicable.
func (q *QueryEnvelope) SetQuery(query string) {
switch spec := q.Spec.(type) {
case PromQuery:
spec.Query = query
q.Spec = spec
case ClickHouseQuery:
spec.Query = query
q.Spec = spec
}
}
// SetStats sets the PromQL stats flag, if applicable.
func (q *QueryEnvelope) SetStats(stats bool) {
switch spec := q.Spec.(type) {
case PromQuery:
spec.Stats = stats
q.Spec = spec
}
}
// SetLeft sets the left query reference of a join, if applicable.
func (q *QueryEnvelope) SetLeft(left QueryRef) {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
spec.Left = left
q.Spec = spec
}
}
// SetRight sets the right query reference of a join, if applicable.
func (q *QueryEnvelope) SetRight(right QueryRef) {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
spec.Right = right
q.Spec = spec
}
}
// SetJoinType sets the join type, if applicable.
func (q *QueryEnvelope) SetJoinType(joinType JoinType) {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
spec.Type = joinType
q.Spec = spec
}
}
// SetOn sets the join ON condition, if applicable.
func (q *QueryEnvelope) SetOn(on string) {
switch spec := q.Spec.(type) {
case QueryBuilderJoin:
spec.On = on
q.Spec = spec
}
}
// SetQueryName sets the name of the spec, if applicable.
func (q *QueryEnvelope) SetQueryName(name string) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Name = name
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Name = name
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Name = name
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Name = name
q.Spec = spec
case QueryBuilderFormula:
spec.Name = name
q.Spec = spec
case QueryBuilderJoin:
spec.Name = name
q.Spec = spec
case PromQuery:
spec.Name = name
q.Spec = spec
case ClickHouseQuery:
spec.Name = name
q.Spec = spec
}
}
// SetDisabled sets the disabled flag of the spec, if applicable.
func (q *QueryEnvelope) SetDisabled(disabled bool) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Disabled = disabled
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Disabled = disabled
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Disabled = disabled
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Disabled = disabled
q.Spec = spec
case QueryBuilderFormula:
spec.Disabled = disabled
q.Spec = spec
case QueryBuilderJoin:
spec.Disabled = disabled
q.Spec = spec
case PromQuery:
spec.Disabled = disabled
q.Spec = spec
case ClickHouseQuery:
spec.Disabled = disabled
q.Spec = spec
}
}
// SetLimit sets the row limit of the spec, if applicable.
func (q *QueryEnvelope) SetLimit(limit int) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Limit = limit
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Limit = limit
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Limit = limit
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Limit = limit
q.Spec = spec
case QueryBuilderFormula:
spec.Limit = limit
q.Spec = spec
case QueryBuilderJoin:
spec.Limit = limit
q.Spec = spec
}
}
// SetOffset sets the row offset of the spec, if applicable.
func (q *QueryEnvelope) SetOffset(offset int) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Offset = offset
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Offset = offset
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Offset = offset
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Offset = offset
q.Spec = spec
}
}
// SetType sets the QueryType of the envelope.
func (q *QueryEnvelope) SetType(t QueryType) {
q.Type = t
}
// SetOrder sets the order-by clauses of the spec, if applicable.
func (q *QueryEnvelope) SetOrder(order []OrderBy) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Order = order
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Order = order
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Order = order
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Order = order
q.Spec = spec
case QueryBuilderFormula:
spec.Order = order
q.Spec = spec
case QueryBuilderJoin:
spec.Order = order
q.Spec = spec
}
}
// SetGroupBy sets the group-by keys of the spec, if applicable.
func (q *QueryEnvelope) SetGroupBy(groupBy []GroupByKey) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.GroupBy = groupBy
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.GroupBy = groupBy
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.GroupBy = groupBy
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.GroupBy = groupBy
q.Spec = spec
case QueryBuilderJoin:
spec.GroupBy = groupBy
q.Spec = spec
}
}
// SetFilter sets the filter of the spec, if applicable.
func (q *QueryEnvelope) SetFilter(filter *Filter) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Filter = filter
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Filter = filter
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Filter = filter
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Filter = filter
q.Spec = spec
case QueryBuilderJoin:
spec.Filter = filter
q.Spec = spec
}
}
// SetHaving sets the having clause of the spec, if applicable.
func (q *QueryEnvelope) SetHaving(having *Having) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Having = having
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Having = having
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Having = having
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Having = having
q.Spec = spec
case QueryBuilderFormula:
spec.Having = having
q.Spec = spec
case QueryBuilderJoin:
spec.Having = having
q.Spec = spec
}
}
// SetFunctions sets the post-processing functions of the spec, if applicable.
func (q *QueryEnvelope) SetFunctions(functions []Function) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Functions = functions
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Functions = functions
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Functions = functions
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Functions = functions
q.Spec = spec
case QueryBuilderFormula:
spec.Functions = functions
q.Spec = spec
case QueryBuilderJoin:
spec.Functions = functions
q.Spec = spec
}
}
// SetSelectFields sets the selected fields of the spec, if applicable.
func (q *QueryEnvelope) SetSelectFields(fields []telemetrytypes.TelemetryFieldKey) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.SelectFields = fields
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.SelectFields = fields
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.SelectFields = fields
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.SelectFields = fields
q.Spec = spec
case QueryBuilderJoin:
spec.SelectFields = fields
q.Spec = spec
}
}
// SetLegend sets the legend label of the spec, if applicable.
func (q *QueryEnvelope) SetLegend(legend string) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Legend = legend
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Legend = legend
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Legend = legend
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Legend = legend
q.Spec = spec
case QueryBuilderFormula:
spec.Legend = legend
q.Spec = spec
case PromQuery:
spec.Legend = legend
q.Spec = spec
case ClickHouseQuery:
spec.Legend = legend
q.Spec = spec
}
}
// SetCursor sets the pagination cursor of the spec, if applicable.
func (q *QueryEnvelope) SetCursor(cursor string) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.Cursor = cursor
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.Cursor = cursor
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.Cursor = cursor
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.Cursor = cursor
q.Spec = spec
}
}
// SetStepInterval sets the step interval of the spec, if applicable.
func (q *QueryEnvelope) SetStepInterval(step Step) {
switch spec := q.Spec.(type) {
case QueryBuilderTraceOperator:
spec.StepInterval = step
q.Spec = spec
case QueryBuilderQuery[TraceAggregation]:
spec.StepInterval = step
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.StepInterval = step
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.StepInterval = step
q.Spec = spec
case PromQuery:
spec.Step = step
q.Spec = spec
}
}
// SetSecondaryAggregations sets the secondary aggregations of the spec, if applicable.
func (q *QueryEnvelope) SetSecondaryAggregations(secondaryAggregations []SecondaryAggregation) {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
spec.SecondaryAggregations = secondaryAggregations
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.SecondaryAggregations = secondaryAggregations
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.SecondaryAggregations = secondaryAggregations
q.Spec = spec
case QueryBuilderJoin:
spec.SecondaryAggregations = secondaryAggregations
q.Spec = spec
}
}
// SetLimitBy sets the limit-by configuration of the spec, if applicable.
func (q *QueryEnvelope) SetLimitBy(limitBy *LimitBy) {
switch spec := q.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
spec.LimitBy = limitBy
q.Spec = spec
case QueryBuilderQuery[LogAggregation]:
spec.LimitBy = limitBy
q.Spec = spec
case QueryBuilderQuery[MetricAggregation]:
spec.LimitBy = limitBy
q.Spec = spec
}
}

View File

@@ -10,9 +10,55 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
// queryName returns the name from any query envelope spec type.
func (e QueryEnvelope) queryName() string {
switch spec := e.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
return spec.Name
case QueryBuilderQuery[LogAggregation]:
return spec.Name
case QueryBuilderQuery[MetricAggregation]:
return spec.Name
case QueryBuilderFormula:
return spec.Name
case QueryBuilderTraceOperator:
return spec.Name
case QueryBuilderJoin:
return spec.Name
case PromQuery:
return spec.Name
case ClickHouseQuery:
return spec.Name
}
return ""
}
// isDisabled returns the disabled status from any query envelope spec type.
func (e QueryEnvelope) isDisabled() bool {
switch spec := e.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
return spec.Disabled
case QueryBuilderQuery[LogAggregation]:
return spec.Disabled
case QueryBuilderQuery[MetricAggregation]:
return spec.Disabled
case QueryBuilderFormula:
return spec.Disabled
case QueryBuilderTraceOperator:
return spec.Disabled
case QueryBuilderJoin:
return spec.Disabled
case PromQuery:
return spec.Disabled
case ClickHouseQuery:
return spec.Disabled
}
return false
}
// getQueryIdentifier returns a friendly identifier for a query based on its type and name/content
func getQueryIdentifier(envelope QueryEnvelope, index int) string {
name := envelope.GetQueryName()
name := envelope.queryName()
var typeLabel string
switch envelope.Type {
@@ -43,115 +89,50 @@ const (
MaxQueryLimit = 10000
)
// ValidationOption is a functional option for configuring validation behaviour.
type ValidationOption func(*validationConfig)
type validationConfig struct {
skipLimitValidation bool
skipAggregationValidation bool
skipHavingValidation bool
skipAggregationOrderBy bool
skipSelectFieldValidation bool
skipGroupByValidation bool
}
func applyValidationOptions(opts []ValidationOption) validationConfig {
cfg := validationConfig{}
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
// SkipLimitValidation returns a ValidationOption that skips the limit range check.
// Use this when the caller has already validated limits with different constraints.
func WithSkipLimitValidation() ValidationOption {
return func(cfg *validationConfig) {
cfg.skipLimitValidation = true
}
}
// SkipAggregationValidation skips aggregation validation.
// Used for raw/trace request types where aggregations are not required.
func WithSkipAggregationValidation() ValidationOption {
return func(cfg *validationConfig) {
cfg.skipAggregationValidation = true
}
}
// SkipHavingValidation skips having-clause validation.
// Used for raw/trace request types where having clauses do not apply.
func WithSkipHavingValidation() ValidationOption {
return func(cfg *validationConfig) {
cfg.skipHavingValidation = true
}
}
// SkipAggregationOrderBy skips the aggregation-specific order-by key validation.
// Used for raw/trace request types where order-by keys are not restricted to group-by or aggregation keys.
func WithSkipAggregationOrderBy() ValidationOption {
return func(cfg *validationConfig) {
cfg.skipAggregationOrderBy = true
}
}
// SkipSelectFieldValidation skips select-field validation.
// Used for aggregation request types where select fields do not apply.
func WithSkipSelectFieldValidation() ValidationOption {
return func(cfg *validationConfig) {
cfg.skipSelectFieldValidation = true
}
}
// SkipGroupByValidation skips group-by validation.
// Used for raw/trace request types where group-by does not apply.
func WithSkipGroupByValidation() ValidationOption {
return func(cfg *validationConfig) {
cfg.skipGroupByValidation = true
}
}
// Validate performs preliminary validation on QueryBuilderQuery.
func (q *QueryBuilderQuery[T]) Validate(opts ...ValidationOption) error {
cfg := applyValidationOptions(opts)
// Validate performs preliminary validation on QueryBuilderQuery
func (q *QueryBuilderQuery[T]) Validate(requestType RequestType) error {
// Validate signal
if err := q.validateSignal(); err != nil {
return err
}
if err := q.validateAggregations(cfg); err != nil {
if err := q.validateAggregations(requestType); err != nil {
return err
}
if err := q.validateGroupBy(cfg); err != nil {
if err := q.validateGroupBy(requestType); err != nil {
return err
}
if err := q.validateLimitAndPagination(cfg); err != nil {
// Validate limit and pagination
if err := q.validateLimitAndPagination(); err != nil {
return err
}
// Validate functions
if err := q.validateFunctions(); err != nil {
return err
}
// Validate secondary aggregations
if err := q.validateSecondaryAggregations(); err != nil {
return err
}
if err := q.validateOrderBy(cfg); err != nil {
if err := q.validateOrderBy(requestType); err != nil {
return err
}
if err := q.validateSelectFields(cfg); err != nil {
if err := q.validateSelectFields(requestType); err != nil {
return err
}
return nil
}
func (q *QueryBuilderQuery[T]) validateSelectFields(cfg validationConfig) error {
if cfg.skipSelectFieldValidation {
func (q *QueryBuilderQuery[T]) validateSelectFields(requestType RequestType) error {
// selectFields don't apply to aggregation queries, skip validation
if requestType.IsAggregation() {
return nil
}
@@ -167,8 +148,9 @@ func (q *QueryBuilderQuery[T]) validateSelectFields(cfg validationConfig) error
return nil
}
func (q *QueryBuilderQuery[T]) validateGroupBy(cfg validationConfig) error {
if cfg.skipGroupByValidation {
func (q *QueryBuilderQuery[T]) validateGroupBy(requestType RequestType) error {
// groupBy doesn't apply to non-aggregation queries, skip validation
if !requestType.IsAggregation() {
return nil
}
for idx, item := range q.GroupBy {
@@ -201,8 +183,9 @@ func (q *QueryBuilderQuery[T]) validateSignal() error {
}
}
func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error {
if cfg.skipAggregationValidation {
func (q *QueryBuilderQuery[T]) validateAggregations(requestType RequestType) error {
// aggregations don't apply to non-aggregation queries, skip validation
if !requestType.IsAggregation() {
return nil
}
@@ -282,25 +265,24 @@ func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error
return nil
}
func (q *QueryBuilderQuery[T]) validateLimitAndPagination(cfg validationConfig) error {
if !cfg.skipLimitValidation {
if q.Limit < 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"limit must be non-negative, got %d",
q.Limit,
)
}
func (q *QueryBuilderQuery[T]) validateLimitAndPagination() error {
// Validate limit
if q.Limit < 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"limit must be non-negative, got %d",
q.Limit,
)
}
if q.Limit > MaxQueryLimit {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"limit exceeds maximum allowed value of %d",
MaxQueryLimit,
).WithAdditional(
fmt.Sprintf("Provided limit: %d", q.Limit),
)
}
if q.Limit > MaxQueryLimit {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"limit exceeds maximum allowed value of %d",
MaxQueryLimit,
).WithAdditional(
fmt.Sprintf("Provided limit: %d", q.Limit),
)
}
// Validate offset
@@ -347,7 +329,7 @@ func (q *QueryBuilderQuery[T]) validateSecondaryAggregations() error {
return nil
}
func (q *QueryBuilderQuery[T]) validateOrderBy(cfg validationConfig) error {
func (q *QueryBuilderQuery[T]) validateOrderBy(requestType RequestType) error {
for i, order := range q.Order {
// Direction validation is handled by the OrderDirection type
if order.Direction != OrderDirectionAsc && order.Direction != OrderDirectionDesc {
@@ -366,7 +348,8 @@ func (q *QueryBuilderQuery[T]) validateOrderBy(cfg validationConfig) error {
}
}
if !cfg.skipAggregationOrderBy {
// aggregation-specific order key validation only applies to aggregation queries
if requestType.IsAggregation() {
return q.validateOrderByForAggregation()
}
@@ -448,8 +431,8 @@ func (q *QueryBuilderQuery[T]) validateOrderByForAggregation() error {
return nil
}
// Validate validates the entire query range request.
func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
// ValidateQueryRangeRequest validates the entire query range request
func (r *QueryRangeRequest) Validate() error {
// Validate time range
if r.RequestType != RequestTypeRawStream && r.Start >= r.End {
return errors.NewInvalidInputf(
@@ -460,10 +443,8 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
// Validate request type
switch r.RequestType {
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace:
opts = append(opts, getValidationOptions(false)...)
case RequestTypeTimeSeries, RequestTypeScalar:
opts = append(opts, getValidationOptions(true)...)
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTimeSeries, RequestTypeScalar, RequestTypeTrace:
// Valid request types
default:
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
@@ -475,7 +456,7 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
}
// Validate composite query
if err := r.CompositeQuery.Validate(opts...); err != nil {
if err := r.validateCompositeQuery(); err != nil {
return err
}
@@ -490,7 +471,7 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
// validateAllQueriesNotDisabled validates that at least one query in the composite query is enabled
func (r *QueryRangeRequest) validateAllQueriesNotDisabled() error {
for _, envelope := range r.CompositeQuery.Queries {
if !envelope.IsDisabled() {
if !envelope.isDisabled() {
return nil
}
}
@@ -501,8 +482,12 @@ func (r *QueryRangeRequest) validateAllQueriesNotDisabled() error {
)
}
func (r *QueryRangeRequest) validateCompositeQuery() error {
return r.CompositeQuery.Validate(r.RequestType)
}
// Validate performs validation on CompositeQuery
func (c *CompositeQuery) Validate(opts ...ValidationOption) error {
func (c *CompositeQuery) Validate(requestType RequestType) error {
if len(c.Queries) == 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
@@ -514,14 +499,14 @@ func (c *CompositeQuery) Validate(opts ...ValidationOption) error {
queryNames := make(map[string]bool)
for i, envelope := range c.Queries {
if err := validateQueryEnvelope(envelope, opts...); err != nil {
if err := validateQueryEnvelope(envelope, requestType); err != nil {
queryId := getQueryIdentifier(envelope, i)
return wrapValidationError(err, queryId, "invalid %s: %s")
}
// Check name uniqueness for builder queries
if envelope.Type == QueryTypeBuilder || envelope.Type == QueryTypeSubQuery {
name := envelope.GetQueryName()
name := envelope.queryName()
if name != "" {
if queryNames[name] {
return errors.NewInvalidInputf(
@@ -538,16 +523,16 @@ func (c *CompositeQuery) Validate(opts ...ValidationOption) error {
return nil
}
func validateQueryEnvelope(envelope QueryEnvelope, opts ...ValidationOption) error {
func validateQueryEnvelope(envelope QueryEnvelope, requestType RequestType) error {
switch envelope.Type {
case QueryTypeBuilder, QueryTypeSubQuery:
switch spec := envelope.Spec.(type) {
case QueryBuilderQuery[TraceAggregation]:
return spec.Validate(opts...)
return spec.Validate(requestType)
case QueryBuilderQuery[LogAggregation]:
return spec.Validate(opts...)
return spec.Validate(requestType)
case QueryBuilderQuery[MetricAggregation]:
return spec.Validate(opts...)
return spec.Validate(requestType)
default:
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
@@ -633,11 +618,3 @@ func validateQueryEnvelope(envelope QueryEnvelope, opts ...ValidationOption) err
)
}
}
func getValidationOptions(isAggregationQuery bool) []ValidationOption {
if isAggregationQuery {
return []ValidationOption{WithSkipSelectFieldValidation()}
}
return []ValidationOption{WithSkipAggregationValidation(), WithSkipHavingValidation(), WithSkipAggregationOrderBy(), WithSkipGroupByValidation()}
}

View File

@@ -743,7 +743,7 @@ func TestValidateQueryEnvelope(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateQueryEnvelope(tt.envelope)
err := validateQueryEnvelope(tt.envelope, tt.requestType)
if tt.wantErr {
if err == nil {
t.Errorf("validateQueryEnvelope() expected error but got none")
@@ -816,7 +816,7 @@ func TestQueryEnvelope_Helpers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.envelope.GetQueryName()
got := tt.envelope.queryName()
if got != tt.want {
t.Errorf("queryName() = %q, want %q", got, tt.want)
}
@@ -868,7 +868,7 @@ func TestQueryEnvelope_Helpers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.envelope.IsDisabled()
got := tt.envelope.isDisabled()
if got != tt.want {
t.Errorf("isDisabled() = %v, want %v", got, tt.want)
}
@@ -1107,7 +1107,7 @@ func TestQueryRangeRequest_ValidateOrderByForAggregation(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.query.Validate(getValidationOptions(true)...)
err := tt.query.Validate(RequestTypeTimeSeries)
if tt.wantErr {
if err == nil {
t.Errorf("validateOrderByForAggregation() expected error but got none")
@@ -1161,7 +1161,7 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"}},
},
}
err := query.Validate(getValidationOptions(false)...)
err := query.Validate(RequestTypeRaw)
if err != nil {
t.Errorf("expected no error for groupBy with raw request type, got: %v", err)
}
@@ -1178,7 +1178,7 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ""}},
},
}
err := query.Validate(getValidationOptions(true)...)
err := query.Validate(RequestTypeTimeSeries)
if err == nil {
t.Errorf("expected error for empty groupBy key with timeseries request type")
}
@@ -1190,7 +1190,7 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
Signal: telemetrytypes.SignalLogs,
Having: &Having{Expression: "count() > 10"},
}
err := query.Validate(getValidationOptions(false)...)
err := query.Validate(RequestTypeRaw)
if err != nil {
t.Errorf("expected no error for having with raw request type, got: %v", err)
}
@@ -1202,7 +1202,7 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
Signal: telemetrytypes.SignalTraces,
Having: &Having{Expression: "count() > 10"},
}
err := query.Validate(getValidationOptions(false)...)
err := query.Validate(RequestTypeTrace)
if err != nil {
t.Errorf("expected no error for having with trace request type, got: %v", err)
}
@@ -1216,7 +1216,7 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
{Expression: "count()"},
},
}
err := query.Validate(getValidationOptions(false)...)
err := query.Validate(RequestTypeRaw)
if err != nil {
t.Errorf("expected no error for aggregations with raw request type, got: %v", err)
}
@@ -1230,7 +1230,7 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
{Expression: "count()"},
},
}
err := query.Validate(getValidationOptions(false)...)
err := query.Validate(RequestTypeRawStream)
if err != nil {
t.Errorf("expected no error for aggregations with raw_stream request type, got: %v", err)
}
@@ -1248,12 +1248,12 @@ func TestNonAggregationFieldsSkipped(t *testing.T) {
},
}
// Should error for raw (selectFields are validated)
err := query.Validate(getValidationOptions(false)...)
err := query.Validate(RequestTypeRaw)
if err == nil {
t.Errorf("expected error for isRoot in selectFields with raw request type")
}
// Should pass for timeseries (selectFields skipped)
err = query.Validate(getValidationOptions(true)...)
err = query.Validate(RequestTypeTimeSeries)
if err != nil {
t.Errorf("expected no error for isRoot in selectFields with timeseries request type, got: %v", err)
}

View File

@@ -355,10 +355,6 @@ func (r *PostableRule) validate() error {
errs = append(errs, signozError.NewInvalidInputf(signozError.CodeInvalidInput, "composite query is required"))
}
if r.Version != "v5" {
errs = append(errs, signozError.NewInvalidInputf(signozError.CodeInvalidInput, "only version v5 is supported, got %q", r.Version))
}
if isAllQueriesDisabled(r.RuleCondition.CompositeQuery) {
errs = append(errs, signozError.NewInvalidInputf(signozError.CodeInvalidInput, "all queries are disabled in rule condition"))
}

View File

@@ -108,7 +108,6 @@ func TestParseIntoRule(t *testing.T) {
"ruleType": "threshold_rule",
"evalWindow": "5m",
"frequency": "1m",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -151,7 +150,6 @@ func TestParseIntoRule(t *testing.T) {
content: []byte(`{
"alert": "DefaultsRule",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -189,7 +187,6 @@ func TestParseIntoRule(t *testing.T) {
initRule: PostableRule{},
content: []byte(`{
"alert": "PromQLRule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "promql",
@@ -259,7 +256,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "SeverityLabelTest",
"schemaVersion": "v1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -348,7 +344,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "NoLabelsTest",
"schemaVersion": "v1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -389,7 +384,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "OverwriteTest",
"schemaVersion": "v1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -480,7 +474,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "V2Test",
"schemaVersion": "v2",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -524,7 +517,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
initRule: PostableRule{},
content: []byte(`{
"alert": "DefaultSchemaTest",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -577,7 +569,6 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
func TestParseIntoRuleThresholdGeneration(t *testing.T) {
content := []byte(`{
"alert": "TestThresholds",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -648,7 +639,6 @@ func TestParseIntoRuleMultipleThresholds(t *testing.T) {
"schemaVersion": "v2",
"alert": "MultiThresholdAlert",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -742,7 +732,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -777,7 +766,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -811,7 +799,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyAboveTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -846,7 +833,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyAboveTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -880,7 +866,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowAllTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -916,7 +901,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowAllTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -951,7 +935,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyOutOfBoundsTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -986,7 +969,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "ThresholdTest",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -1021,7 +1003,6 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "ThresholdTest",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",

View File

@@ -32,13 +32,11 @@ type MetadataStore interface {
// FetchTemporalityMulti fetches the temporality for multiple metrics
FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error)
FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error)
// ListLogsJSONIndexes lists the JSON indexes for the logs table.
ListLogsJSONIndexes(ctx context.Context, filters ...string) (map[string][]schemamigrator.Index, error)
// ListPromotedPaths lists the promoted paths.
GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error)
ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error)
// PromotePaths promotes the paths.
PromotePaths(ctx context.Context, paths ...string) error

View File

@@ -16,8 +16,7 @@ type MockMetadataStore struct {
RelatedValuesMap map[string][]string
AllValuesMap map[string]*telemetrytypes.TelemetryFieldValues
TemporalityMap map[string]metrictypes.Temporality
TypeMap map[string]metrictypes.Type
PromotedPathsMap map[string]bool
PromotedPathsMap map[string]struct{}
LogsJSONIndexesMap map[string][]schemamigrator.Index
LookupKeysMap map[telemetrytypes.MetricMetadataLookupKey]int64
}
@@ -29,8 +28,7 @@ func NewMockMetadataStore() *MockMetadataStore {
RelatedValuesMap: make(map[string][]string),
AllValuesMap: make(map[string]*telemetrytypes.TelemetryFieldValues),
TemporalityMap: make(map[string]metrictypes.Temporality),
TypeMap: make(map[string]metrictypes.Type),
PromotedPathsMap: make(map[string]bool),
PromotedPathsMap: make(map[string]struct{}),
LogsJSONIndexesMap: make(map[string][]schemamigrator.Index),
LookupKeysMap: make(map[telemetrytypes.MetricMetadataLookupKey]int64),
}
@@ -289,27 +287,6 @@ func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, queryTime
return result, nil
}
// FetchTemporalityMulti fetches the temporality for multiple metrics
func (m *MockMetadataStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
temporalities := make(map[string]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
for _, metricName := range metricNames {
if temporality, exists := m.TemporalityMap[metricName]; exists {
temporalities[metricName] = temporality
} else {
temporalities[metricName] = metrictypes.Unknown
}
if metricType, exists := m.TypeMap[metricName]; exists {
types[metricName] = metricType
} else {
types[metricName] = metrictypes.UnspecifiedType
}
}
return temporalities, types, nil
}
// SetTemporality sets the temporality for a metric in the mock store
func (m *MockMetadataStore) SetTemporality(metricName string, temporality metrictypes.Temporality) {
m.TemporalityMap[metricName] = temporality
@@ -318,13 +295,13 @@ func (m *MockMetadataStore) SetTemporality(metricName string, temporality metric
// PromotePaths promotes the paths.
func (m *MockMetadataStore) PromotePaths(ctx context.Context, paths ...string) error {
for _, path := range paths {
m.PromotedPathsMap[path] = true
m.PromotedPathsMap[path] = struct{}{}
}
return nil
}
// GetPromotedPaths returns the promoted paths.
func (m *MockMetadataStore) GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error) {
// ListPromotedPaths lists the promoted paths.
func (m *MockMetadataStore) ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error) {
return m.PromotedPathsMap, nil
}

View File

@@ -52,7 +52,6 @@ def build_builder_query(
time_aggregation: str,
space_aggregation: str,
*,
comparisonSpaceAggregationParam: Optional[Dict] = None,
temporality: Optional[str] = None,
step_interval: int = DEFAULT_STEP_INTERVAL,
group_by: Optional[List[str]] = None,
@@ -75,8 +74,7 @@ def build_builder_query(
}
if temporality:
spec["aggregations"][0]["temporality"] = temporality
if comparisonSpaceAggregationParam:
spec["aggregations"][0]["comparisonSpaceAggregationParam"] = comparisonSpaceAggregationParam
if group_by:
spec["groupBy"] = [
{

View File

@@ -353,7 +353,7 @@ def test_for_week_long_time_range(
metrics = Metrics.load_from_file(
MULTI_TEMPORALITY_FILE_10h,
base_time=now - timedelta(minutes=1441),
base_time=now - timedelta(minutes=600),
metric_name_override=metric_name,
)
insert_metrics(metrics)

View File

@@ -1,322 +0,0 @@
"""
Look at the histogram_data_1h.jsonl file for the relevant data
"""
import random
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, List
import pytest
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.metrics import Metrics
from fixtures.querier import (
build_builder_query,
get_all_series,
get_series_values,
make_query_request,
)
from fixtures.utils import get_testdata_file_path
FILE = get_testdata_file_path("histogram_data_1h.jsonl")
@pytest.mark.parametrize(
"threshold, operator, first_value, last_value",
[
(1000, "<=", 11, 69),
(100, "<=", 1.1, 6.9),
(7500, "<=", 16.75, 74.75),
(8000, "<=", 17, 75),
(80000, "<=", 17, 75), ## cuz we don't know the max value in infinity, all numbers beyond the biggest finite bucket will report the same answer
(1000, ">", 7, 7),
(100, ">", 16.9, 69.1),
(7500, ">", 1.25, 1.25),
(8000, ">", 1, 1),
(80000, ">", 1, 1), ## cuz we don't know the max value in infinity, all numbers beyond the biggest finite bucket will report the same answer
],
)
def test_histogram_count_for_one_endpoint(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
threshold: float,
operator: str,
first_value: float,
last_value: float,
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_one_endpoint_bucket"
metrics = Metrics.load_from_file(
FILE,
base_time=now - timedelta(minutes=60),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"increase",
"count",
comparisonSpaceAggregationParam={
"threshold": threshold,
"operator": operator
},
filter_expression='endpoint = "/health"',
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
assert len(result_values) == 59
assert result_values[0]["value"] == first_value
assert result_values[-1]["value"] == last_value
@pytest.mark.parametrize(
"threshold, operator, first_value, last_value",
[
(1000, "<=", 22, 138),
(100, "<=", 2.2, 13.8),
(7500, "<=", 33.5, 149.5),
(8000, "<=", 34, 150),
(80000, "<=", 34, 150), ## cuz we don't know the max value in infinity, all numbers beyond the biggest finite bucket will report the same answer
(1000, ">", 14, 14),
(100, ">", 33.8, 138.2),
(7500, ">", 2.5, 2.5),
(8000, ">", 2, 2),
(80000, ">", 2, 2), ## cuz we don't know the max value in infinity, all numbers beyond the biggest finite bucket will report the same answer
],
)
def test_histogram_count_for_one_service(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
threshold: float,
operator: str,
first_value: float,
last_value: float,
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_one_service_bucket"
metrics = Metrics.load_from_file(
FILE,
base_time=now - timedelta(minutes=60),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"increase",
"count",
comparisonSpaceAggregationParam={
"threshold": threshold,
"operator": operator
},
filter_expression='service = "api"',
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
assert len(result_values) == 59
assert result_values[0]["value"] == first_value
assert result_values[-1]["value"] == last_value
@pytest.mark.parametrize(
"threshold, operator, zeroth_value, first_value, last_value",
[
(1000, "<=", 12345, 11, 69),
(100, "<=", 1234.5, 1.1, 6.9),
(7500, "<=", 12345, 16.75, 74.75),
(8000, "<=", 12345, 17, 75),
(80000, "<=", 12345, 17, 75), ## cuz we don't know the max value in infinity, all numbers beyond the biggest finite bucket will report the same answer
(1000, ">", 0, 7, 7),
(100, ">", 11110.5, 16.9, 69.1),
(7500, ">", 0, 1.25, 1.25),
(8000, ">", 0, 1, 1),
(80000, ">", 0, 1, 1), ## cuz we don't know the max value in infinity, all numbers beyond the biggest finite bucket will report the same answer
],
)
def test_histogram_count_for_delta_service(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
threshold: float,
operator: str,
zeroth_value: float,
first_value: float,
last_value: float,
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_delta_service_bucket"
metrics = Metrics.load_from_file(
FILE,
base_time=now - timedelta(minutes=60),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"increase",
"count",
comparisonSpaceAggregationParam={
"threshold": threshold,
"operator": operator
},
filter_expression='service = "web"',
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
assert len(result_values) == 60 ## in delta, the value at 10:01 will also be reported
assert result_values[0]["value"] == zeroth_value
assert result_values[1]["value"] == first_value ## to keep parallel to the cumulative test cases, first_value refers to the value at 10:02
assert result_values[-1]["value"] == last_value
@pytest.mark.parametrize(
"threshold, operator, zeroth_value, first_value, last_value",
[
(1000, "<=", 12345, 33, 207),
(100, "<=", 1234.5, 3.3, 20.7),
(7500, "<=", 12345, 50.25, 224.25),
(8000, "<=", 12345, 51, 225),
(80000, "<=", 12345, 51, 225),
(1000, ">", 0, 21, 21),
(100, ">", 11110.5, 50.7, 207.3),
(7500, ">", 0, 3.75, 3.75),
(8000, ">", 0, 3, 3),
(80000, ">", 0, 3, 3),
],
)
def test_histogram_count_for_all_services(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
threshold: float,
operator: str,
zeroth_value: float,
first_value: float,
last_value: float,
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_all_services_bucket"
metrics = Metrics.load_from_file(
FILE,
base_time=now - timedelta(minutes=60),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"increase",
"count",
comparisonSpaceAggregationParam={
"threshold": threshold,
"operator": operator
},
## no services filter, this tests for multitemporality handling as well
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
assert len(result_values) == 60 ## in delta, the value at 10:01 will also be reported
assert result_values[0]["value"] == zeroth_value
assert result_values[1]["value"] == first_value ## to keep parallel to the cumulative test cases, first_value refers to the value at 10:02
assert result_values[-1]["value"] == last_value
def test_histogram_count_no_param(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_count_no_param_bucket"
metrics = Metrics.load_from_file(
FILE,
base_time=now - timedelta(minutes=60),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"increase",
"count",
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
all_series = get_all_series(data, "A")
assert (
len(all_series) == 8
), f"Expected 8 series for 8 le buckets, got {len(all_series)}"
le_buckets = {}
for series in all_series:
le = series.get("labels", [{}])[0].get("value", "unknown")
values = sorted(series.get("values", []), key=lambda x: x["timestamp"])
le_buckets[le] = values
expected_buckets = {"1000", "1500", "2000", "4000", "5000", "6000", "8000", "+Inf"}
assert (
set(le_buckets.keys()) == expected_buckets
), f"Expected endpoints {expected_buckets}, got {set(le_buckets.keys())}"
first_values = {"1000": 33, "1500": 36, "2000": 39, "4000": 42, "5000": 45, "6000": 48, "8000": 51, "+Inf": 54}
last_values = {"1000": 207, "1500": 210, "2000": 213, "4000": 216, "5000": 219, "6000": 222, "8000": 225, "+Inf": 228}
for le, values in le_buckets.items():
assert len(values) == 60
for v in values:
assert (
v["value"] >= 0
), f"Count for {le} should not be negative: {v['value']}"
assert values[0]["value"] == 12345
assert values[1]["value"] == first_values[le] ## to keep parallel to the cumulative test cases, first_value refers to the value at 10:02
assert values[-1]["value"] == last_values[le]

View File

@@ -1,141 +0,0 @@
import random
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, List
import pytest
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.metrics import Metrics
from fixtures.querier import (
build_builder_query,
get_all_series,
get_series_values,
make_query_request,
)
from fixtures.utils import get_testdata_file_path
FILE = get_testdata_file_path("gauge_data_1h.jsonl")
@pytest.mark.parametrize(
"time_agg, space_agg, service, num_elements, start_val, first_val, twentieth_min_val, after_twentieth_min_val",
[
("avg", "avg", "api", 60, 400, 800, 800, 400),
("avg", "avg", "web", 50, 800, 800, 800, 600),
("avg", "avg", "lab", 60, 500, 700, 700, 500),
("sum", "sum", "api", 60, 400, 800, 800, 400),
("sum", "sum", "web", 50, 800, 800, 800, 600),
("sum", "sum", "lab", 60, 1000, 1400, 1400, 1000),
("max", "max", "api", 60, 400, 800, 800, 400),
("max", "max", "web", 50, 800, 800, 800, 600),
("max", "max", "lab", 60, 600, 800, 800, 600),
("avg", "sum", "api", 60, 400, 800, 800, 400),
("avg", "sum", "web", 50, 800, 800, 800, 600),
("avg", "sum", "lab", 60, 500, 700, 700, 500),
("max", "sum", "api", 60, 400, 800, 800, 400),
("max", "sum", "web", 50, 800, 800, 800, 600),
("max", "sum", "lab", 60, 600, 800, 800, 600),
],
)
def test_for_one_service(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
time_agg: str,
space_agg: str,
service: str,
num_elements: float,
start_val: float,
first_val: float,
twentieth_min_val: float,
after_twentieth_min_val: float ## web service has a gap of 10 mins after the 20th minute
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = f"test_memory_{time_agg}_{space_agg}_{service}_usage"
metrics = Metrics.load_from_file(
FILE,
base_time=now - timedelta(minutes=60),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
time_agg,
space_agg,
filter_expression=f'service = "{service}"',
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
assert len(result_values) == num_elements
assert result_values[0]["value"] == start_val
assert result_values[1]["value"] == first_val
assert result_values[19]["value"] == twentieth_min_val
assert result_values[20]["value"] == after_twentieth_min_val
@pytest.mark.parametrize(
"time_agg, space_agg, start_val, first_val, twentieth_min_val, twenty_first_min_val, thirty_first_min_val",
[
("avg", "avg", 566.667, 766.667, 766.667, 450, 500),
("avg", "sum", 1700, 2300, 2300, 900, 1500),
("avg", "max", 800, 800, 800, 500, 600),
("max", "avg", 600, 800, 800, 500, 533.333),
("max", "sum", 1800, 2400, 2400, 1000, 1600),
("max", "max", 800, 800, 800, 600, 600),
],
)
def test_for_multiple_aggregations(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
time_agg: str,
space_agg: str,
start_val: float,
first_val: float,
twentieth_min_val: float,
twenty_first_min_val: float, ## web service has a gap of 10 mins after the 20th minute
thirty_first_min_val: float
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = f"test_memory_{time_agg}_{space_agg}_usage"
metrics = Metrics.load_from_file(
FILE,
base_time=now - timedelta(minutes=60),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
time_agg,
space_agg,
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
assert len(result_values) == 60
assert result_values[0]["value"] == start_val
assert result_values[1]["value"] == first_val
assert result_values[19]["value"] == twentieth_min_val
assert result_values[20]["value"] == twenty_first_min_val
assert result_values[30]["value"] == thirty_first_min_val

View File

@@ -1,220 +0,0 @@
"""
Look at the delta_counters_1h.jsonl file for the relevant data
"""
import os
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Any, Callable, List
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.metrics import Metrics
from fixtures.querier import (
build_builder_query,
get_all_series,
get_series_values,
make_query_request,
)
TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "testdata")
DELTA_COUNTERS_FILE = os.path.join(TESTDATA_DIR, "delta_counters_1h.jsonl")
def test_rate_with_steady_values_and_reset(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_rate_stale"
metrics = Metrics.load_from_file(
DELTA_COUNTERS_FILE,
base_time=now - timedelta(minutes=61),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"rate",
"sum",
filter_expression='endpoint = "/orders"',
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
assert len(result_values) == 60 ## total 61 minutes covered, and 30th minute is missing
assert (
result_values[30]["value"] == 0.0333
) # reset happens and [30] is for 31st minute. 2/60 cuz delta divides by step interval
assert (
result_values[31]["value"] == 0.133
) # i.e 8/60 i.e 31st to 32nd minute changes
count_of_steady_rate = sum(1 for v in result_values if v["value"] == 0.0833)
assert (
count_of_steady_rate == 58
) # 1 reset + 1 high rate are excluded
# All rates should be non-negative (stale periods = 0 rate)
for v in result_values:
assert v["value"] >= 0, f"Rate should not be negative: {v['value']}"
def test_rate_group_by_endpoint(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
metric_name = "test_rate_groupby"
metrics = Metrics.load_from_file(
DELTA_COUNTERS_FILE,
base_time=now - timedelta(minutes=61),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"rate",
"sum",
group_by=["endpoint"],
)
response = make_query_request(signoz, token, start_ms, end_ms, [query])
assert response.status_code == HTTPStatus.OK
data = response.json()
all_series = get_all_series(data, "A")
# Should have 5 different endpoints
assert (
len(all_series) == 5
), f"Expected 5 series for 5 endpoints, got {len(all_series)}"
# endpoint -> values
endpoint_values = {}
for series in all_series:
endpoint = series.get("labels", [{}])[0].get("value", "unknown")
values = sorted(series.get("values", []), key=lambda x: x["timestamp"])
endpoint_values[endpoint] = values
expected_endpoints = {"/products", "/health", "/checkout", "/orders", "/users"}
assert (
set(endpoint_values.keys()) == expected_endpoints
), f"Expected endpoints {expected_endpoints}, got {set(endpoint_values.keys())}"
# at no point rate should be negative
for endpoint, values in endpoint_values.items():
for v in values:
assert (
v["value"] >= 0
), f"Rate for {endpoint} should not be negative: {v['value']}"
# /health: 60 data points (t01-t60), steady +10/min
# rate = 10/60 = 0.167
health_values = endpoint_values["/health"]
assert (
len(health_values) == 60
), f"Expected 60 values for /health, got {len(health_values)}"
count_steady_health = sum(1 for v in health_values if v["value"] == 0.167)
assert (
count_steady_health == 60
), f"Expected == 60 steady rate values (0.167) for /health, got {count_steady_health}"
# all /health rates should be 0.167 except possibly first/last due to boundaries
for v in health_values[1:-1]:
assert v["value"] == 0.167, f"Expected /health rate 0.167, got {v['value']}"
# /products: 51 data points with 10-minute gap (t20-t29 missing), steady +20/min
# rate = 20/60 = 0.333, gap causes lower averaged rate at boundary
products_values = endpoint_values["/products"]
assert (
len(products_values) == 51
), f"Expected 51 values for /products, got {len(products_values)}"
count_steady_products = sum(1 for v in products_values if v["value"] == 0.333)
assert (
count_steady_products == 51
), f"Expected 51 steady rate values (0.333) for /products, got {count_steady_products}"
# /checkout: 61 data points (t00-t60), +1/min normal, +50/min spike at t40-t44
# normal rate = 1/60 = 0.0167, spike rate = 50/60 = 0.833
checkout_values = endpoint_values["/checkout"]
assert (
len(checkout_values) == 61
), f"Expected 61 values for /checkout, got {len(checkout_values)}"
count_steady_checkout = sum(1 for v in checkout_values if v["value"] == 0.0167)
assert (
count_steady_checkout == 56
), f"Expected 56 steady rate values (0.0167) for /checkout, got {count_steady_checkout}"
# check that spike values exist (traffic spike +50/min at t40-t44)
count_spike_checkout = sum(1 for v in checkout_values if v["value"] == 0.833)
assert (
count_spike_checkout == 5
), f"Expected 5 spike rate values (0.833) for /checkout, got {count_spike_checkout}"
# spike values should be consecutive
spike_indices = [
i for i, v in enumerate[Any](checkout_values) if v["value"] == 0.833
]
assert len(spike_indices) == 5, f"Expected 5 spike indices, got {spike_indices}"
# consecutiveness
for i in range(1, len(spike_indices)):
assert (
spike_indices[i] == spike_indices[i - 1] + 1
), f"Spike indices should be consecutive, got {spike_indices}"
# /orders: 60 data points (t00-t60) with gap at t30, counter reset at t31 (150->2)
# rate = 5/60 = 0.0833
# reset at t31 causes: rate at t30 includes gap (lower), t31 has high rate after reset
orders_values = endpoint_values["/orders"]
assert (
len(orders_values) == 60
), f"Expected 59 values for /orders, got {len(orders_values)}"
count_steady_orders = sum(1 for v in orders_values if v["value"] == 0.0833)
assert (
count_steady_orders == 58
), f"Expected 58 steady rate values (0.0833) for /orders, got {count_steady_orders}"
# check for counter reset effects - there should be some non-standard values
non_standard_orders = [v["value"] for v in orders_values if v["value"] != 0.0833]
assert (
len(non_standard_orders) == 2
), f"Expected 2 non-standard values due to counter reset, got {non_standard_orders}"
# post-reset value should be higher (new counter value / interval)
high_rate_orders = [v for v in non_standard_orders if v > 0.0833]
assert (
len(high_rate_orders) == 1
), f"Expected one high rate value after counter reset, got {non_standard_orders}"
# /users: 56 data points (t05-t60), sparse +1 every 5 minutes (12 of them)
# Rate = 1/60 = 0.0167 during increment, 0 during flat periods
users_values = endpoint_values["/users"]
assert (
len(users_values) == 56
), f"Expected 56 values for /users, got {len(users_values)}"
count_zero_users = sum(1 for v in users_values if v["value"] == 0)
# most values should be 0 (flat periods between increments)
assert (
count_zero_users == 44
), f"Expected 44 zero rate values for /users (sparse data), got {count_zero_users}"
# non-zero values should be 0.0167 (1/60 increment rate)
non_zero_users = [v["value"] for v in users_values if v["value"] != 0]
count_increment_rate = sum(1 for v in non_zero_users if v == 0.0167)
assert (
count_increment_rate == 12
), f"Expected 12 increment rate values (0.0167) for /users, got {count_increment_rate}"

View File

@@ -1,634 +0,0 @@
import csv
import io
import json
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, List
from urllib.parse import urlencode
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.logs import Logs
def test_export_logs_csv(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
) -> None:
"""
Setup:
Insert 3 logs with different severity levels and attributes.
Tests:
1. Export logs as CSV format
2. Verify CSV structure and content
3. Validate headers are present
4. Check log data is correctly formatted
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_logs(
[
Logs(
timestamp=now - timedelta(seconds=10),
body="Application started successfully",
severity_text="INFO",
resources={
"service.name": "api-service",
"deployment.environment": "production",
"host.name": "server-01",
},
attributes={
"http.method": "GET",
"http.status_code": 200,
"user.id": "user123",
},
),
Logs(
timestamp=now - timedelta(seconds=8),
body="Connection to database failed",
severity_text="ERROR",
resources={
"service.name": "api-service",
"deployment.environment": "production",
"host.name": "server-01",
},
attributes={
"error.type": "ConnectionError",
"db.name": "production_db",
},
),
Logs(
timestamp=now - timedelta(seconds=5),
body="Request processed",
severity_text="DEBUG",
resources={
"service.name": "worker-service",
"deployment.environment": "production",
"host.name": "server-02",
},
attributes={
"request.id": "req-456",
"duration_ms": 150.5,
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
}
# Export logs as CSV (default format, no source needed)
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=30,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "text/csv"
assert "attachment" in response.headers.get("Content-Disposition", "")
# Parse CSV content
csv_content = response.text
csv_reader = csv.DictReader(io.StringIO(csv_content))
rows = list(csv_reader)
assert len(rows) == 3, f"Expected 3 rows, got {len(rows)}"
# Verify log bodies are present in the exported data
bodies = [row.get("body") for row in rows]
assert "Application started successfully" in bodies
assert "Connection to database failed" in bodies
assert "Request processed" in bodies
# Verify severity levels
severities = [row.get("severity_text") for row in rows]
assert "INFO" in severities
assert "ERROR" in severities
assert "DEBUG" in severities
def test_export_logs_jsonl(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
) -> None:
"""
Setup:
Insert 2 logs with different attributes.
Tests:
1. Export logs as JSONL format
2. Verify JSONL structure and content
3. Check each line is valid JSON
4. Validate log data is correctly formatted
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_logs(
[
Logs(
timestamp=now - timedelta(seconds=10),
body="User logged in",
severity_text="INFO",
resources={
"service.name": "auth-service",
"deployment.environment": "staging",
},
attributes={
"user.email": "test@example.com",
"session.id": "sess-789",
},
),
Logs(
timestamp=now - timedelta(seconds=5),
body="Payment processed successfully",
severity_text="INFO",
resources={
"service.name": "payment-service",
"deployment.environment": "staging",
},
attributes={
"transaction.id": "txn-123",
"amount": 99.99,
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"format": "jsonl",
"source": "logs",
}
# Export logs as JSONL
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
assert "attachment" in response.headers.get("Content-Disposition", "")
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 2, f"Expected 2 lines, got {len(jsonl_lines)}"
# Verify each line is valid JSON
json_objects = []
for line in jsonl_lines:
obj = json.loads(line)
json_objects.append(obj)
assert "id" in obj
assert "timestamp" in obj
assert "body" in obj
assert "severity_text" in obj
# Verify log bodies
bodies = [obj.get("body") for obj in json_objects]
assert "User logged in" in bodies
assert "Payment processed successfully" in bodies
def test_export_logs_with_filter(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
) -> None:
"""
Setup:
Insert logs with different severity levels.
Tests:
1. Export logs with filter applied
2. Verify only filtered logs are returned
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_logs(
[
Logs(
timestamp=now - timedelta(seconds=10),
body="Info message",
severity_text="INFO",
resources={
"service.name": "test-service",
},
attributes={},
),
Logs(
timestamp=now - timedelta(seconds=8),
body="Error message",
severity_text="ERROR",
resources={
"service.name": "test-service",
},
attributes={},
),
Logs(
timestamp=now - timedelta(seconds=5),
body="Another error message",
severity_text="ERROR",
resources={
"service.name": "test-service",
},
attributes={},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"format": "jsonl",
"source": "logs",
"filter": "severity_text = 'ERROR'",
}
# Export logs with filter
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 2, f"Expected 2 lines (filtered), got {len(jsonl_lines)}"
# Verify only ERROR logs are returned
for line in jsonl_lines:
obj = json.loads(line)
assert obj["severity_text"] == "ERROR"
assert "error message" in obj["body"].lower()
def test_export_logs_with_limit(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
) -> None:
"""
Setup:
Insert 5 logs.
Tests:
1. Export logs with limit applied
2. Verify only limited number of logs are returned
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
logs = []
for i in range(5):
logs.append(
Logs(
timestamp=now - timedelta(seconds=i),
body=f"Log message {i}",
severity_text="INFO",
resources={
"service.name": "test-service",
},
attributes={
"index": i,
},
)
)
insert_logs(logs)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"format": "csv",
"source": "logs",
"limit": 3,
}
# Export logs with limit
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "text/csv"
# Parse CSV content
csv_content = response.text
csv_reader = csv.DictReader(io.StringIO(csv_content))
rows = list(csv_reader)
assert len(rows) == 3, f"Expected 3 rows (limited), got {len(rows)}"
def test_export_logs_with_columns(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
) -> None:
"""
Setup:
Insert logs with various attributes.
Tests:
1. Export logs with specific columns
2. Verify only specified columns are returned
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_logs(
[
Logs(
timestamp=now - timedelta(seconds=10),
body="Test log message",
severity_text="INFO",
resources={
"service.name": "test-service",
"deployment.environment": "production",
},
attributes={
"http.method": "GET",
"http.status_code": 200,
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
# Request only specific columns
params = {
"start": start_ns,
"end": end_ns,
"format": "csv",
"source": "logs",
"columns": ["timestamp", "severity_text", "body"],
}
# Export logs with specific columns
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params, doseq=True)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "text/csv"
# Parse CSV content
csv_content = response.text
csv_reader = csv.DictReader(io.StringIO(csv_content))
rows = list(csv_reader)
assert len(rows) == 1
# Verify the specified columns are present
row = rows[0]
assert "timestamp" in row
assert "severity_text" in row
assert "body" in row
assert row["severity_text"] == "INFO"
assert row["body"] == "Test log message"
def test_export_logs_with_order_by(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
) -> None:
"""
Setup:
Insert logs at different timestamps.
Tests:
1. Export logs with ascending timestamp order
2. Verify logs are returned in correct order
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_logs(
[
Logs(
timestamp=now - timedelta(seconds=10),
body="First log",
severity_text="INFO",
resources={
"service.name": "test-service",
},
attributes={},
),
Logs(
timestamp=now - timedelta(seconds=5),
body="Second log",
severity_text="INFO",
resources={
"service.name": "test-service",
},
attributes={},
),
Logs(
timestamp=now - timedelta(seconds=1),
body="Third log",
severity_text="INFO",
resources={
"service.name": "test-service",
},
attributes={},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"format": "jsonl",
"source": "logs",
"order_by": "timestamp:asc",
}
# Export logs with ascending order
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 3
# Verify order - first log should be "First log" (oldest)
json_objects = [json.loads(line) for line in jsonl_lines]
assert json_objects[0]["body"] == "First log"
assert json_objects[1]["body"] == "Second log"
assert json_objects[2]["body"] == "Third log"
def test_export_logs_with_complex_filter(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[List[Logs]], None],
) -> None:
"""
Setup:
Insert logs with various service names and severity levels.
Tests:
1. Export logs with complex filter (multiple conditions)
2. Verify only logs matching all conditions are returned
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_logs(
[
Logs(
timestamp=now - timedelta(seconds=10),
body="API error occurred",
severity_text="ERROR",
resources={
"service.name": "api-service",
},
attributes={},
),
Logs(
timestamp=now - timedelta(seconds=8),
body="Worker info message",
severity_text="INFO",
resources={
"service.name": "worker-service",
},
attributes={},
),
Logs(
timestamp=now - timedelta(seconds=5),
body="API info message",
severity_text="INFO",
resources={
"service.name": "api-service",
},
attributes={},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
# Filter for api-service AND ERROR severity
params = {
"start": start_ns,
"end": end_ns,
"format": "jsonl",
"source": "logs",
"filter": "service.name = 'api-service' AND severity_text = 'ERROR'",
}
# Export logs with complex filter
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert (
len(jsonl_lines) == 1
), f"Expected 1 line (complex filter), got {len(jsonl_lines)}"
# Verify the filtered log
filtered_obj = json.loads(jsonl_lines[0])
assert filtered_obj["body"] == "API error occurred"
assert filtered_obj["severity_text"] == "ERROR"

View File

@@ -1,782 +0,0 @@
import csv
import io
import json
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, List
from urllib.parse import urlencode
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
def test_export_traces_csv(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[List[Traces]], None],
) -> None:
"""
Setup:
Insert 3 traces with different attributes.
Tests:
1. Export traces as CSV format
2. Verify CSV structure and content
3. Validate headers are present
4. Check trace data is correctly formatted
"""
http_service_trace_id = TraceIdGenerator.trace_id()
http_service_span_id = TraceIdGenerator.span_id()
http_service_db_span_id = TraceIdGenerator.span_id()
topic_service_trace_id = TraceIdGenerator.trace_id()
topic_service_span_id = TraceIdGenerator.span_id()
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_traces(
[
Traces(
timestamp=now - timedelta(seconds=4),
duration=timedelta(seconds=3),
trace_id=http_service_trace_id,
span_id=http_service_span_id,
parent_span_id="",
name="POST /integration",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"deployment.environment": "production",
"service.name": "http-service",
"os.type": "linux",
"host.name": "linux-000",
},
attributes={
"net.transport": "IP.TCP",
"http.scheme": "http",
"http.user_agent": "Integration Test",
"http.request.method": "POST",
"http.response.status_code": "200",
},
),
Traces(
timestamp=now - timedelta(seconds=3.5),
duration=timedelta(seconds=0.5),
trace_id=http_service_trace_id,
span_id=http_service_db_span_id,
parent_span_id=http_service_span_id,
name="SELECT",
kind=TracesKind.SPAN_KIND_CLIENT,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"deployment.environment": "production",
"service.name": "http-service",
"os.type": "linux",
"host.name": "linux-000",
},
attributes={
"db.name": "integration",
"db.operation": "SELECT",
"db.statement": "SELECT * FROM integration",
},
),
Traces(
timestamp=now - timedelta(seconds=1),
duration=timedelta(seconds=2),
trace_id=topic_service_trace_id,
span_id=topic_service_span_id,
parent_span_id="",
name="topic publish",
kind=TracesKind.SPAN_KIND_PRODUCER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"deployment.environment": "production",
"service.name": "topic-service",
"os.type": "linux",
"host.name": "linux-001",
},
attributes={
"message.type": "SENT",
"messaging.operation": "publish",
"messaging.message.id": "001",
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"source": "traces",
"limit": 1000,
}
# Export traces as CSV (GET for simple queries)
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=30,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "text/csv"
assert "attachment" in response.headers.get("Content-Disposition", "")
# Parse CSV content
csv_content = response.text
csv_reader = csv.DictReader(io.StringIO(csv_content))
rows = list(csv_reader)
assert len(rows) == 3, f"Expected 3 rows, got {len(rows)}"
# Verify trace IDs are present in the exported data
trace_ids = [row.get("trace_id") for row in rows]
assert http_service_trace_id in trace_ids
assert topic_service_trace_id in trace_ids
# Verify span names are present
span_names = [row.get("name") for row in rows]
assert "POST /integration" in span_names
assert "SELECT" in span_names
assert "topic publish" in span_names
def test_export_traces_jsonl(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[List[Traces]], None],
) -> None:
"""
Setup:
Insert 2 traces with different attributes.
Tests:
1. Export traces as JSONL format
2. Verify JSONL structure and content
3. Check each line is valid JSON
4. Validate trace data is correctly formatted
"""
http_service_trace_id = TraceIdGenerator.trace_id()
http_service_span_id = TraceIdGenerator.span_id()
topic_service_trace_id = TraceIdGenerator.trace_id()
topic_service_span_id = TraceIdGenerator.span_id()
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_traces(
[
Traces(
timestamp=now - timedelta(seconds=4),
duration=timedelta(seconds=3),
trace_id=http_service_trace_id,
span_id=http_service_span_id,
parent_span_id="",
name="POST /api/test",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "api-service",
"deployment.environment": "staging",
},
attributes={
"http.request.method": "POST",
"http.response.status_code": "201",
},
),
Traces(
timestamp=now - timedelta(seconds=2),
duration=timedelta(seconds=1),
trace_id=topic_service_trace_id,
span_id=topic_service_span_id,
parent_span_id="",
name="queue.process",
kind=TracesKind.SPAN_KIND_CONSUMER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "queue-service",
"deployment.environment": "staging",
},
attributes={
"messaging.operation": "process",
"messaging.system": "rabbitmq",
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"format": "jsonl",
"source": "traces",
"limit": 1000,
}
# Export traces as JSONL (GET for simple queries)
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
assert "attachment" in response.headers.get("Content-Disposition", "")
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 2, f"Expected 2 lines, got {len(jsonl_lines)}"
# Verify each line is valid JSON
json_objects = []
for line in jsonl_lines:
obj = json.loads(line)
json_objects.append(obj)
assert "trace_id" in obj
assert "span_id" in obj
assert "name" in obj
# Verify trace IDs are present
trace_ids = [obj.get("trace_id") for obj in json_objects]
assert http_service_trace_id in trace_ids
assert topic_service_trace_id in trace_ids
# Verify span names are present
span_names = [obj.get("name") for obj in json_objects]
assert "POST /api/test" in span_names
assert "queue.process" in span_names
def test_export_traces_with_filter(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[List[Traces]], None],
) -> None:
"""
Setup:
Insert traces with different service names.
Tests:
1. Export traces with filter applied
2. Verify only filtered traces are returned
"""
service_a_trace_id = TraceIdGenerator.trace_id()
service_a_span_id = TraceIdGenerator.span_id()
service_b_trace_id = TraceIdGenerator.trace_id()
service_b_span_id = TraceIdGenerator.span_id()
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_traces(
[
Traces(
timestamp=now - timedelta(seconds=4),
duration=timedelta(seconds=1),
trace_id=service_a_trace_id,
span_id=service_a_span_id,
parent_span_id="",
name="operation-a",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "service-a",
},
attributes={},
),
Traces(
timestamp=now - timedelta(seconds=2),
duration=timedelta(seconds=1),
trace_id=service_b_trace_id,
span_id=service_b_span_id,
parent_span_id="",
name="operation-b",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "service-b",
},
attributes={},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"format": "jsonl",
"source": "traces",
"limit": 1000,
"filter": "service.name = 'service-a'",
}
# Export traces with filter (GET supports filter param)
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 1, f"Expected 1 line (filtered), got {len(jsonl_lines)}"
# Verify the filtered trace
filtered_obj = json.loads(jsonl_lines[0])
assert filtered_obj["trace_id"] == service_a_trace_id
assert filtered_obj["name"] == "operation-a"
def test_export_traces_with_limit(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[List[Traces]], None],
) -> None:
"""
Setup:
Insert 5 traces.
Tests:
1. Export traces with limit applied
2. Verify only limited number of traces are returned
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
traces = []
for i in range(5):
traces.append(
Traces(
timestamp=now - timedelta(seconds=i),
duration=timedelta(seconds=1),
trace_id=TraceIdGenerator.trace_id(),
span_id=TraceIdGenerator.span_id(),
parent_span_id="",
name=f"operation-{i}",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "test-service",
},
attributes={},
)
)
insert_traces(traces)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
params = {
"start": start_ns,
"end": end_ns,
"format": "csv",
"source": "traces",
"limit": 3,
}
# Export traces with limit (GET supports limit param)
response = requests.get(
signoz.self.host_configs["8080"].get(
f"/api/v1/export_raw_data?{urlencode(params)}"
),
timeout=10,
headers={
"authorization": f"Bearer {token}",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "text/csv"
# Parse CSV content
csv_content = response.text
csv_reader = csv.DictReader(io.StringIO(csv_content))
rows = list(csv_reader)
assert len(rows) == 3, f"Expected 3 rows (limited), got {len(rows)}"
def test_export_traces_multiple_queries_rejected(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
) -> None:
"""
Tests:
1. POST with multiple builder queries but no trace operator is rejected
2. Verify 400 error is returned
"""
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
body = {
"start": start_ns,
"end": end_ns,
"compositeQuery": {
"queries": [
{
"type": "builder_query",
"spec": {
"signal": "traces",
"name": "A",
"limit": 1000,
"filter": {"expression": "service.name = 'service-a'"},
},
},
{
"type": "builder_query",
"spec": {
"signal": "traces",
"name": "B",
"limit": 1000,
"filter": {"expression": "service.name = 'service-b'"},
},
},
]
},
}
url = signoz.self.host_configs["8080"].get("/api/v1/export_raw_data?format=jsonl")
response = requests.post(
url,
json=body,
timeout=10,
headers={
"authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
)
assert response.status_code == HTTPStatus.BAD_REQUEST
def test_export_traces_with_composite_query_trace_operator(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[List[Traces]], None],
) -> None:
"""
Setup:
Insert multiple traces with parent-child relationships.
Tests:
1. Export traces using trace operator in composite query (POST)
2. Verify trace operator query works correctly
"""
parent_trace_id = TraceIdGenerator.trace_id()
parent_span_id = TraceIdGenerator.span_id()
child_span_id_1 = TraceIdGenerator.span_id()
child_span_id_2 = TraceIdGenerator.span_id()
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_traces(
[
Traces(
timestamp=now - timedelta(seconds=10),
duration=timedelta(seconds=5),
trace_id=parent_trace_id,
span_id=parent_span_id,
parent_span_id="",
name="parent-operation",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "parent-service",
},
attributes={
"operation.type": "parent",
},
),
Traces(
timestamp=now - timedelta(seconds=9),
duration=timedelta(seconds=2),
trace_id=parent_trace_id,
span_id=child_span_id_1,
parent_span_id=parent_span_id,
name="child-operation-1",
kind=TracesKind.SPAN_KIND_INTERNAL,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "parent-service",
},
attributes={
"operation.type": "child",
},
),
Traces(
timestamp=now - timedelta(seconds=7),
duration=timedelta(seconds=1),
trace_id=parent_trace_id,
span_id=child_span_id_2,
parent_span_id=parent_span_id,
name="child-operation-2",
kind=TracesKind.SPAN_KIND_INTERNAL,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "parent-service",
},
attributes={
"operation.type": "child",
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
# A: spans with operation.type = 'parent'
query_a = {
"type": "builder_query",
"spec": {
"signal": "traces",
"name": "A",
"limit": 1000,
"filter": {"expression": "operation.type = 'parent'"},
},
}
# B: spans with operation.type = 'child'
query_b = {
"type": "builder_query",
"spec": {
"signal": "traces",
"name": "B",
"limit": 1000,
"filter": {"expression": "operation.type = 'child'"},
},
}
# Trace operator: find traces where A has a direct descendant B
query_c = {
"type": "builder_trace_operator",
"spec": {
"name": "C",
"expression": "A => B",
"returnSpansFrom": "A",
"limit": 1000,
"order": [{"key": {"name": "timestamp"}, "direction": "desc"}],
},
}
body = {
"start": start_ns,
"end": end_ns,
"requestType": "raw",
"compositeQuery": {
"queries": [query_a, query_b, query_c],
},
}
url = signoz.self.host_configs["8080"].get("/api/v1/export_raw_data?format=jsonl")
response = requests.post(
url,
json=body,
timeout=10,
headers={
"authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
)
print(response.text)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 1, f"Expected at least 1 line, got {len(jsonl_lines)}"
# Verify all returned spans belong to the matched trace
json_objects = [json.loads(line) for line in jsonl_lines]
trace_ids = [obj.get("trace_id") for obj in json_objects]
assert all(tid == parent_trace_id for tid in trace_ids)
# Verify the parent span (returnSpansFrom = "A") is present
span_names = [obj.get("name") for obj in json_objects]
assert "parent-operation" in span_names
def test_export_traces_with_select_fields(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[List[Traces]], None],
) -> None:
"""
Setup:
Insert traces with various attributes.
Tests:
1. Export traces with specific select fields via POST
2. Verify only specified fields are returned in the output
"""
trace_id = TraceIdGenerator.trace_id()
span_id = TraceIdGenerator.span_id()
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
insert_traces(
[
Traces(
timestamp=now - timedelta(seconds=10),
duration=timedelta(seconds=2),
trace_id=trace_id,
span_id=span_id,
parent_span_id="",
name="test-operation",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
status_message="",
resources={
"service.name": "test-service",
"deployment.environment": "production",
"host.name": "server-01",
},
attributes={
"http.method": "POST",
"http.status_code": "201",
"user.id": "user123",
},
),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Calculate timestamps in nanoseconds
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
end_ns = int(now.timestamp() * 1e9)
body = {
"start": start_ns,
"end": end_ns,
"requestType": "raw",
"compositeQuery": {
"queries": [
{
"type": "builder_query",
"spec": {
"signal": "traces",
"name": "A",
"limit": 1000,
"selectFields": [
{
"name": "trace_id",
"fieldDataType": "string",
"fieldContext": "span",
"signal": "traces",
},
{
"name": "span_id",
"fieldDataType": "string",
"fieldContext": "span",
"signal": "traces",
},
{
"name": "name",
"fieldDataType": "string",
"fieldContext": "span",
"signal": "traces",
},
{
"name": "service.name",
"fieldDataType": "string",
"fieldContext": "resource",
"signal": "traces",
},
],
},
}
]
},
}
url = signoz.self.host_configs["8080"].get("/api/v1/export_raw_data?format=jsonl")
response = requests.post(
url,
json=body,
timeout=10,
headers={
"authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
)
assert response.status_code == HTTPStatus.OK
assert response.headers["Content-Type"] == "application/x-ndjson"
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 1
# Verify the selected fields are present
result = json.loads(jsonl_lines[0])
assert "trace_id" in result
assert "span_id" in result
assert "name" in result
# Verify values
assert result["trace_id"] == trace_id
assert result["span_id"] == span_id
assert result["name"] == "test-operation"

View File

@@ -1,288 +0,0 @@
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:01:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:02:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:03:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:04:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:05:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:06:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:07:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:08:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:09:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:10:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:11:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:12:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:13:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:14:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:15:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:16:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:17:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:18:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:19:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:20:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:21:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:22:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:23:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:24:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:25:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:26:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:27:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:28:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:29:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:30:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:31:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:32:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:33:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:34:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:35:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:36:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:37:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:38:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:39:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:40:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:41:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:42:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:43:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:44:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:45:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:46:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:47:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:48:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:49:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:50:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:51:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:52:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:53:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:54:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:55:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:56:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:57:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:58:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:59:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T11:00:00+00:00","value":10,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:05:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:06:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:07:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:08:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:09:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:10:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:11:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:12:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:13:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:14:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:15:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:16:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:17:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:18:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:19:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:20:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:21:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:22:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:23:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:24:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:25:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:26:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:27:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:28:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:29:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:30:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:31:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:32:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:33:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:34:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:35:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:36:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:37:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:38:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:39:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:40:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:41:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:42:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:43:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:44:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:45:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:46:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:47:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:48:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:49:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:50:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:51:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:52:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:53:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:54:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:55:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:56:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:57:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:58:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:59:00+00:00","value":0,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T11:00:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:00:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:01:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:02:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:03:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:04:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:05:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:06:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:07:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:08:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:09:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:10:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:11:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:12:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:13:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:14:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:15:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:16:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:17:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:18:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:19:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:20:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:21:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:22:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:23:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:24:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:25:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:26:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:27:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:28:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:29:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:31:00+00:00","value":2,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:32:00+00:00","value":8,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:33:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:34:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:35:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:36:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:37:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:38:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:39:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:40:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:41:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:42:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:43:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:44:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:45:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:46:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:47:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:48:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:49:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:50:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:51:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:52:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:53:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:54:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:55:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:56:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:57:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:58:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:59:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T11:00:00+00:00","value":5,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:00:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:01:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:02:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:03:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:04:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:05:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:06:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:07:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:08:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:09:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:10:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:11:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:12:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:13:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:14:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:15:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:16:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:17:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:18:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:19:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:30:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:31:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:32:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:33:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:34:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:35:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:36:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:37:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:38:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:39:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:40:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:41:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:42:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:43:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:44:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:45:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:46:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:47:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:48:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:49:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:50:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:51:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:52:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:53:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:54:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:55:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:56:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:57:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:58:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:59:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T11:00:00+00:00","value":20,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:01:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:02:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:03:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:04:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:05:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:06:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:07:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:08:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:09:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:10:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:11:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:12:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:13:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:14:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:15:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:16:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:17:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:18:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:19:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:20:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:21:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:22:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:23:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:24:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:25:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:26:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:27:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:28:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:29:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:30:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:31:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:32:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:33:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:34:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:35:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:36:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:37:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:38:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:39:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:40:00+00:00","value":50,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:41:00+00:00","value":50,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:42:00+00:00","value":50,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:43:00+00:00","value":50,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:44:00+00:00","value":50,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:45:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:46:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:47:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:48:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:49:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:50:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:51:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:52:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:53:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:54:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:55:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:56:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:57:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:58:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:59:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T11:00:00+00:00","value":1,"temporality":"Delta","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}

View File

@@ -1,229 +0,0 @@
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:01:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:02:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:03:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:04:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:05:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:06:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:07:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:08:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:09:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:10:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:11:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:12:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:13:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:14:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:15:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:16:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:17:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:18:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:19:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:20:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:21:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:22:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:23:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:24:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:25:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:26:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:27:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:28:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:29:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:30:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:31:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:32:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:33:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:34:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:35:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:36:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:37:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:38:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:39:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:40:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:41:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:42:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:43:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:44:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:45:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:46:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:47:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:48:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:49:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:50:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:51:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:52:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:53:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:54:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:55:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:56:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:57:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:58:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T10:59:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"api"},"timestamp":"2025-01-10T11:00:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:01:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:02:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:03:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:04:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:05:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:06:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:07:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:08:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:09:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:10:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:11:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:12:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:13:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:14:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:15:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:16:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:17:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:18:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:19:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:20:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:31:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:32:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:33:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:34:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:35:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:36:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:37:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:38:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:39:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:40:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:41:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:42:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:43:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:44:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:45:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:46:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:47:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:48:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:49:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:50:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:51:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:52:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:53:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:54:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:55:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:56:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:57:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:58:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T10:59:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"web"},"timestamp":"2025-01-10T11:00:00+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:01:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:01:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:02:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:02:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:03:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:03:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:04:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:04:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:05:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:05:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:06:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:06:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:07:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:07:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:08:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:08:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:09:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:09:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:10:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:10:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:11:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:11:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:12:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:12:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:13:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:13:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:14:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:14:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:15:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:15:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:16:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:16:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:17:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:17:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:18:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:18:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:19:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:19:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:20:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:20:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:21:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:21:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:22:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:22:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:23:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:23:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:24:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:24:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:25:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:25:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:26:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:26:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:27:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:27:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:28:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:28:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:29:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:29:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:30:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:30:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:31:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:31:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:32:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:32:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:33:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:33:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:34:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:34:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:35:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:35:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:36:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:36:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:37:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:37:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:38:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:38:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:39:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:39:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:40:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:40:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:41:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:41:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:42:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:42:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:43:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:43:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:44:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:44:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:45:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:45:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:46:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:46:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:47:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:47:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:48:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:48:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:49:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:49:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:50:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:50:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:51:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:51:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:52:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:52:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:53:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:53:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:54:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:54:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:55:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:55:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:56:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:56:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:57:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:57:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:58:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:58:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:59:00+00:00","value":400,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T10:59:30+00:00","value":600,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
{"metric_name":"system.memory.usage","labels":{"__temporality__":"Unspecified","service":"lab"},"timestamp":"2025-01-10T11:00:00+00:00","value":800,"temporality":"Unspecified","type_":"Gauge","is_monotonic":false,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}

File diff suppressed because it is too large Load Diff