Compare commits

...

5 Commits

Author SHA1 Message Date
Srikanth Chekuri
3966c9b294 Merge branch 'main' into remove-v1-metrics 2026-03-31 03:58:30 +05:30
srikanthccv
b3c5dfec22 chore: fix tests 2026-03-30 23:35:24 +05:30
srikanthccv
1e7a77b39d chore: fix ci 2026-03-30 22:53:31 +05:30
srikanthccv
be5e4d4004 chore: resolve conflicts 2026-03-30 22:23:00 +05:30
srikanthccv
62231aedb7 chore: remove v1 metrics explorer code 2026-03-30 21:53:02 +05:30
59 changed files with 857 additions and 3977 deletions

View File

@@ -1114,6 +1114,33 @@ components:
enabled:
type: boolean
type: object
MetricsexplorertypesInspectMetricsRequest:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
metricName:
type: string
start:
format: int64
type: integer
required:
- metricName
- start
- end
type: object
MetricsexplorertypesInspectMetricsResponse:
properties:
series:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
nullable: true
type: array
required:
- series
type: object
MetricsexplorertypesListMetric:
properties:
description:
@@ -1262,6 +1289,13 @@ components:
- temporality
- isMonotonic
type: object
MetricsexplorertypesMetricsOnboardingResponse:
properties:
hasMetrics:
type: boolean
required:
- hasMetrics
type: object
MetricsexplorertypesStat:
properties:
description:
@@ -7750,6 +7784,111 @@ paths:
summary: Update metric metadata
tags:
- metrics
/api/v2/metrics/inspect:
post:
deprecated: false
description: Returns raw time series data points for a metric within a time
range (max 30 minutes). Each series includes labels and timestamp/value pairs.
operationId: InspectMetrics
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsRequest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Inspect raw metric data points
tags:
- metrics
/api/v2/metrics/onboarding:
get:
deprecated: false
description: Lightweight endpoint that checks if any non-SigNoz metrics have
been ingested, used for onboarding status detection
operationId: GetMetricsOnboardingStatus
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesMetricsOnboardingResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"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: Check if non-SigNoz metrics have been received
tags:
- metrics
/api/v2/metrics/stats:
post:
deprecated: false

View File

@@ -242,7 +242,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
err := s.signoz.APIServer.AddToRouter(r)

View File

@@ -31,10 +31,13 @@ import type {
GetMetricHighlightsPathParameters,
GetMetricMetadata200,
GetMetricMetadataPathParameters,
GetMetricsOnboardingStatus200,
GetMetricsStats200,
GetMetricsTreemap200,
InspectMetrics200,
ListMetrics200,
ListMetricsParams,
MetricsexplorertypesInspectMetricsRequestDTO,
MetricsexplorertypesStatsRequestDTO,
MetricsexplorertypesTreemapRequestDTO,
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
@@ -778,6 +781,176 @@ export const useUpdateMetricMetadata = <
return useMutation(mutationOptions);
};
/**
* Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.
* @summary Inspect raw metric data points
*/
export const inspectMetrics = (
metricsexplorertypesInspectMetricsRequestDTO: BodyType<MetricsexplorertypesInspectMetricsRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<InspectMetrics200>({
url: `/api/v2/metrics/inspect`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesInspectMetricsRequestDTO,
signal,
});
};
export const getInspectMetricsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
> => {
const mutationKey = ['inspectMetrics'];
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 inspectMetrics>>,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> }
> = (props) => {
const { data } = props ?? {};
return inspectMetrics(data);
};
return { mutationFn, ...mutationOptions };
};
export type InspectMetricsMutationResult = NonNullable<
Awaited<ReturnType<typeof inspectMetrics>>
>;
export type InspectMetricsMutationBody = BodyType<MetricsexplorertypesInspectMetricsRequestDTO>;
export type InspectMetricsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Inspect raw metric data points
*/
export const useInspectMetrics = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
> => {
const mutationOptions = getInspectMetricsMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection
* @summary Check if non-SigNoz metrics have been received
*/
export const getMetricsOnboardingStatus = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetMetricsOnboardingStatus200>({
url: `/api/v2/metrics/onboarding`,
method: 'GET',
signal,
});
};
export const getGetMetricsOnboardingStatusQueryKey = () => {
return [`/api/v2/metrics/onboarding`] as const;
};
export const getGetMetricsOnboardingStatusQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricsOnboardingStatusQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>
> = ({ signal }) => getMetricsOnboardingStatus(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricsOnboardingStatusQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>
>;
export type GetMetricsOnboardingStatusQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Check if non-SigNoz metrics have been received
*/
export function useGetMetricsOnboardingStatus<
TData = Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricsOnboardingStatusQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Check if non-SigNoz metrics have been received
*/
export const invalidateGetMetricsOnboardingStatus = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricsOnboardingStatusQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint provides list of metrics with their number of samples and timeseries for the given time range
* @summary Get metrics statistics

View File

@@ -1363,6 +1363,32 @@ export interface GlobaltypesTokenizerConfigDTO {
enabled?: boolean;
}
export interface MetricsexplorertypesInspectMetricsRequestDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type string
*/
metricName: string;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface MetricsexplorertypesInspectMetricsResponseDTO {
/**
* @type array
* @nullable true
*/
series: Querybuildertypesv5TimeSeriesDTO[] | null;
}
export interface MetricsexplorertypesListMetricDTO {
/**
* @type string
@@ -1508,6 +1534,13 @@ export interface MetricsexplorertypesMetricMetadataDTO {
unit: string;
}
export interface MetricsexplorertypesMetricsOnboardingResponseDTO {
/**
* @type boolean
*/
hasMetrics: boolean;
}
export interface MetricsexplorertypesStatDTO {
/**
* @type string
@@ -4391,6 +4424,22 @@ export type GetMetricMetadata200 = {
export type UpdateMetricMetadataPathParameters = {
metricName: string;
};
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricsOnboardingStatus200 = {
data: MetricsexplorertypesMetricsOnboardingResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricsStats200 = {
data: MetricsexplorertypesStatsResponseDTO;
/**

View File

@@ -1,54 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface InspectMetricsRequest {
metricName: string;
start: number;
end: number;
filters: TagFilter;
}
export interface InspectMetricsResponse {
status: string;
data: {
series: InspectMetricsSeries[];
};
}
export interface InspectMetricsSeries {
title?: string;
strokeColor?: string;
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: InspectMetricsTimestampValue[];
}
interface InspectMetricsTimestampValue {
timestamp: number;
value: string;
}
export const getInspectMetricsDetails = async (
request: InspectMetricsRequest,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<InspectMetricsResponse> | ErrorResponse> => {
try {
const response = await axios.post(`/metrics/inspect`, request, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,75 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricType } from './getMetricsList';
export interface MetricDetails {
name: string;
description: string;
type: string;
unit: string;
timeseries: number;
samples: number;
timeSeriesTotal: number;
timeSeriesActive: number;
lastReceived: string;
attributes: MetricDetailsAttribute[] | null;
metadata?: {
metric_type: MetricType;
description: string;
unit: string;
temporality?: Temporality;
};
alerts: MetricDetailsAlert[] | null;
dashboards: MetricDetailsDashboard[] | null;
}
export enum Temporality {
CUMULATIVE = 'Cumulative',
DELTA = 'Delta',
}
export interface MetricDetailsAttribute {
key: string;
value: string[];
valueCount: number;
}
export interface MetricDetailsAlert {
alert_name: string;
alert_id: string;
}
export interface MetricDetailsDashboard {
dashboard_name: string;
dashboard_id: string;
}
export interface MetricDetailsResponse {
status: string;
data: MetricDetails;
}
export const getMetricDetails = async (
metricName: string,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricDetailsResponse> | ErrorResponse> => {
try {
const response = await axios.get(`/metrics/${metricName}/metadata`, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,67 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import {
OrderByPayload,
TreemapViewType,
} from 'container/MetricsExplorer/Summary/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: OrderByPayload;
}
export enum MetricType {
SUM = 'Sum',
GAUGE = 'Gauge',
HISTOGRAM = 'Histogram',
SUMMARY = 'Summary',
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
}
export interface MetricsListItemData {
metric_name: string;
description: string;
type: MetricType;
unit: string;
[TreemapViewType.TIMESERIES]: number;
[TreemapViewType.SAMPLES]: number;
lastReceived: string;
}
export interface MetricsListResponse {
status: string;
data: {
metrics: MetricsListItemData[];
total?: number;
};
}
export const getMetricsList = async (
props: MetricsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,44 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export interface MetricsListFilterKeysResponse {
status: string;
data: {
metricColumns: string[];
attributeKeys: BaseAutocompleteData[];
};
}
export interface GetMetricsListFilterKeysParams {
searchText: string;
limit?: number;
}
export const getMetricsListFilterKeys = async (
params: GetMetricsListFilterKeysParams,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse> => {
try {
const response = await axios.get('/metrics/filters/keys', {
params: {
searchText: params.searchText,
limit: params.limit,
},
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,43 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface MetricsListFilterValuesPayload {
filterAttributeKeyDataType: string;
filterKey: string;
searchText: string;
limit: number;
}
export interface MetricsListFilterValuesResponse {
status: string;
data: {
filterValues: string[];
};
}
export const getMetricsListFilterValues = async (
props: MetricsListFilterValuesPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/metrics/filters/values', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,60 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export interface RelatedMetricsPayload {
start: number;
end: number;
currentMetricName: string;
}
export interface RelatedMetricDashboard {
dashboard_name: string;
dashboard_id: string;
widget_id: string;
widget_name: string;
}
export interface RelatedMetricAlert {
alert_name: string;
alert_id: string;
}
export interface RelatedMetric {
name: string;
query: IBuilderQuery;
dashboards: RelatedMetricDashboard[];
alerts: RelatedMetricAlert[];
}
export interface RelatedMetricsResponse {
status: 'success';
data: {
related_metrics: RelatedMetric[];
};
}
export const getRelatedMetrics = async (
props: RelatedMetricsPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<RelatedMetricsResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics/related', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,10 +1,11 @@
/* eslint-disable sonarjs/no-duplicate-string */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
import { Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricsOnboardingStatus } from 'api/generated/services/metrics';
import listUserPreferences from 'api/v1/user/preferences/list';
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
import { PersistedAnnouncementBanner } from 'components/AnnouncementBanner';
@@ -15,10 +16,8 @@ import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -127,38 +126,7 @@ export default function Home(): JSX.Element {
);
// Detect Metrics
const query = useMemo(() => {
const baseQuery = getMetricsListQuery();
let queryStartTime = startTime;
let queryEndTime = endTime;
if (!startTime || !endTime) {
const now = new Date();
const startTime = new Date(now.getTime() - homeInterval);
const endTime = now;
queryStartTime = startTime.getTime();
queryEndTime = endTime.getTime();
}
return {
...baseQuery,
limit: 10,
offset: 0,
filters: {
items: [],
op: 'AND',
},
start: queryStartTime,
end: queryEndTime,
};
}, [startTime, endTime]);
const { data: metricsData } = useGetMetricsList(query, {
enabled: !!query,
queryKey: ['metricsList', query],
});
const { data: metricsOnboardingData } = useGetMetricsOnboardingStatus();
const [isLogsIngestionActive, setIsLogsIngestionActive] = useState(false);
const [isTracesIngestionActive, setIsTracesIngestionActive] = useState(false);
@@ -284,14 +252,12 @@ export default function Home(): JSX.Element {
}, [tracesData, handleUpdateChecklistDoneItem]);
useEffect(() => {
const metricsDataTotal = metricsData?.payload?.data?.total ?? 0;
if (metricsDataTotal > 0) {
if (metricsOnboardingData?.data?.hasMetrics) {
setIsMetricsIngestionActive(true);
handleUpdateChecklistDoneItem('ADD_DATA_SOURCE');
handleUpdateChecklistDoneItem('SEND_METRICS');
}
}, [metricsData, handleUpdateChecklistDoneItem]);
}, [metricsOnboardingData, handleUpdateChecklistDoneItem]);
useEffect(() => {
logEvent('Homepage: Visited', {});

View File

@@ -1,37 +1,3 @@
import { UseQueryResult } from 'react-query';
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
RELATED_METRICS = 'related-metrics',
}
export interface TimeSeriesProps {
showOneChartPerQuery: boolean;
}
export interface RelatedMetricsProps {
metricNames: string[];
}
export interface RelatedMetricsCardProps {
metric: RelatedMetricWithQueryResult;
}
export interface UseGetRelatedMetricsGraphsProps {
selectedMetricName: string | null;
startMs: number;
endMs: number;
}
export interface UseGetRelatedMetricsGraphsReturn {
relatedMetrics: RelatedMetricWithQueryResult[];
isRelatedMetricsLoading: boolean;
isRelatedMetricsError: boolean;
}
export interface RelatedMetricWithQueryResult extends RelatedMetric {
queryResult: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>;
}

View File

@@ -30,31 +30,6 @@
}
}
.explore-tabs {
margin: 15px 0;
.tab {
background-color: var(--bg-slate-500);
border-color: var(--bg-ink-200);
width: 180px;
padding: 16px 0;
display: flex;
justify-content: center;
align-items: center;
}
.tab:first-of-type {
border-top-left-radius: 2px;
}
.tab:last-of-type {
border-top-right-radius: 2px;
}
.selected-view {
background: var(--bg-ink-500);
}
}
.explore-content {
padding: 0 8px;
@@ -116,81 +91,6 @@
width: 100%;
height: fit-content;
}
.related-metrics-container {
width: 100%;
min-height: 300px;
display: flex;
flex-direction: column;
gap: 10px;
.related-metrics-header {
display: flex;
align-items: center;
justify-content: flex-start;
.metric-name-select {
width: 20%;
margin-right: 10px;
}
.related-metrics-input {
width: 40%;
.ant-input-wrapper {
.ant-input-group-addon {
.related-metrics-select {
width: 250px;
border: 1px solid var(--bg-slate-500) !important;
.ant-select-selector {
text-align: left;
color: var(--text-vanilla-500) !important;
}
}
}
}
}
}
.related-metrics-body {
margin-top: 20px;
max-height: 650px;
overflow-y: scroll;
.related-metrics-card-container {
margin-bottom: 20px;
min-height: 640px;
.related-metrics-card {
display: flex;
flex-direction: column;
gap: 16px;
.related-metrics-card-error {
padding-top: 10px;
height: fit-content;
width: fit-content;
}
}
}
}
}
}
}
.lightMode {
.metrics-explorer-explore-container {
.explore-tabs {
.tab {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
}
.selected-view {
background: var(--bg-vanilla-500);
}
}
}
}

View File

@@ -32,7 +32,6 @@ import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import MetricDetails from '../MetricDetails/MetricDetails';
import TimeSeries from './TimeSeries';
import { ExplorerTabs } from './types';
import {
getMetricUnits,
splitQueryIntoOneChartPerQuery,
@@ -95,7 +94,6 @@ function Explorer(): JSX.Element {
const [disableOneChartPerQuery, toggleDisableOneChartPerQuery] = useState(
false,
);
const [selectedTab] = useState<ExplorerTabs>(ExplorerTabs.TIME_SERIES);
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
const unitsLength = useMemo(() => units.length, [units]);
@@ -319,48 +317,21 @@ function Explorer(): JSX.Element {
showFunctions={false}
version="v3"
/>
{/* TODO: Enable once we have resolved all related metrics issues */}
{/* <Button.Group className="explore-tabs">
<Button
value={ExplorerTabs.TIME_SERIES}
className={classNames('tab', {
'selected-view': selectedTab === ExplorerTabs.TIME_SERIES,
})}
onClick={(): void => setSelectedTab(ExplorerTabs.TIME_SERIES)}
>
<Typography.Text>Time series</Typography.Text>
</Button>
<Button
value={ExplorerTabs.RELATED_METRICS}
className={classNames('tab', {
'selected-view': selectedTab === ExplorerTabs.RELATED_METRICS,
})}
onClick={(): void => setSelectedTab(ExplorerTabs.RELATED_METRICS)}
>
<Typography.Text>Related</Typography.Text>
</Button>
</Button.Group> */}
<div className="explore-content">
{selectedTab === ExplorerTabs.TIME_SERIES && (
<TimeSeries
showOneChartPerQuery={showOneChartPerQuery}
setWarning={setWarning}
areAllMetricUnitsSame={areAllMetricUnitsSame}
isMetricUnitsLoading={isMetricUnitsLoading}
isMetricUnitsError={isMetricUnitsError}
metricUnits={units}
metricNames={metricNames}
metrics={metrics}
handleOpenMetricDetails={handleOpenMetricDetails}
yAxisUnit={yAxisUnit}
setYAxisUnit={setYAxisUnit}
showYAxisUnitSelector={showYAxisUnitSelector}
/>
)}
{/* TODO: Enable once we have resolved all related metrics issues */}
{/* {selectedTab === ExplorerTabs.RELATED_METRICS && (
<RelatedMetrics metricNames={metricNames} />
)} */}
<TimeSeries
showOneChartPerQuery={showOneChartPerQuery}
setWarning={setWarning}
areAllMetricUnitsSame={areAllMetricUnitsSame}
isMetricUnitsLoading={isMetricUnitsLoading}
isMetricUnitsError={isMetricUnitsError}
metricUnits={units}
metricNames={metricNames}
metrics={metrics}
handleOpenMetricDetails={handleOpenMetricDetails}
yAxisUnit={yAxisUnit}
setYAxisUnit={setYAxisUnit}
showYAxisUnitSelector={showYAxisUnitSelector}
/>
</div>
</div>
<ExplorerOptionWrapper

View File

@@ -1,153 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Color } from '@signozhq/design-tokens';
import { Card, Col, Empty, Input, Row, Select, Skeleton } from 'antd';
import { Gauge } from 'lucide-react';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import RelatedMetricsCard from './RelatedMetricsCard';
import { RelatedMetricsProps, RelatedMetricWithQueryResult } from './types';
import { useGetRelatedMetricsGraphs } from './useGetRelatedMetricsGraphs';
function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [selectedMetricName, setSelectedMetricName] = useState<string | null>(
null,
);
const [selectedRelatedMetric, setSelectedRelatedMetric] = useState('all');
const [searchValue, setSearchValue] = useState<string | null>(null);
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
]);
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
maxTime,
]);
useEffect(() => {
if (metricNames.length) {
setSelectedMetricName(metricNames[0]);
}
}, [metricNames]);
const {
relatedMetrics,
isRelatedMetricsLoading,
isRelatedMetricsError,
} = useGetRelatedMetricsGraphs({
selectedMetricName,
startMs,
endMs,
});
const metricNamesSelectOptions = useMemo(
() =>
metricNames.map((name) => ({
value: name,
label: name,
})),
[metricNames],
);
const relatedMetricsSelectOptions = useMemo(() => {
const options: { value: string; label: string }[] = [
{
value: 'all',
label: 'All',
},
];
relatedMetrics.forEach((metric) => {
options.push({
value: metric.name,
label: metric.name,
});
});
return options;
}, [relatedMetrics]);
const filteredRelatedMetrics = useMemo(() => {
let filteredMetrics: RelatedMetricWithQueryResult[] = [];
if (selectedRelatedMetric === 'all') {
filteredMetrics = [...relatedMetrics];
} else {
filteredMetrics = relatedMetrics.filter(
(metric) => metric.name === selectedRelatedMetric,
);
}
if (searchValue?.length) {
filteredMetrics = filteredMetrics.filter((metric) =>
metric.name.toLowerCase().includes(searchValue?.toLowerCase() ?? ''),
);
}
return filteredMetrics;
}, [relatedMetrics, selectedRelatedMetric, searchValue]);
return (
<div className="related-metrics-container">
<div className="related-metrics-header">
<Select
className="metric-name-select"
value={selectedMetricName}
options={metricNamesSelectOptions}
onChange={(value): void => setSelectedMetricName(value)}
suffixIcon={<Gauge size={12} color={Color.BG_SAKURA_500} />}
/>
<Input
className="related-metrics-input"
placeholder="Search..."
onChange={(e): void => setSearchValue(e.target.value)}
bordered
addonBefore={
<Select
loading={isRelatedMetricsLoading}
value={selectedRelatedMetric}
className="related-metrics-select"
options={relatedMetricsSelectOptions}
onChange={(value): void => setSelectedRelatedMetric(value)}
bordered={false}
/>
}
/>
</div>
<div className="related-metrics-body">
{isRelatedMetricsLoading && <Skeleton active />}
{isRelatedMetricsError && (
<Empty description="Error fetching related metrics" />
)}
{!isRelatedMetricsLoading &&
!isRelatedMetricsError &&
filteredRelatedMetrics.length === 0 && (
<Empty description="No related metrics found" />
)}
{!isRelatedMetricsLoading &&
!isRelatedMetricsError &&
filteredRelatedMetrics.length > 0 && (
<Row gutter={24}>
{filteredRelatedMetrics.map((relatedMetricWithQueryResult) => (
<Col span={12} key={relatedMetricWithQueryResult.name}>
<Card
bordered
ref={graphRef}
className="related-metrics-card-container"
>
<RelatedMetricsCard
key={relatedMetricWithQueryResult.name}
metric={relatedMetricWithQueryResult}
/>
</Card>
</Col>
))}
</Row>
)}
</div>
</div>
);
}
export default RelatedMetrics;

View File

@@ -1,47 +0,0 @@
import { Empty, Skeleton, Typography } from 'antd';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { DataSource } from 'types/common/queryBuilder';
import DashboardsAndAlertsPopover from '../MetricDetails/DashboardsAndAlertsPopover';
import { RelatedMetricsCardProps } from './types';
function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element {
const { queryResult } = metric;
if (queryResult.isLoading) {
return <Skeleton />;
}
if (queryResult.error) {
const errorMessage =
(queryResult.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
return (
<div className="related-metrics-card">
<Typography.Text className="related-metrics-card-name">
{metric.name}
</Typography.Text>
{queryResult.isLoading ? <Skeleton /> : null}
{queryResult.isError ? (
<div className="related-metrics-card-error">
<Empty description="Error fetching metric data" />
</div>
) : null}
{!queryResult.isLoading && !queryResult.error && (
<TimeSeriesView
isFilterApplied={false}
isError={queryResult.isError}
isLoading={queryResult.isLoading}
data={queryResult.data}
yAxisUnit="ms"
dataSource={DataSource.METRICS}
/>
)}
<DashboardsAndAlertsPopover metricName={metric.name} />
</div>
);
}
export default RelatedMetricsCard;

View File

@@ -1,14 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
RELATED_METRICS = 'related-metrics',
}
import { Warning } from 'types/api';
export interface TimeSeriesProps {
showOneChartPerQuery: boolean;
@@ -24,27 +16,3 @@ export interface TimeSeriesProps {
setYAxisUnit: (unit: string) => void;
showYAxisUnitSelector: boolean;
}
export interface RelatedMetricsProps {
metricNames: string[];
}
export interface RelatedMetricsCardProps {
metric: RelatedMetricWithQueryResult;
}
export interface UseGetRelatedMetricsGraphsProps {
selectedMetricName: string | null;
startMs: number;
endMs: number;
}
export interface UseGetRelatedMetricsGraphsReturn {
relatedMetrics: RelatedMetricWithQueryResult[];
isRelatedMetricsLoading: boolean;
isRelatedMetricsError: boolean;
}
export interface RelatedMetricWithQueryResult extends RelatedMetric {
queryResult: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>;
}

View File

@@ -1,113 +0,0 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useGetRelatedMetrics } from 'hooks/metricsExplorer/useGetRelatedMetrics';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import { convertNanoToMilliseconds } from '../Summary/utils';
import {
UseGetRelatedMetricsGraphsProps,
UseGetRelatedMetricsGraphsReturn,
} from './types';
export const useGetRelatedMetricsGraphs = ({
selectedMetricName,
startMs,
endMs,
}: UseGetRelatedMetricsGraphsProps): UseGetRelatedMetricsGraphsReturn => {
const { maxTime, minTime, selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
// Build the query for the related metrics
const relatedMetricsQuery = useMemo(
() => ({
start: convertNanoToMilliseconds(minTime),
end: convertNanoToMilliseconds(maxTime),
currentMetricName: selectedMetricName ?? '',
}),
[selectedMetricName, minTime, maxTime],
);
// Get the related metrics
const {
data: relatedMetricsData,
isLoading: isRelatedMetricsLoading,
isError: isRelatedMetricsError,
} = useGetRelatedMetrics(relatedMetricsQuery, {
enabled: !!selectedMetricName,
});
// Build the related metrics array
const relatedMetrics = useMemo(() => {
if (relatedMetricsData?.payload?.data?.related_metrics) {
return relatedMetricsData.payload.data.related_metrics;
}
return [];
}, [relatedMetricsData]);
// Build the query results for the related metrics
const relatedMetricsQueryResults = useQueries(
useMemo(
() =>
relatedMetrics.map((metric) => ({
queryKey: ['related-metrics', metric.name],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(
{
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: {
queryData: [metric.query],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: uuidv4(),
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
start: startMs,
end: endMs,
formatForWeb: false,
params: {
dataSource: DataSource.METRICS,
},
},
ENTITY_VERSION_V4,
),
enabled: !!metric.query,
})),
[relatedMetrics, globalSelectedTime, startMs, endMs],
),
);
// Build the related metrics with query results
const relatedMetricsWithQueryResults = useMemo(
() =>
relatedMetrics.map((metric, index) => ({
...metric,
queryResult: relatedMetricsQueryResults[index],
})),
[relatedMetrics, relatedMetricsQueryResults],
);
return {
relatedMetrics: relatedMetricsWithQueryResults,
isRelatedMetricsLoading,
isRelatedMetricsError,
};
};

View File

@@ -4,7 +4,6 @@ import { Color } from '@signozhq/design-tokens';
import type { TableColumnsType as ColumnsType } from 'antd';
import { Card, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import classNames from 'classnames';
import ResizeTable from 'components/ResizeTable/ResizeTable';
import { DataType } from 'container/LogDetailedView/TableView';
@@ -15,6 +14,7 @@ import {
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
TIME_AGGREGATION_OPTIONS,
} from './constants';
import { InspectMetricsSeries } from './types';
import {
ExpandedViewProps,
InspectionStep,
@@ -42,7 +42,8 @@ function ExpandedView({
useEffect(() => {
logEvent(MetricsExplorerEvents.InspectPointClicked, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionAppliedOptions.filters,
[MetricsExplorerEventKeys.Filters]:
metricInspectionAppliedOptions.filterExpression,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionAppliedOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:

View File

@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useGetMetricMetadata } from 'api/generated/services/metrics';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -21,6 +21,7 @@ import {
GraphPopoverOptions,
InspectProps,
MetricInspectionAction,
MetricType,
} from './types';
import { useInspectMetrics } from './useInspectMetrics';
@@ -48,10 +49,12 @@ function Inspect({
] = useState<GraphPopoverOptions | null>(null);
const [showExpandedView, setShowExpandedView] = useState(false);
const { data: metricDetailsData } = useGetMetricDetails(
appliedMetricName ?? '',
const { data: metricDetailsData } = useGetMetricMetadata(
{ metricName: appliedMetricName ?? '' },
{
enabled: !!appliedMetricName,
query: {
enabled: !!appliedMetricName,
},
},
);
@@ -119,14 +122,13 @@ function Inspect({
);
const selectedMetricType = useMemo(
() => metricDetailsData?.payload?.data?.metadata?.metric_type,
() => (metricDetailsData?.data?.type as unknown) as MetricType,
[metricDetailsData],
);
const selectedMetricUnit = useMemo(
() => metricDetailsData?.payload?.data?.metadata?.unit,
[metricDetailsData],
);
const selectedMetricUnit = useMemo(() => metricDetailsData?.data?.unit, [
metricDetailsData,
]);
const aggregateAttribute = useMemo(
() => ({

View File

@@ -33,7 +33,7 @@ function MetricFilters({
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: tagFilter,
payload: expression,
});
},
[currentQuery, dispatchMetricInspectionOptions, setCurrentQuery],

View File

@@ -1,8 +1,8 @@
import { useCallback, useMemo } from 'react';
import type { TableColumnsType as ColumnsType } from 'antd';
import { Card, Flex, Table, Typography } from 'antd';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { InspectMetricsSeries } from './types';
import { TableViewProps } from './types';
import { formatTimestampToFullDateTime } from './utils';

View File

@@ -1,11 +1,11 @@
import { render, screen } from '@testing-library/react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import {
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
TIME_AGGREGATION_OPTIONS,
} from '../constants';
import ExpandedView from '../ExpandedView';
import { InspectMetricsSeries } from '../types';
import {
GraphPopoverData,
InspectionStep,
@@ -25,7 +25,6 @@ describe('ExpandedView', () => {
labels: {
host_id: 'test-id',
},
labelsArray: [],
title: 'TS1',
};
@@ -66,10 +65,7 @@ describe('ExpandedView', () => {
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],
filters: {
items: [],
op: 'AND',
},
filterExpression: '',
};
it('renders entire time series for a raw data inspection', () => {

View File

@@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import GraphPopover from '../GraphPopover';
import { InspectMetricsSeries } from '../types';
import { GraphPopoverOptions, InspectionStep } from '../types';
describe('GraphPopover', () => {
@@ -16,7 +16,6 @@ describe('GraphPopover', () => {
{ timestamp: 1672531260000, value: '43.456' },
],
labels: {},
labelsArray: [],
},
};
const mockSpaceAggregationSeriesMap: Map<

View File

@@ -2,12 +2,11 @@
import { Provider } from 'react-redux';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import store from 'store';
import { AlignedData } from 'uplot';
import GraphView from '../GraphView';
import { InspectMetricsSeries, MetricType } from '../types';
import {
InspectionStep,
SpaceAggregationOptions,
@@ -32,7 +31,6 @@ describe('GraphView', () => {
{ timestamp: 1234567891000, value: '20' },
],
labels: { label1: 'value1' },
labelsArray: [{ label: 'label1', value: 'value1' }],
},
];
@@ -58,10 +56,7 @@ describe('GraphView', () => {
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],
timeAggregationOption: TimeAggregationOptions.MAX,
filters: {
items: [],
op: 'AND',
},
filterExpression: '',
},
isInspectMetricsRefetching: false,
};

View File

@@ -2,17 +2,21 @@ import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { render, screen } from '@testing-library/react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as useInspectMetricsHooks from 'hooks/metricsExplorer/useGetInspectMetricsDetails';
import * as useGetMetricDetailsHooks from 'hooks/metricsExplorer/useGetMetricDetails';
import * as metricsGeneratedAPI from 'api/generated/services/metrics';
import * as appContextHooks from 'providers/App/App';
import store from 'store';
import ROUTES from '../../../../constants/routes';
import { LicenseEvent } from '../../../../types/api/licensesV3/getActive';
import { INITIAL_INSPECT_METRICS_OPTIONS } from '../constants';
import Inspect from '../Inspect';
import { InspectionStep } from '../types';
import {
InspectionStep,
InspectMetricsSeries,
MetricType,
UseInspectMetricsReturnData,
} from '../types';
import * as useInspectMetricsModule from '../useInspectMetrics';
const queryClient = new QueryClient();
const mockTimeSeries: InspectMetricsSeries[] = [
@@ -24,7 +28,6 @@ const mockTimeSeries: InspectMetricsSeries[] = [
{ timestamp: 1234567891000, value: '20' },
],
labels: { label1: 'value1' },
labelsArray: [{ label: 'label1', value: 'value1' }],
},
];
@@ -52,29 +55,19 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
},
} as any);
jest.spyOn(useGetMetricDetailsHooks, 'useGetMetricDetails').mockReturnValue({
jest.spyOn(metricsGeneratedAPI, 'useGetMetricMetadata').mockReturnValue({
data: {
metricDetails: {
metricName: 'test_metric',
metricType: MetricType.GAUGE,
data: {
type: MetricType.GAUGE,
unit: '',
description: '',
temporality: '',
isMonotonic: false,
},
status: 'success',
},
} as any);
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: mockTimeSeries,
},
status: 'success',
},
},
isLoading: false,
} as any);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
@@ -90,16 +83,26 @@ mockResizeObserver.mockImplementation(() => ({
}));
window.ResizeObserver = mockResizeObserver;
const baseHookReturn: UseInspectMetricsReturnData = {
inspectMetricsTimeSeries: [],
inspectMetricsStatusCode: 200,
isInspectMetricsLoading: false,
isInspectMetricsError: false,
formattedInspectMetricsTimeSeries: [[], []],
spaceAggregationLabels: [],
metricInspectionOptions: INITIAL_INSPECT_METRICS_OPTIONS,
dispatchMetricInspectionOptions: jest.fn(),
inspectionStep: InspectionStep.COMPLETED,
isInspectMetricsRefetching: false,
spaceAggregatedSeriesMap: new Map(),
aggregatedTimeSeries: [],
timeAggregatedSeriesMap: new Map(),
reset: jest.fn(),
};
describe('Inspect', () => {
const defaultProps = {
inspectMetricsTimeSeries: mockTimeSeries,
formattedInspectMetricsTimeSeries: [],
metricUnit: '',
metricName: 'test_metric',
metricType: MetricType.GAUGE,
spaceAggregationSeriesMap: new Map(),
inspectionStep: InspectionStep.COMPLETED,
resetInspection: jest.fn(),
isOpen: true,
onClose: jest.fn(),
};
@@ -109,6 +112,12 @@ describe('Inspect', () => {
});
it('renders all components', () => {
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
inspectMetricsTimeSeries: mockTimeSeries,
aggregatedTimeSeries: mockTimeSeries,
});
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -123,18 +132,11 @@ describe('Inspect', () => {
});
it('renders loading state', () => {
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: [],
},
},
},
isLoading: true,
} as any);
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
isInspectMetricsLoading: true,
});
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -147,18 +149,11 @@ describe('Inspect', () => {
});
it('renders empty state', () => {
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: [],
},
},
},
isLoading: false,
} as any);
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
inspectMetricsTimeSeries: [],
});
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -171,19 +166,11 @@ describe('Inspect', () => {
});
it('renders error state', () => {
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: [],
},
},
},
isLoading: false,
isError: true,
} as any);
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
isInspectMetricsError: true,
});
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -196,14 +183,11 @@ describe('Inspect', () => {
});
it('renders error state with 400 status code', () => {
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
statusCode: 400,
},
isError: false,
} as any);
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
inspectMetricsStatusCode: 400,
});
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>

View File

@@ -4,13 +4,13 @@ import { Provider } from 'react-redux';
import { fireEvent, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as metricsService from 'api/generated/services/metrics';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as appContextHooks from 'providers/App/App';
import store from 'store';
import ROUTES from '../../../../constants/routes';
import { LicenseEvent } from '../../../../types/api/licensesV3/getActive';
import QueryBuilder from '../QueryBuilder';
import { MetricType } from '../types';
import {
InspectionStep,
SpaceAggregationOptions,
@@ -89,10 +89,7 @@ describe('QueryBuilder', () => {
timeAggregationOption: TimeAggregationOptions.AVG,
spaceAggregationLabels: [],
spaceAggregationOption: SpaceAggregationOptions.AVG_BY,
filters: {
items: [],
op: 'and',
},
filterExpression: '',
},
dispatchMetricInspectionOptions: jest.fn(),
metricType: MetricType.SUM,
@@ -103,6 +100,7 @@ describe('QueryBuilder', () => {
items: [],
op: 'and',
},
filterExpression: '',
} as any,
setCurrentQuery: jest.fn(),
};

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import TableView from '../TableView';
import { InspectMetricsSeries } from '../types';
import {
InspectionStep,
SpaceAggregationOptions,
@@ -19,12 +19,6 @@ describe('TableView', () => {
{ timestamp: 1234567891000, value: '20' },
],
labels: { label1: 'value1' },
labelsArray: [
{
label: 'label1',
value: 'value1',
},
],
},
{
strokeColor: '#fff',
@@ -34,12 +28,6 @@ describe('TableView', () => {
{ timestamp: 1234567891000, value: '40' },
],
labels: { label2: 'value2' },
labelsArray: [
{
label: 'label2',
value: 'value2',
},
],
},
];
@@ -53,10 +41,7 @@ describe('TableView', () => {
timeAggregationOption: TimeAggregationOptions.MAX,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],
filters: {
items: [],
op: 'AND',
},
filterExpression: '',
},
isInspectMetricsRefetching: false,
};

View File

@@ -1,6 +1,5 @@
import { ForwardRefExoticComponent, RefAttributes } from 'react';
import { Color } from '@signozhq/design-tokens';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
BarChart,
BarChart2,
@@ -10,6 +9,7 @@ import {
LucideProps,
} from 'lucide-react';
import { MetricType } from './types';
import {
MetricInspectionState,
SpaceAggregationOptions,
@@ -77,20 +77,14 @@ export const INITIAL_INSPECT_METRICS_OPTIONS: MetricInspectionState = {
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
filterExpression: '',
},
appliedOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
filterExpression: '',
},
};

View File

@@ -1,11 +1,26 @@
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { AlignedData } from 'uplot';
export enum MetricType {
SUM = 'sum',
GAUGE = 'gauge',
HISTOGRAM = 'histogram',
SUMMARY = 'summary',
EXPONENTIAL_HISTOGRAM = 'exponentialhistogram',
}
export interface InspectMetricsTimestampValue {
timestamp: number;
value: string;
}
export interface InspectMetricsSeries {
title?: string;
strokeColor?: string;
labels: Record<string, string>;
values: InspectMetricsTimestampValue[];
}
export type InspectProps = {
metricName: string;
isOpen: boolean;
@@ -106,7 +121,7 @@ export interface MetricInspectionOptions {
timeAggregationInterval: number | undefined;
spaceAggregationOption: SpaceAggregationOptions | undefined;
spaceAggregationLabels: string[];
filters: TagFilter;
filterExpression: string;
}
export interface MetricInspectionState {
@@ -119,7 +134,7 @@ export type MetricInspectionAction =
| { type: 'SET_TIME_AGGREGATION_INTERVAL'; payload: number }
| { type: 'SET_SPACE_AGGREGATION_OPTION'; payload: SpaceAggregationOptions }
| { type: 'SET_SPACE_AGGREGATION_LABELS'; payload: string[] }
| { type: 'SET_FILTERS'; payload: TagFilter }
| { type: 'SET_FILTERS'; payload: string }
| { type: 'RESET_INSPECTION' }
| { type: 'APPLY_METRIC_INSPECTION_OPTIONS' };

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { useQuery } from 'react-query';
import { inspectMetrics } from 'api/generated/services/metrics';
import { themeColors } from 'constants/theme';
import { useGetInspectMetricsDetails } from 'hooks/metricsExplorer/useGetInspectMetricsDetails';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
@@ -9,6 +9,7 @@ import { INITIAL_INSPECT_METRICS_OPTIONS } from './constants';
import {
GraphPopoverData,
InspectionStep,
InspectMetricsSeries,
MetricInspectionAction,
MetricInspectionState,
UseInspectMetricsReturnData,
@@ -61,7 +62,7 @@ const metricInspectionReducer = (
...state,
currentOptions: {
...state.currentOptions,
filters: action.payload,
filterExpression: action.payload,
},
};
case 'APPLY_METRIC_INSPECTION_OPTIONS':
@@ -100,26 +101,58 @@ export function useInspectMetrics(
);
const {
data: inspectMetricsData,
data: inspectMetricsResponse,
isLoading: isInspectMetricsLoading,
isError: isInspectMetricsError,
isRefetching: isInspectMetricsRefetching,
} = useGetInspectMetricsDetails(
{
metricName: metricName ?? '',
} = useQuery({
queryKey: [
'inspectMetrics',
metricName,
start,
end,
filters: metricInspectionOptions.appliedOptions.filters,
},
{
enabled: !!metricName,
keepPreviousData: true,
},
metricInspectionOptions.appliedOptions.filterExpression,
],
queryFn: ({ signal }) =>
inspectMetrics(
{
metricName: metricName ?? '',
start,
end,
filter: metricInspectionOptions.appliedOptions.filterExpression
? { expression: metricInspectionOptions.appliedOptions.filterExpression }
: undefined,
},
signal,
),
enabled: !!metricName,
keepPreviousData: true,
});
const inspectMetricsData = useMemo(
() => ({
series: (inspectMetricsResponse?.data?.series ?? []).map((s) => {
const labels: Record<string, string> = {};
for (const l of s.labels ?? []) {
if (l.key?.name) {
labels[l.key.name] = String(l.value ?? '');
}
}
return {
labels,
values: (s.values ?? []).map((v) => ({
timestamp: v.timestamp ?? 0,
value: String(v.value ?? 0),
})),
};
}) as InspectMetricsSeries[],
}),
[inspectMetricsResponse],
);
const isDarkMode = useIsDarkMode();
const inspectMetricsTimeSeries = useMemo(() => {
const series = inspectMetricsData?.payload?.data?.series ?? [];
const series = inspectMetricsData?.series ?? [];
return series.map((series, index) => {
const title = `TS${index + 1}`;
@@ -136,10 +169,7 @@ export function useInspectMetrics(
});
}, [inspectMetricsData, isDarkMode]);
const inspectMetricsStatusCode = useMemo(
() => inspectMetricsData?.statusCode || 200,
[inspectMetricsData],
);
const inspectMetricsStatusCode = 200;
// Evaluate inspection step
const currentInspectionStep = useMemo(() => {
@@ -231,7 +261,7 @@ export function useInspectMetrics(
const spaceAggregationLabels = useMemo(() => {
const labels = new Set<string>();
inspectMetricsData?.payload?.data.series.forEach((series) => {
inspectMetricsData?.series?.forEach((series) => {
Object.keys(series.labels).forEach((label) => {
labels.add(label);
});

View File

@@ -1,7 +1,7 @@
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { InspectMetricsSeries } from './types';
import {
GraphPopoverData,
GraphPopoverOptions,

View File

@@ -5,7 +5,6 @@ import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import {
UniversalYAxisUnit,
YAxisUnitSelectorProps,
@@ -180,7 +179,10 @@ describe('Metadata', () => {
const temporalitySelect = screen.getByTestId('temporality-select');
expect(temporalitySelect).toBeInTheDocument();
await userEvent.selectOptions(temporalitySelect, Temporality.CUMULATIVE);
await userEvent.selectOptions(
temporalitySelect,
MetrictypesTemporalityDTO.cumulative,
);
const unitSelect = screen.getByTestId('unit-select');
expect(unitSelect).toBeInTheDocument();

View File

@@ -9,16 +9,14 @@ import {
Popover,
Spin,
} from 'antd';
import { useListMetrics } from 'api/generated/services/metrics';
import { Filter } from 'api/v5/v5';
import {
convertExpressionToFilters,
convertFiltersToExpression,
} from 'components/QueryBuilderV2/utils';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Search } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
function MetricNameSearch({
queryFilterExpression,
@@ -44,25 +42,20 @@ function MetricNameSearch({
}, [isPopoverOpen]);
const {
data: metricNameFilterValuesData,
data: metricNameListData,
isLoading: isLoadingMetricNameFilterValues,
isError: isErrorMetricNameFilterValues,
} = useGetMetricsListFilterValues(
} = useListMetrics(
{
searchText: debouncedSearchString,
filterKey: 'metric_name',
filterAttributeKeyDataType: DataTypes.String,
limit: 10,
},
{
enabled: isPopoverOpen,
refetchOnWindowFocus: false,
queryKey: [
REACT_QUERY_KEY.GET_METRICS_LIST_FILTER_VALUES,
'metric_name',
debouncedSearchString,
isPopoverOpen,
],
query: {
enabled: isPopoverOpen,
refetchOnWindowFocus: false,
queryKey: ['listMetricsForSearch', debouncedSearchString, isPopoverOpen],
},
},
);
@@ -95,8 +88,8 @@ function MetricNameSearch({
);
const metricNameFilterValues = useMemo(
() => metricNameFilterValuesData?.payload?.data?.filterValues || [],
[metricNameFilterValuesData],
() => metricNameListData?.data?.metrics?.map((m) => m.metricName) || [],
[metricNameListData],
);
const handleKeyDown = useCallback(

View File

@@ -2,8 +2,8 @@
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import * as metricsGeneratedAPI from 'api/generated/services/metrics';
import { Filter } from 'api/v5/v5';
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
import store from 'store';
import APIError from 'types/api/error';
@@ -53,21 +53,33 @@ describe('MetricsTable', () => {
} as any);
});
jest
.spyOn(useGetMetricsListFilterValues, 'useGetMetricsListFilterValues')
.mockReturnValue({
jest.spyOn(metricsGeneratedAPI, 'useListMetrics').mockReturnValue({
data: {
data: {
statusCode: 200,
payload: {
status: 'success',
data: {
filterValues: ['metric1', 'metric2'],
metrics: [
{
metricName: 'metric1',
description: '',
type: '',
unit: '',
temporality: '',
isMonotonic: false,
},
},
{
metricName: 'metric2',
description: '',
type: '',
unit: '',
temporality: '',
isMonotonic: false,
},
],
},
isLoading: false,
isError: false,
} as any);
status: 'success',
},
isLoading: false,
isError: false,
} as any);
it('renders table with data correctly', () => {
render(

View File

@@ -6,7 +6,6 @@ import {
MetricsexplorertypesTreemapEntryDTO,
MetricsexplorertypesTreemapModeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { MetricsListPayload } from 'api/metricsExplorer/getMetricsList';
import { Filter } from 'api/v5/v5';
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
@@ -76,14 +75,6 @@ export const getMetricsTableColumns = (
},
];
export const getMetricsListQuery = (): MetricsListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'metric_name', order: 'asc' },
});
function ValidateRowValueWrapper({
value,
children,

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { MetricType } from 'container/MetricsExplorer/Inspect/types';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { ReduceOperators } from 'types/common/queryBuilder';

View File

@@ -1,55 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getInspectMetricsDetails,
InspectMetricsRequest,
InspectMetricsResponse,
} from 'api/metricsExplorer/getInspectMetricsDetails';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetInspectMetricsDetails = (
requestData: InspectMetricsRequest,
options?: UseQueryOptions<
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
Error
>;
export const useGetInspectMetricsDetails: UseGetInspectMetricsDetails = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [
REACT_QUERY_KEY.GET_INSPECT_METRICS_DETAILS,
requestData.metricName,
requestData.start,
requestData.end,
requestData.filters,
];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) =>
getInspectMetricsDetails(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,46 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricDetails,
MetricDetailsResponse,
} from 'api/metricsExplorer/getMetricDetails';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricDetails = (
metricName: string,
options?: UseQueryOptions<
SuccessResponse<MetricDetailsResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricDetailsResponse> | ErrorResponse,
Error
>;
export const useGetMetricDetails: UseGetMetricDetails = (
metricName,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_METRIC_DETAILS, metricName];
}, [options?.queryKey, metricName]);
return useQuery<SuccessResponse<MetricDetailsResponse> | ErrorResponse, Error>(
{
queryFn: ({ signal }) => getMetricDetails(metricName, signal, headers),
...options,
queryKey,
},
);
};

View File

@@ -1,47 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsList,
MetricsListPayload,
MetricsListResponse,
} from 'api/metricsExplorer/getMetricsList';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsList = (
requestData: MetricsListPayload,
options?: UseQueryOptions<
SuccessResponse<MetricsListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsListResponse> | ErrorResponse,
Error
>;
export const useGetMetricsList: UseGetMetricsList = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_METRICS_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<MetricsListResponse> | ErrorResponse, Error>({
queryFn: ({ signal }) => getMetricsList(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,48 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsListFilterKeys,
GetMetricsListFilterKeysParams,
MetricsListFilterKeysResponse,
} from 'api/metricsExplorer/getMetricsListFilterKeys';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsListFilterKeys = (
params: GetMetricsListFilterKeysParams,
options?: UseQueryOptions<
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
Error
>;
export const useGetMetricsListFilterKeys: UseGetMetricsListFilterKeys = (
params,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_METRICS_LIST_FILTER_KEYS];
}, [options?.queryKey]);
return useQuery<
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getMetricsListFilterKeys(params, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,42 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsListFilterValues,
MetricsListFilterValuesPayload,
MetricsListFilterValuesResponse,
} from 'api/metricsExplorer/getMetricsListFilterValues';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsListFilterValues = (
payload: MetricsListFilterValuesPayload,
options?: UseQueryOptions<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
Error
>;
export const useGetMetricsListFilterValues: UseGetMetricsListFilterValues = (
props,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
return [props];
}, [options?.queryKey, props]);
return useQuery<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getMetricsListFilterValues(props, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,48 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getRelatedMetrics,
RelatedMetricsPayload,
RelatedMetricsResponse,
} from 'api/metricsExplorer/getRelatedMetrics';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetRelatedMetrics = (
requestData: RelatedMetricsPayload,
options?: UseQueryOptions<
SuccessResponse<RelatedMetricsResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<RelatedMetricsResponse> | ErrorResponse,
Error
>;
export const useGetRelatedMetrics: UseGetRelatedMetrics = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_RELATED_METRICS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<RelatedMetricsResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getRelatedMetrics(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,7 +1,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDebounce } from 'react-use';
import { getMetricsListFilterValues } from 'api/metricsExplorer/getMetricsListFilterValues';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
@@ -14,7 +13,6 @@ import {
getTagToken,
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetMetricsListFilterKeys } from 'hooks/metricsExplorer/useGetMetricsListFilterKeys';
import useDebounceValue from 'hooks/useDebounce';
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -47,6 +45,7 @@ type IuseFetchKeysAndValues = {
* @returns an object containing the fetched attribute keys, results, and the status of the fetch
*/
// eslint-disable-next-line max-params
export const useFetchKeysAndValues = (
searchValue: string,
query: IBuilderQuery,
@@ -56,6 +55,7 @@ export const useFetchKeysAndValues = (
isInfraMonitoring?: boolean,
entity?: K8sCategory | null,
isMetricsExplorer?: boolean,
// eslint-disable-next-line max-params
): IuseFetchKeysAndValues => {
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
@@ -152,21 +152,8 @@ export const useFetchKeysAndValues = (
},
);
const {
data: metricsListFilterKeysData,
isFetching: isFetchingMetricsListFilterKeys,
status: fetchingMetricsListFilterKeysStatus,
} = useGetMetricsListFilterKeys(
{
searchText: searchKey,
},
{
enabled: isMetricsExplorer && isQueryEnabled && !shouldUseSuggestions,
queryKey: [searchKey],
},
);
function isAttributeValuesResponse(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any,
): payload is IAttributeValuesResponse {
return (
@@ -180,14 +167,6 @@ export const useFetchKeysAndValues = (
);
}
function isMetricsListFilterValuesData(
payload: any,
): payload is { filterValues: string[] } {
return (
payload && 'filterValues' in payload && Array.isArray(payload.filterValues)
);
}
/**
* Fetches the options to be displayed based on the selected value
* @param value - the selected value
@@ -231,15 +210,6 @@ export const useFetchKeysAndValues = (
: tagValue?.toString() ?? '',
});
payload = response.payload;
} else if (isMetricsExplorer) {
const response = await getMetricsListFilterValues({
searchText: searchKey,
filterKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType:
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
limit: 10,
});
payload = response.payload?.data;
} else {
const response = await getAttributesValues({
aggregateOperator: query.aggregateOperator || '',
@@ -256,18 +226,11 @@ export const useFetchKeysAndValues = (
payload = response.payload;
}
if (payload) {
if (isAttributeValuesResponse(payload)) {
const dataType = filterAttributeKey?.dataType ?? DataTypes.String;
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
setResults(key ? payload[key] || [] : []);
return;
}
if (isMetricsExplorer && isMetricsListFilterValuesData(payload)) {
setResults(payload.filterValues || []);
return;
}
if (payload && isAttributeValuesResponse(payload)) {
const dataType = filterAttributeKey?.dataType ?? DataTypes.String;
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
setResults(key ? payload[key] || [] : []);
return;
}
} catch (e) {
console.error(e);
@@ -305,32 +268,6 @@ export const useFetchKeysAndValues = (
}
}, [data?.payload?.attributeKeys, status]);
useEffect(() => {
if (
isMetricsExplorer &&
fetchingMetricsListFilterKeysStatus === 'success' &&
!isFetchingMetricsListFilterKeys &&
metricsListFilterKeysData?.payload?.data?.attributeKeys
) {
setKeys(metricsListFilterKeysData.payload.data.attributeKeys);
setSourceKeys((prevState) =>
uniqWith(
[
...(metricsListFilterKeysData.payload.data.attributeKeys ?? []),
...prevState,
],
isEqual,
),
);
}
}, [
metricsListFilterKeysData?.payload?.data?.attributeKeys,
fetchingMetricsListFilterKeysStatus,
isMetricsExplorer,
metricsListFilterKeysData,
isFetchingMetricsListFilterKeys,
]);
useEffect(() => {
if (
fetchingSuggestionsStatus === 'success' &&

View File

@@ -1,15 +0,0 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
export interface MetricMetadata {
description: string;
type: MetricType;
unit: string;
temporality: Temporality;
isMonotonic: boolean;
}
export interface MetricMetadataResponse {
status: string;
data: MetricMetadata;
}

View File

@@ -183,5 +183,43 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/inspect", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.InspectMetrics),
handler.OpenAPIDef{
ID: "InspectMetrics",
Tags: []string{"metrics"},
Summary: "Inspect raw metric data points",
Description: "Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.",
Request: new(metricsexplorertypes.InspectMetricsRequest),
RequestContentType: "application/json",
Response: new(metricsexplorertypes.InspectMetricsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/metrics/onboarding", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetOnboardingStatus),
handler.OpenAPIDef{
ID: "GetMetricsOnboardingStatus",
Tags: []string{"metrics"},
Summary: "Check if non-SigNoz metrics have been received",
Description: "Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection",
Request: nil,
RequestContentType: "",
Response: new(metricsexplorertypes.MetricsOnboardingResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -300,6 +300,41 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
render.Success(rw, http.StatusOK, out)
}
func (h *handler) InspectMetrics(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
var in metricsexplorertypes.InspectMetricsRequest
if err := binding.JSON.BindBody(req.Body, &in); err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
out, err := h.module.InspectMetrics(req.Context(), orgID, &in)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, out)
}
func (h *handler) GetOnboardingStatus(rw http.ResponseWriter, req *http.Request) {
hasMetrics, err := h.module.HasNonSigNozMetrics(req.Context())
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, &metricsexplorertypes.MetricsOnboardingResponse{
HasMetrics: hasMetrics,
})
}
func (h *handler) checkMetricExists(ctx context.Context, orgID valuer.UUID, metricName string) error {
exists, err := h.module.CheckMetricExists(ctx, orgID, metricName)
if err != nil {

View File

@@ -3,6 +3,7 @@ package implmetricsexplorer
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"strings"
@@ -502,6 +503,157 @@ func (m *module) CheckMetricExists(ctx context.Context, orgID valuer.UUID, metri
return exists, nil
}
func (m *module) HasNonSigNozMetrics(ctx context.Context) (bool, error) {
ctx = m.withMetricsExplorerContext(ctx, "HasNonSigNozMetrics")
sb := sqlbuilder.NewSelectBuilder()
sb.Select("count(*) > 0")
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.TimeseriesV41weekTableName))
sb.Where("metric_name NOT LIKE 'signoz_%'")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
db := m.telemetryStore.ClickhouseDB()
var hasMetrics bool
valueCtx := ctxtypes.SetClickhouseMaxThreads(ctx, m.config.TelemetryStore.Threads)
err := db.QueryRow(valueCtx, query, args...).Scan(&hasMetrics)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "failed to check for non-signoz metrics")
}
return hasMetrics, nil
}
func (m *module) InspectMetrics(
ctx context.Context,
orgID valuer.UUID,
req *metricsexplorertypes.InspectMetricsRequest,
) (*metricsexplorertypes.InspectMetricsResponse, error) {
ctx = m.withMetricsExplorerContext(ctx, "InspectMetrics")
if err := req.Validate(); err != nil {
return nil, err
}
start := uint64(req.Start)
end := uint64(req.End)
filterWhereClause, err := m.buildFilterClause(ctx, req.Filter, req.Start, req.End)
if err != nil {
return nil, err
}
tsStart, _, tsTable, _ := telemetrymetrics.WhichTSTableToUse(start, end, nil)
tsSb := sqlbuilder.NewSelectBuilder()
tsSb.Select("fingerprint", "labels")
tsSb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, tsTable))
tsSb.Where(
tsSb.E("metric_name", req.MetricName),
tsSb.GE("unix_milli", tsStart),
tsSb.LE("unix_milli", end),
)
if filterWhereClause != nil {
tsSb.AddWhereClause(sqlbuilder.CopyWhereClause(filterWhereClause))
}
tsSb.GroupBy("fingerprint", "labels")
tsSb.Limit(50)
tsQuery, tsArgs := tsSb.BuildWithFlavor(sqlbuilder.ClickHouse)
db := m.telemetryStore.ClickhouseDB()
valueCtx := ctxtypes.SetClickhouseMaxThreads(ctx, m.config.TelemetryStore.Threads)
tsRows, err := db.Query(valueCtx, tsQuery, tsArgs...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to query time series for inspect")
}
defer tsRows.Close()
type tsInfo struct {
fingerprint uint64
labels map[string]string
}
var tsList []tsInfo
var fingerprints []any
for tsRows.Next() {
var fp uint64
var labelsStr string
if err := tsRows.Scan(&fp, &labelsStr); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan time series row")
}
parsedLabels := make(map[string]string)
if err := json.Unmarshal([]byte(labelsStr), &parsedLabels); err != nil {
parsedLabels = map[string]string{}
}
tsList = append(tsList, tsInfo{fingerprint: fp, labels: parsedLabels})
fingerprints = append(fingerprints, fp)
}
if err := tsRows.Err(); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to iterate time series rows")
}
if len(fingerprints) == 0 {
return &metricsexplorertypes.InspectMetricsResponse{Series: []*qbtypes.TimeSeries{}}, nil
}
samplesSb := sqlbuilder.NewSelectBuilder()
samplesSb.Select("fingerprint", "unix_milli", "value")
samplesSb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.SamplesV4TableName))
samplesSb.Where(
samplesSb.In("fingerprint", fingerprints...),
samplesSb.E("metric_name", req.MetricName),
samplesSb.GE("unix_milli", int64(start)),
samplesSb.LE("unix_milli", int64(end)),
)
samplesSb.OrderBy("fingerprint", "unix_milli")
samplesQuery, samplesArgs := samplesSb.BuildWithFlavor(sqlbuilder.ClickHouse)
samplesRows, err := db.Query(valueCtx, samplesQuery, samplesArgs...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to query samples for inspect")
}
defer samplesRows.Close()
valuesMap := make(map[uint64][]*qbtypes.TimeSeriesValue)
for samplesRows.Next() {
var fp uint64
var unixMilli int64
var value float64
if err := samplesRows.Scan(&fp, &unixMilli, &value); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan samples row")
}
valuesMap[fp] = append(valuesMap[fp], &qbtypes.TimeSeriesValue{
Timestamp: unixMilli,
Value: value,
})
}
if err := samplesRows.Err(); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to iterate samples rows")
}
series := make([]*qbtypes.TimeSeries, 0, len(tsList))
for _, ts := range tsList {
labels := make([]*qbtypes.Label, 0, len(ts.labels))
for k, v := range ts.labels {
labels = append(labels, &qbtypes.Label{
Key: telemetrytypes.TelemetryFieldKey{Name: k},
Value: v,
})
}
values := valuesMap[ts.fingerprint]
if values == nil {
values = []*qbtypes.TimeSeriesValue{}
}
series = append(series, &qbtypes.TimeSeries{
Labels: labels,
Values: values,
})
}
return &metricsexplorertypes.InspectMetricsResponse{Series: series}, nil
}
func (m *module) fetchMetadataFromCache(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]*metricsexplorertypes.MetricMetadata, []string) {
hits := make(map[string]*metricsexplorertypes.MetricMetadata)
misses := make([]string, 0)
@@ -511,7 +663,7 @@ func (m *module) fetchMetadataFromCache(ctx context.Context, orgID valuer.UUID,
if err := m.cache.Get(ctx, orgID, cacheKey, &cachedMetadata); err == nil {
hits[metricName] = &cachedMetadata
} else {
m.logger.WarnContext(ctx, "cache miss for metric metadata", slog.String("metric_name", metricName), errors.Attr(err))
m.logger.WarnContext(ctx, "cache miss for metric metadata", slog.String("metric_name", metricName))
misses = append(misses, metricName)
}
}

View File

@@ -19,6 +19,8 @@ type Handler interface {
GetMetricAlerts(http.ResponseWriter, *http.Request)
GetMetricDashboards(http.ResponseWriter, *http.Request)
GetMetricHighlights(http.ResponseWriter, *http.Request)
GetOnboardingStatus(http.ResponseWriter, *http.Request)
InspectMetrics(http.ResponseWriter, *http.Request)
}
// Module represents the metrics module interface.
@@ -33,4 +35,6 @@ type Module interface {
GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error)
GetMetricHighlights(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricHighlightsResponse, error)
GetMetricAttributes(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.MetricAttributesRequest) (*metricsexplorertypes.MetricAttributesResponse, error)
HasNonSigNozMetrics(ctx context.Context) (bool, error)
InspectMetrics(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.InspectMetricsRequest) (*metricsexplorertypes.InspectMetricsResponse, error)
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,6 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/metricsexplorer"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -138,8 +137,6 @@ type APIHandler struct {
statefulsetsRepo *inframetrics.StatefulSetsRepo
jobsRepo *inframetrics.JobsRepo
SummaryService *metricsexplorer.SummaryService
pvcsRepo *inframetrics.PvcsRepo
AlertmanagerAPI *alertmanager.API
@@ -209,7 +206,6 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
statefulsetsRepo := inframetrics.NewStatefulSetsRepo(opts.Reader, querierv2)
jobsRepo := inframetrics.NewJobsRepo(opts.Reader, querierv2)
pvcsRepo := inframetrics.NewPvcsRepo(opts.Reader, querierv2)
summaryService := metricsexplorer.NewSummaryService(opts.Reader, opts.RuleManager, opts.Signoz.Modules.Dashboard)
//quickFilterModule := quickfilter.NewAPI(opts.QuickFilterModule)
aH := &APIHandler{
@@ -233,7 +229,6 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
statefulsetsRepo: statefulsetsRepo,
jobsRepo: jobsRepo,
pvcsRepo: pvcsRepo,
SummaryService: summaryService,
AlertmanagerAPI: opts.AlertmanagerAPI,
LicensingAPI: opts.LicensingAPI,
Signoz: opts.Signoz,
@@ -582,32 +577,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_filter/analyze", am.ViewAccess(aH.QueryParserAPI.AnalyzeQueryFilter)).Methods(http.MethodPost)
}
func (ah *APIHandler) MetricExplorerRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/metrics/filters/keys",
am.ViewAccess(ah.FilterKeysSuggestion)).
Methods(http.MethodGet)
router.HandleFunc("/api/v1/metrics/filters/values",
am.ViewAccess(ah.FilterValuesSuggestion)).
Methods(http.MethodPost)
router.HandleFunc("/api/v1/metrics/{metric_name}/metadata",
am.ViewAccess(ah.GetMetricsDetails)).
Methods(http.MethodGet)
router.HandleFunc("/api/v1/metrics",
am.ViewAccess(ah.ListMetrics)).
Methods(http.MethodPost)
router.HandleFunc("/api/v1/metrics/treemap",
am.ViewAccess(ah.GetTreeMap)).
Methods(http.MethodPost)
router.HandleFunc("/api/v1/metrics/related",
am.ViewAccess(ah.GetRelatedMetrics)).
Methods(http.MethodPost)
router.HandleFunc("/api/v1/metrics/inspect",
am.ViewAccess(ah.GetInspectMetricsData)).
Methods(http.MethodPost)
router.HandleFunc("/api/v1/metrics/{metric_name}/metadata",
am.ViewAccess(ah.UpdateMetricsMetadata)).
Methods(http.MethodPost)
}
func Intersection(a, b []int) (c []int) {
m := make(map[int]bool)

View File

@@ -1,154 +0,0 @@
package metricsexplorer
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/model/metrics_explorer"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
)
func ParseFilterKeySuggestions(r *http.Request) (*metrics_explorer.FilterKeyRequest, *model.ApiError) {
searchText := r.URL.Query().Get("searchText")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
limit = 50
}
return &metrics_explorer.FilterKeyRequest{Limit: limit, SearchText: searchText}, nil
}
func ParseFilterValueSuggestions(r *http.Request) (*metrics_explorer.FilterValueRequest, *model.ApiError) {
var filterValueRequest metrics_explorer.FilterValueRequest
// parse the request body
if err := json.NewDecoder(r.Body).Decode(&filterValueRequest); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
return &filterValueRequest, nil
}
func ParseSummaryListMetricsParams(r *http.Request) (*metrics_explorer.SummaryListMetricsRequest, *model.ApiError) {
var listMetricsParams *metrics_explorer.SummaryListMetricsRequest
// parse the request body
if err := json.NewDecoder(r.Body).Decode(&listMetricsParams); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
if listMetricsParams.OrderBy.ColumnName == "" || listMetricsParams.OrderBy.Order == "" {
listMetricsParams.OrderBy.ColumnName = "timeseries" // DEFAULT ORDER BY
listMetricsParams.OrderBy.Order = v3.DirectionDesc
}
if listMetricsParams.Limit == 0 {
listMetricsParams.Limit = 10 // DEFAULT LIMIT
}
return listMetricsParams, nil
}
func ParseTreeMapMetricsParams(r *http.Request) (*metrics_explorer.TreeMapMetricsRequest, *model.ApiError) {
var treeMapMetricParams *metrics_explorer.TreeMapMetricsRequest
// parse the request body
if err := json.NewDecoder(r.Body).Decode(&treeMapMetricParams); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
if treeMapMetricParams.Limit == 0 {
treeMapMetricParams.Limit = 10
}
return treeMapMetricParams, nil
}
func ParseRelatedMetricsParams(r *http.Request) (*metrics_explorer.RelatedMetricsRequest, *model.ApiError) {
var relatedMetricParams metrics_explorer.RelatedMetricsRequest
if err := json.NewDecoder(r.Body).Decode(&relatedMetricParams); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
return &relatedMetricParams, nil
}
func ParseInspectMetricsParams(r *http.Request) (*metrics_explorer.InspectMetricsRequest, *model.ApiError) {
var inspectMetricParams metrics_explorer.InspectMetricsRequest
if err := json.NewDecoder(r.Body).Decode(&inspectMetricParams); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
if inspectMetricParams.End-inspectMetricParams.Start > constants.InspectMetricsMaxTimeDiff { // half hour only
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("time duration shouldn't be more than 30 mins")}
}
return &inspectMetricParams, nil
}
func ParseUpdateMetricsMetadataParams(r *http.Request) (*metrics_explorer.UpdateMetricsMetadataRequest, *model.ApiError) {
var updateMetricsMetadataReq metrics_explorer.UpdateMetricsMetadataRequest
if err := json.NewDecoder(r.Body).Decode(&updateMetricsMetadataReq); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
updateMetricsMetadataReq.MetricName = mux.Vars(r)["metric_name"]
switch updateMetricsMetadataReq.MetricType {
case v3.MetricTypeSum:
if updateMetricsMetadataReq.Temporality == "" {
return nil, &model.ApiError{
Typ: model.ErrorBadData,
Err: fmt.Errorf("temporality is required when metric type is Sum"),
}
}
if updateMetricsMetadataReq.Temporality != v3.Cumulative && updateMetricsMetadataReq.Temporality != v3.Delta {
return nil, &model.ApiError{
Typ: model.ErrorBadData,
Err: fmt.Errorf("invalid value for temporality"),
}
}
case v3.MetricTypeHistogram:
if updateMetricsMetadataReq.Temporality == "" {
return nil, &model.ApiError{
Typ: model.ErrorBadData,
Err: fmt.Errorf("temporality is required when metric type is Histogram"),
}
}
if updateMetricsMetadataReq.Temporality != v3.Cumulative && updateMetricsMetadataReq.Temporality != v3.Delta {
return nil, &model.ApiError{
Typ: model.ErrorBadData,
Err: fmt.Errorf("invalid value for temporality"),
}
}
case v3.MetricTypeExponentialHistogram:
if updateMetricsMetadataReq.Temporality == "" {
return nil, &model.ApiError{
Typ: model.ErrorBadData,
Err: fmt.Errorf("temporality is required when metric type is exponential histogram"),
}
}
if updateMetricsMetadataReq.Temporality != v3.Cumulative && updateMetricsMetadataReq.Temporality != v3.Delta {
return nil, &model.ApiError{
Typ: model.ErrorBadData,
Err: fmt.Errorf("invalid value for temporality"),
}
}
case v3.MetricTypeGauge:
updateMetricsMetadataReq.Temporality = v3.Unspecified
case v3.MetricTypeSummary:
updateMetricsMetadataReq.Temporality = v3.Cumulative
default:
return nil, &model.ApiError{
Typ: model.ErrorBadData,
Err: fmt.Errorf("invalid metric type"),
}
}
return &updateMetricsMetadataReq, nil
}

View File

@@ -1,576 +0,0 @@
package metricsexplorer
import (
"context"
"encoding/json"
"errors"
"sort"
"strings"
"time"
signozerrors "github.com/SigNoz/signoz/pkg/errors"
"log/slog"
"golang.org/x/sync/errgroup"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/model/metrics_explorer"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type SummaryService struct {
reader interfaces.Reader
rulesManager *rules.Manager
dashboard dashboard.Module
}
func NewSummaryService(reader interfaces.Reader, alertManager *rules.Manager, dashboard dashboard.Module) *SummaryService {
return &SummaryService{reader: reader, rulesManager: alertManager, dashboard: dashboard}
}
func (receiver *SummaryService) FilterKeys(ctx context.Context, params *metrics_explorer.FilterKeyRequest) (*metrics_explorer.FilterKeyResponse, *model.ApiError) {
var response metrics_explorer.FilterKeyResponse
keys, apiError := receiver.reader.GetAllMetricFilterAttributeKeys(ctx, params)
if apiError != nil {
return nil, apiError
}
response.AttributeKeys = *keys
var availableColumnFilter []string
for key := range metrics_explorer.AvailableColumnFilterMap {
availableColumnFilter = append(availableColumnFilter, key)
}
response.MetricColumns = availableColumnFilter
return &response, nil
}
func (receiver *SummaryService) FilterValues(ctx context.Context, orgID valuer.UUID, params *metrics_explorer.FilterValueRequest) (*metrics_explorer.FilterValueResponse, *model.ApiError) {
var response metrics_explorer.FilterValueResponse
switch params.FilterKey {
case "metric_name":
var filterValues []string
request := v3.AggregateAttributeRequest{DataSource: v3.DataSourceMetrics, SearchText: params.SearchText, Limit: params.Limit}
attributes, err := receiver.reader.GetMetricAggregateAttributes(ctx, orgID, &request, true)
if err != nil {
return nil, model.InternalError(err)
}
for _, item := range attributes.AttributeKeys {
filterValues = append(filterValues, item.Key)
}
response.FilterValues = filterValues
return &response, nil
case "metric_unit":
attributes, apiErr := receiver.reader.GetAllMetricFilterUnits(ctx, params)
if apiErr != nil {
return nil, apiErr
}
response.FilterValues = attributes
return &response, nil
case "metric_type":
attributes, apiErr := receiver.reader.GetAllMetricFilterTypes(ctx, params)
if apiErr != nil {
return nil, apiErr
}
response.FilterValues = attributes
return &response, nil
default:
attributes, apiErr := receiver.reader.GetAllMetricFilterAttributeValues(ctx, params)
if apiErr != nil {
return nil, apiErr
}
response.FilterValues = attributes
return &response, nil
}
}
func (receiver *SummaryService) GetMetricsSummary(ctx context.Context, orgID valuer.UUID, metricName string) (metrics_explorer.MetricDetailsDTO, *model.ApiError) {
var metricDetailsDTO metrics_explorer.MetricDetailsDTO
g, ctx := errgroup.WithContext(ctx)
// Call 1: GetMetricMetadata
g.Go(func() error {
metadata, err := receiver.reader.GetMetricMetadata(ctx, orgID, metricName, metricName)
if err != nil {
return &model.ApiError{Typ: "ClickHouseError", Err: err}
}
metricDetailsDTO.Name = metricName
metricDetailsDTO.Unit = metadata.Unit
metricDetailsDTO.Description = metadata.Description
metricDetailsDTO.Type = metadata.Type
metricDetailsDTO.Metadata.MetricType = metadata.Type
metricDetailsDTO.Metadata.Description = metadata.Description
metricDetailsDTO.Metadata.Unit = metadata.Unit
metricDetailsDTO.Metadata.Temporality = metadata.Temporality
metricDetailsDTO.Metadata.Monotonic = metadata.IsMonotonic
return nil
})
g.Go(func() error {
dataPoints, apiErr := receiver.reader.GetMetricsDataPoints(ctx, metricName)
if apiErr != nil {
return apiErr.ToError()
}
metricDetailsDTO.Samples = dataPoints
return nil
})
g.Go(func() error {
lastReceived, apiErr := receiver.reader.GetMetricsLastReceived(ctx, metricName)
if apiErr != nil {
return apiErr.ToError()
}
metricDetailsDTO.LastReceived = lastReceived
return nil
})
g.Go(func() error {
totalSeries, apiErr := receiver.reader.GetTotalTimeSeriesForMetricName(ctx, metricName)
if apiErr != nil {
return apiErr.ToError()
}
metricDetailsDTO.TimeSeriesTotal = totalSeries
return nil
})
g.Go(func() error {
activeSeries, apiErr := receiver.reader.GetActiveTimeSeriesForMetricName(ctx, metricName, 120*time.Minute)
if apiErr != nil {
return apiErr.ToError()
}
metricDetailsDTO.TimeSeriesActive = activeSeries
return nil
})
g.Go(func() error {
attributes, apiErr := receiver.reader.GetAttributesForMetricName(ctx, metricName, nil, nil, nil)
if apiErr != nil {
return apiErr.ToError()
}
if attributes != nil {
metricDetailsDTO.Attributes = *attributes
}
return nil
})
g.Go(func() error {
var metricNames []string
metricNames = append(metricNames, metricName)
claims, errv2 := authtypes.ClaimsFromContext(ctx)
if errv2 != nil {
return &model.ApiError{Typ: model.ErrorInternal, Err: errv2}
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
data, err := receiver.dashboard.GetByMetricNames(ctx, orgID, metricNames)
if err != nil {
return err
}
if data != nil {
jsonData, err := json.Marshal(data)
if err != nil {
slog.Error("error marshalling data", signozerrors.Attr(err))
return &model.ApiError{Typ: "MarshallingErr", Err: err}
}
var dashboards map[string][]metrics_explorer.Dashboard
err = json.Unmarshal(jsonData, &dashboards)
if err != nil {
slog.Error("error unmarshalling data", signozerrors.Attr(err))
return &model.ApiError{Typ: "UnMarshallingErr", Err: err}
}
if _, ok := dashboards[metricName]; ok {
metricDetailsDTO.Dashboards = dashboards[metricName]
}
}
return nil
})
g.Go(func() error {
var metrics []string
var metricsAlerts []metrics_explorer.Alert
metrics = append(metrics, metricName)
data, err := receiver.rulesManager.GetAlertDetailsForMetricNames(ctx, metrics)
if err != nil {
return err
}
if rulesLists, ok := data[metricName]; ok {
for _, rule := range rulesLists {
metricsAlerts = append(metricsAlerts, metrics_explorer.Alert{AlertName: rule.AlertName, AlertID: rule.Id})
}
}
metricDetailsDTO.Alerts = metricsAlerts
return nil
})
// Wait for all goroutines and handle any errors
if err := g.Wait(); err != nil {
var apiErr *model.ApiError
if errors.As(err, &apiErr) {
return metrics_explorer.MetricDetailsDTO{}, apiErr
}
return metrics_explorer.MetricDetailsDTO{}, &model.ApiError{Typ: "InternalError", Err: err}
}
return metricDetailsDTO, nil
}
func (receiver *SummaryService) ListMetricsWithSummary(ctx context.Context, orgID valuer.UUID, params *metrics_explorer.SummaryListMetricsRequest) (*metrics_explorer.SummaryListMetricsResponse, *model.ApiError) {
return receiver.reader.ListSummaryMetrics(ctx, orgID, params)
}
func (receiver *SummaryService) GetMetricsTreemap(ctx context.Context, params *metrics_explorer.TreeMapMetricsRequest) (*metrics_explorer.TreeMap, *model.ApiError) {
var response metrics_explorer.TreeMap
switch params.Treemap {
case metrics_explorer.TimeSeriesTeeMap:
ts, apiError := receiver.reader.GetMetricsTimeSeriesPercentage(ctx, params)
if apiError != nil {
return nil, apiError
}
if ts != nil {
response.TimeSeries = *ts
}
return &response, nil
case metrics_explorer.SamplesTreeMap:
samples, apiError := receiver.reader.GetMetricsSamplesPercentage(ctx, params)
if apiError != nil {
return nil, apiError
}
if samples != nil {
response.Samples = *samples
}
return &response, nil
default:
return nil, nil
}
}
func (receiver *SummaryService) GetRelatedMetrics(ctx context.Context, params *metrics_explorer.RelatedMetricsRequest) (*metrics_explorer.RelatedMetricsResponse, *model.ApiError) {
// Get name similarity scores
nameSimilarityScores, err := receiver.reader.GetNameSimilarity(ctx, params)
if err != nil {
return nil, err
}
attrCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
attrSimilarityScores, err := receiver.reader.GetAttributeSimilarity(attrCtx, params)
if err != nil {
// If we hit a deadline exceeded error, proceed with only name similarity
if errors.Is(err.Err, context.DeadlineExceeded) {
slog.Warn("attribute similarity calculation timed out, proceeding with name similarity only")
attrSimilarityScores = make(map[string]metrics_explorer.RelatedMetricsScore)
} else {
return nil, err
}
}
// Combine scores and compute final scores
finalScores := make(map[string]float64)
relatedMetricsMap := make(map[string]metrics_explorer.RelatedMetricsScore)
// Merge name and attribute similarity scores
for metric, nameScore := range nameSimilarityScores {
attrScore, exists := attrSimilarityScores[metric]
if exists {
relatedMetricsMap[metric] = metrics_explorer.RelatedMetricsScore{
NameSimilarity: nameScore.NameSimilarity,
AttributeSimilarity: attrScore.AttributeSimilarity,
Filters: attrScore.Filters,
MetricType: attrScore.MetricType,
Temporality: attrScore.Temporality,
IsMonotonic: attrScore.IsMonotonic,
}
} else {
relatedMetricsMap[metric] = nameScore
}
finalScores[metric] = nameScore.NameSimilarity*0.7 + relatedMetricsMap[metric].AttributeSimilarity*0.3
}
// Handle metrics that are only present in attribute similarity scores
for metric, attrScore := range attrSimilarityScores {
if _, exists := nameSimilarityScores[metric]; !exists {
relatedMetricsMap[metric] = metrics_explorer.RelatedMetricsScore{
AttributeSimilarity: attrScore.AttributeSimilarity,
Filters: attrScore.Filters,
MetricType: attrScore.MetricType,
Temporality: attrScore.Temporality,
IsMonotonic: attrScore.IsMonotonic,
}
finalScores[metric] = attrScore.AttributeSimilarity * 0.3
}
}
type metricScore struct {
Name string
Score float64
}
var sortedScores []metricScore
for metric, score := range finalScores {
sortedScores = append(sortedScores, metricScore{
Name: metric,
Score: score,
})
}
sort.Slice(sortedScores, func(i, j int) bool {
return sortedScores[i].Score > sortedScores[j].Score
})
metricNames := make([]string, len(sortedScores))
for i, ms := range sortedScores {
metricNames[i] = ms.Name
}
// Fetch dashboards and alerts concurrently
g, ctx := errgroup.WithContext(ctx)
dashboardsRelatedData := make(map[string][]metrics_explorer.Dashboard)
alertsRelatedData := make(map[string][]metrics_explorer.Alert)
g.Go(func() error {
claims, errv2 := authtypes.ClaimsFromContext(ctx)
if errv2 != nil {
return &model.ApiError{Typ: model.ErrorInternal, Err: errv2}
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
names, err := receiver.dashboard.GetByMetricNames(ctx, orgID, metricNames)
if err != nil {
return err
}
if names != nil {
jsonData, err := json.Marshal(names)
if err != nil {
slog.Error("error marshalling dashboard data", signozerrors.Attr(err))
return &model.ApiError{Typ: "MarshallingErr", Err: err}
}
err = json.Unmarshal(jsonData, &dashboardsRelatedData)
if err != nil {
slog.Error("error unmarshalling dashboard data", signozerrors.Attr(err))
return &model.ApiError{Typ: "UnMarshallingErr", Err: err}
}
}
return nil
})
g.Go(func() error {
rulesData, apiError := receiver.rulesManager.GetAlertDetailsForMetricNames(ctx, metricNames)
if apiError != nil {
return apiError
}
for s, gettableRules := range rulesData {
var metricsRules []metrics_explorer.Alert
for _, rule := range gettableRules {
metricsRules = append(metricsRules, metrics_explorer.Alert{AlertID: rule.Id, AlertName: rule.AlertName})
}
alertsRelatedData[s] = metricsRules
}
return nil
})
// Check for context cancellation before waiting
if ctx.Err() != nil {
return nil, &model.ApiError{Typ: "ContextCanceled", Err: ctx.Err()}
}
if err := g.Wait(); err != nil {
var apiErr *model.ApiError
if errors.As(err, &apiErr) {
return nil, apiErr
}
return nil, &model.ApiError{Typ: "InternalError", Err: err}
}
// Build response
var response metrics_explorer.RelatedMetricsResponse
for _, ms := range sortedScores {
relatedMetric := metrics_explorer.RelatedMetrics{
Name: ms.Name,
Query: getQueryRangeForRelateMetricsList(ms.Name, relatedMetricsMap[ms.Name]),
}
if dashboardsDetails, ok := dashboardsRelatedData[ms.Name]; ok {
relatedMetric.Dashboards = dashboardsDetails
}
if alerts, ok := alertsRelatedData[ms.Name]; ok {
relatedMetric.Alerts = alerts
}
response.RelatedMetrics = append(response.RelatedMetrics, relatedMetric)
}
return &response, nil
}
func getQueryRangeForRelateMetricsList(metricName string, scores metrics_explorer.RelatedMetricsScore) *v3.BuilderQuery {
var filterItems []v3.FilterItem
for _, pair := range scores.Filters {
if len(pair) < 2 {
continue // Skip invalid filter pairs.
}
filterItem := v3.FilterItem{
Key: v3.AttributeKey{
Key: pair[0], // Default type, or you can use v3.AttributeKeyTypeUnspecified.
IsColumn: false,
IsJSON: false,
},
Value: pair[1],
Operator: v3.FilterOperatorEqual, // Using "=" as the operator.
}
filterItems = append(filterItems, filterItem)
}
// If there are any filters, combine them with an "AND" operator.
var filters *v3.FilterSet
if len(filterItems) > 0 {
filters = &v3.FilterSet{
Operator: "AND",
Items: filterItems,
}
}
// Create the BuilderQuery. Here we set the QueryName to the metric name.
query := v3.BuilderQuery{
QueryName: metricName,
DataSource: v3.DataSourceMetrics,
Expression: metricName, // Using metric name as expression
Filters: filters,
}
if scores.MetricType == v3.MetricTypeSum && !scores.IsMonotonic && scores.Temporality == v3.Cumulative {
scores.MetricType = v3.MetricTypeGauge
}
switch scores.MetricType {
case v3.MetricTypeGauge:
query.TimeAggregation = v3.TimeAggregationAvg
query.SpaceAggregation = v3.SpaceAggregationAvg
case v3.MetricTypeSum:
query.TimeAggregation = v3.TimeAggregationRate
query.SpaceAggregation = v3.SpaceAggregationSum
case v3.MetricTypeHistogram:
query.SpaceAggregation = v3.SpaceAggregationPercentile95
}
query.AggregateAttribute = v3.AttributeKey{
Key: metricName,
Type: v3.AttributeKeyType(scores.MetricType),
}
query.StepInterval = 60
return &query
}
func (receiver *SummaryService) GetInspectMetrics(ctx context.Context, params *metrics_explorer.InspectMetricsRequest) (*metrics_explorer.InspectMetricsResponse, *model.ApiError) {
// Capture the original context.
parentCtx := ctx
// Create an errgroup using the original context.
g, egCtx := errgroup.WithContext(ctx)
var attributes []metrics_explorer.Attribute
var resourceAttrs map[string]uint64
// Run the two queries concurrently using the derived context.
g.Go(func() error {
attrs, apiErr := receiver.reader.GetAttributesForMetricName(egCtx, params.MetricName, &params.Start, &params.End, &params.Filters)
if apiErr != nil {
return apiErr
}
if attrs != nil {
attributes = *attrs
}
return nil
})
g.Go(func() error {
resAttrs, apiErr := receiver.reader.GetMetricsAllResourceAttributes(egCtx, params.Start, params.End)
if apiErr != nil {
return apiErr
}
if resAttrs != nil {
resourceAttrs = resAttrs
}
return nil
})
// Wait for the concurrent operations to complete.
if err := g.Wait(); err != nil {
return nil, &model.ApiError{Typ: "InternalError", Err: err}
}
// Use the parentCtx (or create a new context from it) for the rest of the calls.
if parentCtx.Err() != nil {
return nil, &model.ApiError{Typ: "ContextCanceled", Err: parentCtx.Err()}
}
// Build a set of attribute keys for O(1) lookup.
attributeKeys := make(map[string]struct{})
for _, attr := range attributes {
attributeKeys[attr.Key] = struct{}{}
}
// Filter resource attributes that are present in attributes.
var validAttrs []string
for attrName := range resourceAttrs {
normalizedAttrName := strings.ReplaceAll(attrName, ".", "_")
if _, ok := attributeKeys[normalizedAttrName]; ok {
validAttrs = append(validAttrs, normalizedAttrName)
}
}
// Get top 3 resource attributes (or use top attributes by valueCount if none match).
if len(validAttrs) > 3 {
validAttrs = validAttrs[:3]
} else if len(validAttrs) == 0 {
sort.Slice(attributes, func(i, j int) bool {
return attributes[i].ValueCount > attributes[j].ValueCount
})
for i := 0; i < len(attributes) && i < 3; i++ {
validAttrs = append(validAttrs, attributes[i].Key)
}
}
fingerprints, apiError := receiver.reader.GetInspectMetricsFingerprints(parentCtx, validAttrs, params)
if apiError != nil {
return nil, apiError
}
baseResponse, apiErr := receiver.reader.GetInspectMetrics(parentCtx, params, fingerprints)
if apiErr != nil {
return nil, apiErr
}
return baseResponse, nil
}
func (receiver *SummaryService) UpdateMetricsMetadata(ctx context.Context, orgID valuer.UUID, params *metrics_explorer.UpdateMetricsMetadataRequest) *model.ApiError {
if params.MetricType == v3.MetricTypeSum && !params.IsMonotonic && params.Temporality == v3.Cumulative {
params.MetricType = v3.MetricTypeGauge
}
metadata := model.UpdateMetricsMetadata{
MetricName: params.MetricName,
MetricType: params.MetricType,
Temporality: params.Temporality,
Unit: params.Unit,
Description: params.Description,
IsMonotonic: params.IsMonotonic,
CreatedAt: time.Now(),
}
apiError := receiver.reader.UpdateMetricsMetadata(ctx, orgID, &metadata)
if apiError != nil {
return apiError
}
return nil
}

View File

@@ -223,7 +223,6 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
api.RegisterQueryRangeV4Routes(r, am)
api.RegisterMessagingQueuesRoutes(r, am)
api.RegisterThirdPartyApiRoutes(r, am)
api.MetricExplorerRoutes(r, am)
api.RegisterTraceFunnelsRoutes(r, am)
err := s.signoz.APIServer.AddToRouter(r)

View File

@@ -1,214 +0,0 @@
package app
import (
"bytes"
"io"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"log/slog"
explorer "github.com/SigNoz/signoz/pkg/query-service/app/metricsexplorer"
)
func (aH *APIHandler) FilterKeysSuggestion(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
params, apiError := explorer.ParseFilterKeySuggestions(r)
if apiError != nil {
slog.ErrorContext(ctx, "error parsing summary filter keys request", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
keys, apiError := aH.SummaryService.FilterKeys(ctx, params)
if apiError != nil {
slog.ErrorContext(ctx, "error getting filter keys", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) FilterValuesSuggestion(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
params, apiError := explorer.ParseFilterValueSuggestions(r)
if apiError != nil {
slog.ErrorContext(ctx, "error parsing summary filter values request", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
values, apiError := aH.SummaryService.FilterValues(ctx, orgID, params)
if apiError != nil {
slog.ErrorContext(ctx, "error getting filter values", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) GetMetricsDetails(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
metricName := mux.Vars(r)["metric_name"]
metricsDetail, apiError := aH.SummaryService.GetMetricsSummary(ctx, orgID, metricName)
if apiError != nil {
slog.ErrorContext(ctx, "error getting metrics summary", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, metricsDetail)
}
func (aH *APIHandler) ListMetrics(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
params, apiErr := explorer.ParseSummaryListMetricsParams(r)
if apiErr != nil {
slog.ErrorContext(ctx, "error parsing metric list metric summary api request", errors.Attr(apiErr.Err))
RespondError(w, model.BadRequest(apiErr), nil)
return
}
slmr, apiErr := aH.SummaryService.ListMetricsWithSummary(ctx, orgID, params)
if apiErr != nil {
slog.ErrorContext(ctx, "error in getting list metrics summary", errors.Attr(apiErr.Err))
RespondError(w, apiErr, nil)
return
}
aH.Respond(w, slmr)
}
func (aH *APIHandler) GetTreeMap(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
ctx := r.Context()
params, apiError := explorer.ParseTreeMapMetricsParams(r)
if apiError != nil {
slog.ErrorContext(ctx, "error parsing tree map metric params", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
result, apiError := aH.SummaryService.GetMetricsTreemap(ctx, params)
if apiError != nil {
slog.ErrorContext(ctx, "error getting tree map data", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, result)
}
func (aH *APIHandler) GetRelatedMetrics(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
ctx := r.Context()
params, apiError := explorer.ParseRelatedMetricsParams(r)
if apiError != nil {
slog.ErrorContext(ctx, "error parsing related metric params", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
result, apiError := aH.SummaryService.GetRelatedMetrics(ctx, params)
if apiError != nil {
slog.ErrorContext(ctx, "error getting related metrics", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, result)
}
func (aH *APIHandler) GetInspectMetricsData(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
ctx := r.Context()
params, apiError := explorer.ParseInspectMetricsParams(r)
if apiError != nil {
slog.ErrorContext(ctx, "error parsing inspect metric params", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
result, apiError := aH.SummaryService.GetInspectMetrics(ctx, params)
if apiError != nil {
slog.ErrorContext(ctx, "error getting inspect metrics data", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, result)
}
func (aH *APIHandler) UpdateMetricsMetadata(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
params, apiError := explorer.ParseUpdateMetricsMetadataParams(r)
if apiError != nil {
slog.ErrorContext(ctx, "error parsing update metrics metadata params", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
apiError = aH.SummaryService.UpdateMetricsMetadata(ctx, orgID, params)
if apiError != nil {
slog.ErrorContext(ctx, "error updating metrics metadata", errors.Attr(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, nil)
}

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/model/metrics_explorer"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/querycache"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -107,29 +106,6 @@ type Reader interface {
GetTraceFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError)
UpdateTraceField(ctx context.Context, field *model.UpdateField) *model.ApiError
GetAllMetricFilterAttributeValues(ctx context.Context, req *metrics_explorer.FilterValueRequest) ([]string, *model.ApiError)
GetAllMetricFilterUnits(ctx context.Context, req *metrics_explorer.FilterValueRequest) ([]string, *model.ApiError)
GetAllMetricFilterTypes(ctx context.Context, req *metrics_explorer.FilterValueRequest) ([]string, *model.ApiError)
GetAllMetricFilterAttributeKeys(ctx context.Context, req *metrics_explorer.FilterKeyRequest) (*[]v3.AttributeKey, *model.ApiError)
GetMetricsDataPoints(ctx context.Context, metricName string) (uint64, *model.ApiError)
GetMetricsLastReceived(ctx context.Context, metricName string) (int64, *model.ApiError)
GetTotalTimeSeriesForMetricName(ctx context.Context, metricName string) (uint64, *model.ApiError)
GetActiveTimeSeriesForMetricName(ctx context.Context, metricName string, duration time.Duration) (uint64, *model.ApiError)
GetAttributesForMetricName(ctx context.Context, metricName string, start, end *int64, set *v3.FilterSet) (*[]metrics_explorer.Attribute, *model.ApiError)
ListSummaryMetrics(ctx context.Context, orgID valuer.UUID, req *metrics_explorer.SummaryListMetricsRequest) (*metrics_explorer.SummaryListMetricsResponse, *model.ApiError)
GetMetricsTimeSeriesPercentage(ctx context.Context, request *metrics_explorer.TreeMapMetricsRequest) (*[]metrics_explorer.TreeMapResponseItem, *model.ApiError)
GetMetricsSamplesPercentage(ctx context.Context, req *metrics_explorer.TreeMapMetricsRequest) (*[]metrics_explorer.TreeMapResponseItem, *model.ApiError)
GetNameSimilarity(ctx context.Context, req *metrics_explorer.RelatedMetricsRequest) (map[string]metrics_explorer.RelatedMetricsScore, *model.ApiError)
GetAttributeSimilarity(ctx context.Context, req *metrics_explorer.RelatedMetricsRequest) (map[string]metrics_explorer.RelatedMetricsScore, *model.ApiError)
GetMetricsAllResourceAttributes(ctx context.Context, start int64, end int64) (map[string]uint64, *model.ApiError)
GetInspectMetricsFingerprints(ctx context.Context, attributes []string, req *metrics_explorer.InspectMetricsRequest) ([]string, *model.ApiError)
GetInspectMetrics(ctx context.Context, req *metrics_explorer.InspectMetricsRequest, fingerprints []string) (*metrics_explorer.InspectMetricsResponse, *model.ApiError)
UpdateMetricsMetadata(ctx context.Context, orgID valuer.UUID, req *model.UpdateMetricsMetadata) *model.ApiError
GetUpdatedMetricsMetadata(ctx context.Context, orgID valuer.UUID, metricNames ...string) (map[string]*model.UpdateMetricsMetadata, *model.ApiError)

View File

@@ -1,173 +0,0 @@
package metrics_explorer
import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
)
type SummaryListMetricsRequest struct {
Offset int `json:"offset"`
Limit int `json:"limit"`
OrderBy v3.OrderBy `json:"orderBy"`
Start int64 `json:"start"`
End int64 `json:"end"`
Filters v3.FilterSet `json:"filters"`
}
type TreeMapType string
const (
TimeSeriesTeeMap TreeMapType = "timeseries"
SamplesTreeMap TreeMapType = "samples"
)
type TreeMapMetricsRequest struct {
Limit int `json:"limit"`
Treemap TreeMapType `json:"treemap"`
Start int64 `json:"start"`
End int64 `json:"end"`
Filters v3.FilterSet `json:"filters"`
}
type MetricDetail struct {
MetricName string `json:"metric_name"`
Description string `json:"description"`
MetricType string `json:"type"`
MetricUnit string `json:"unit"`
TimeSeries uint64 `json:"timeseries"`
Samples uint64 `json:"samples"`
LastReceived int64 `json:"lastReceived"`
}
type TreeMapResponseItem struct {
Percentage float64 `json:"percentage"`
TotalValue uint64 `json:"total_value"`
MetricName string `json:"metric_name"`
}
type TreeMap struct {
TimeSeries []TreeMapResponseItem `json:"timeseries"`
Samples []TreeMapResponseItem `json:"samples"`
}
type SummaryListMetricsResponse struct {
Metrics []MetricDetail `json:"metrics"`
Total uint64 `json:"total"`
}
type Attribute struct {
Key string `json:"key" db:"key"`
Value []string `json:"value" db:"value"`
ValueCount uint64 `json:"valueCount" db:"valueCount"`
}
// Metadata holds additional information about the metric.
type Metadata struct {
MetricType string `json:"metric_type"`
Description string `json:"description"`
Unit string `json:"unit"`
Temporality string `json:"temporality"`
Monotonic bool `json:"monotonic"`
}
// Alert represents individual alerts associated with the metric.
type Alert struct {
AlertName string `json:"alert_name"`
AlertID string `json:"alert_id"`
}
// Dashboard represents individual dashboards associated with the metric.
type Dashboard struct {
DashboardName string `json:"dashboard_name"`
DashboardID string `json:"dashboard_id"`
WidgetID string `json:"widget_id"`
WidgetName string `json:"widget_name"`
}
type MetricDetailsDTO struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
Unit string `json:"unit"`
Samples uint64 `json:"samples"`
TimeSeriesTotal uint64 `json:"timeSeriesTotal"`
TimeSeriesActive uint64 `json:"timeSeriesActive"`
LastReceived int64 `json:"lastReceived"`
Attributes []Attribute `json:"attributes"`
Metadata Metadata `json:"metadata"`
Alerts []Alert `json:"alerts"`
Dashboards []Dashboard `json:"dashboards"`
}
type FilterKeyRequest struct {
SearchText string `json:"searchText"`
Limit int `json:"limit"`
}
type FilterValueRequest struct {
FilterKey string `json:"filterKey"`
FilterAttributeKeyDataType v3.AttributeKeyDataType `json:"filterAttributeKeyDataType"`
SearchText string `json:"searchText"`
Limit int `json:"limit"`
}
type FilterValueResponse struct {
FilterValues []string `json:"filterValues"`
}
type FilterKeyResponse struct {
MetricColumns []string `json:"metricColumns"`
AttributeKeys []v3.AttributeKey `json:"attributeKeys"`
}
var AvailableColumnFilterMap = map[string]bool{
"metric_name": true,
"metric_unit": true,
"metric_type": true,
}
type RelatedMetricsScore struct {
AttributeSimilarity float64
NameSimilarity float64
Filters [][]string
MetricType v3.MetricType
Temporality v3.Temporality
IsMonotonic bool
}
type RelatedMetricsRequest struct {
CurrentMetricName string `json:"currentMetricName"`
Start int64 `json:"start"`
End int64 `json:"end"`
Filters v3.FilterSet `json:"filters"`
}
type RelatedMetricsResponse struct {
RelatedMetrics []RelatedMetrics `json:"related_metrics"`
}
type RelatedMetrics struct {
Name string `json:"name"`
Query *v3.BuilderQuery `json:"query"`
Dashboards []Dashboard `json:"dashboards"`
Alerts []Alert `json:"alerts"`
}
type InspectMetricsRequest struct {
MetricName string `json:"metricName"`
Filters v3.FilterSet `json:"filters"`
Start int64 `json:"start"`
End int64 `json:"end"`
}
type InspectMetricsResponse struct {
Series *[]v3.Series `json:"series,omitempty"`
}
type UpdateMetricsMetadataRequest struct {
MetricName string `json:"metricName"`
MetricType v3.MetricType `json:"metricType"`
Description string `json:"description"`
Unit string `json:"unit"`
Temporality v3.Temporality `json:"temporality"`
IsMonotonic bool `json:"isMonotonic"`
}

View File

@@ -1,105 +1,11 @@
package utils
import (
"fmt"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/model/metrics_explorer"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
)
// skipKey is an optional parameter to skip processing of a specific key
func BuildFilterConditions(fs *v3.FilterSet, skipKey string) ([]string, error) {
if fs == nil || len(fs.Items) == 0 {
return nil, nil
}
var conditions []string
for _, item := range fs.Items {
if skipKey != "" && item.Key.Key == skipKey {
continue
}
toFormat := item.Value
op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
if op == v3.FilterOperatorContains || op == v3.FilterOperatorNotContains {
toFormat = fmt.Sprintf("%%%s%%", toFormat)
}
fmtVal := ClickHouseFormattedValue(toFormat)
// Determine if the key is a JSON key or a normal column
isJSONKey := false
if _, exists := metrics_explorer.AvailableColumnFilterMap[item.Key.Key]; exists {
isJSONKey = false
} else {
isJSONKey = true
}
condition, err := buildSingleFilterCondition(item.Key.Key, op, fmtVal, isJSONKey)
if err != nil {
return nil, err
}
conditions = append(conditions, condition)
}
return conditions, nil
}
func buildSingleFilterCondition(key string, op v3.FilterOperator, fmtVal string, isJSONKey bool) (string, error) {
var keyCondition string
if isJSONKey {
keyCondition = fmt.Sprintf("JSONExtractString(labels, '%s')", key)
} else { // Assuming normal column access
if key == "metric_unit" {
key = "unit"
}
if key == "metric_type" {
key = "type"
}
keyCondition = key
}
switch op {
case v3.FilterOperatorEqual:
return fmt.Sprintf("%s = %s", keyCondition, fmtVal), nil
case v3.FilterOperatorNotEqual:
return fmt.Sprintf("%s != %s", keyCondition, fmtVal), nil
case v3.FilterOperatorIn:
return fmt.Sprintf("%s IN %s", keyCondition, fmtVal), nil
case v3.FilterOperatorNotIn:
return fmt.Sprintf("%s NOT IN %s", keyCondition, fmtVal), nil
case v3.FilterOperatorLike:
return fmt.Sprintf("like(%s, %s)", keyCondition, fmtVal), nil
case v3.FilterOperatorNotLike:
return fmt.Sprintf("notLike(%s, %s)", keyCondition, fmtVal), nil
case v3.FilterOperatorRegex:
return fmt.Sprintf("match(%s, %s)", keyCondition, fmtVal), nil
case v3.FilterOperatorNotRegex:
return fmt.Sprintf("not match(%s, %s)", keyCondition, fmtVal), nil
case v3.FilterOperatorGreaterThan:
return fmt.Sprintf("%s > %s", keyCondition, fmtVal), nil
case v3.FilterOperatorGreaterThanOrEq:
return fmt.Sprintf("%s >= %s", keyCondition, fmtVal), nil
case v3.FilterOperatorLessThan:
return fmt.Sprintf("%s < %s", keyCondition, fmtVal), nil
case v3.FilterOperatorLessThanOrEq:
return fmt.Sprintf("%s <= %s", keyCondition, fmtVal), nil
case v3.FilterOperatorContains:
return fmt.Sprintf("ilike(%s, %s)", keyCondition, fmtVal), nil
case v3.FilterOperatorNotContains:
return fmt.Sprintf("notILike(%s, %s)", keyCondition, fmtVal), nil
case v3.FilterOperatorExists:
return fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", key), nil
case v3.FilterOperatorNotExists:
return fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", key), nil
default:
return "", fmt.Errorf("unsupported filter operator: %s", op)
}
}
var (
sixHoursInMilliseconds = time.Hour.Milliseconds() * 6
oneDayInMilliseconds = time.Hour.Milliseconds() * 24

View File

@@ -341,3 +341,39 @@ type ListMetric struct {
type ListMetricsResponse struct {
Metrics []ListMetric `json:"metrics" required:"true" nullable:"true"`
}
// MetricsOnboardingResponse is the response for the lightweight metrics onboarding check.
type MetricsOnboardingResponse struct {
HasMetrics bool `json:"hasMetrics" required:"true"`
}
// InspectMetricsRequest is the request for inspecting raw metric data points.
type InspectMetricsRequest struct {
MetricName string `json:"metricName" required:"true"`
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *qbtypes.Filter `json:"filter,omitempty"`
}
// Validate ensures the request is valid.
func (r *InspectMetricsRequest) Validate() error {
if r.MetricName == "" {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
}
if r.Start <= 0 || r.End <= 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start and end must be positive")
}
if r.Start >= r.End {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "start must be less than end")
}
maxRange := int64(30 * 60 * 1000) // 30 minutes in milliseconds
if r.End-r.Start > maxRange {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "time range must not exceed 30 minutes")
}
return nil
}
// InspectMetricsResponse is the response for the inspect metrics endpoint.
type InspectMetricsResponse struct {
Series []*qbtypes.TimeSeries `json:"series" required:"true" nullable:"true"`
}