Merge branch 'demo/trace-operators' of github.com:SigNoz/signoz into demo/trace_operators_backend

This commit is contained in:
eKuG 2025-08-21 10:18:35 +05:30
commit a2ab97a347
41 changed files with 1269 additions and 300 deletions

View File

@ -257,6 +257,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)

View File

@ -5,7 +5,10 @@ import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
@ -276,6 +279,103 @@ export function convertBuilderQueriesToV5(
);
}
function createTraceOperatorBaseSpec(
queryData: IBuilderTraceOperator,
requestType: RequestType,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
const nonEmptySelectColumns = (queryData.selectColumns as (
| BaseAutocompleteData
| TelemetryFieldKey
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || undefined,
groupBy:
queryData.groupBy?.length > 0
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? queryData.limit || queryData.pageSize || undefined
: queryData.limit || undefined,
offset:
requestType === 'raw' || requestType === 'trace'
? queryData.offset
: undefined,
order:
queryData.orderBy?.length > 0
? queryData.orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
having: isEmpty(queryData.having) ? undefined : (queryData?.having as Having),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
export function convertTraceOperatorToV5(
traceOperator: Record<string, IBuilderTraceOperator>,
requestType: RequestType,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(traceOperator).map(
([queryName, traceOperatorData]): QueryEnvelope => {
const baseSpec = createTraceOperatorBaseSpec(
traceOperatorData,
requestType,
panelType,
);
let spec: QueryEnvelope['spec'];
// Skip aggregation for raw request type
const aggregations =
requestType === 'raw'
? undefined
: createAggregation(traceOperatorData, panelType);
spec = {
name: queryName,
returnSpansFrom: traceOperatorData.returnSpansFrom || '',
...baseSpec,
expression: traceOperatorData.expression || '',
aggregations: aggregations as TraceAggregation[],
};
return {
type: 'builder_trace_operator' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
@ -357,14 +457,27 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas } = query.builder;
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
const filteredTraceOperator =
queryTraceOperator && queryTraceOperator.length > 0
? queryTraceOperator.filter((traceOperator) =>
Boolean(traceOperator.expression.trim()),
)
: [];
const currentTraceOperator = mapQueryDataToApi(
filteredTraceOperator,
'queryName',
);
// Combine legend maps
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
...currentTraceOperator.newLegendMap,
};
// Convert builder queries
@ -397,8 +510,36 @@ export const prepareQueryRangePayloadV5 = ({
}),
);
const traceOperatorQueries = convertTraceOperatorToV5(
currentTraceOperator.data,
requestType,
graphType,
);
// const traceOperatorQueries = Object.entries(currentTraceOperator.data).map(
// ([queryName, traceOperatorData]): QueryEnvelope => ({
// type: 'builder_trace_operator' as const,
// spec: {
// name: queryName,
// expression: traceOperatorData.expression || '',
// legend: isEmpty(traceOperatorData.legend)
// ? undefined
// : traceOperatorData.legend,
// limit: 10,
// order: traceOperatorData.orderBy?.map(
// // eslint-disable-next-line sonarjs/no-identical-functions
// (order: any): OrderBy => ({
// key: {
// name: order.columnName,
// },
// direction: order.order,
// }),
// ),
// },
// }),
// );
// Combine both types
queries = [...builderQueries, ...formulaQueries];
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
break;
}
case EQueryType.PROM: {

View File

@ -22,6 +22,10 @@
flex: 1;
position: relative;
.qb-trace-view-selector-container {
padding: 12px 8px 8px 8px;
}
}
.qb-content-section {
@ -179,7 +183,7 @@
flex-direction: column;
gap: 8px;
margin-left: 32px;
margin-left: 26px;
padding-bottom: 16px;
padding-left: 8px;
@ -195,8 +199,8 @@
}
.formula-container {
margin-left: 82px;
padding: 4px 0px;
padding: 8px;
margin-left: 74px;
.ant-col {
&::before {
@ -331,6 +335,12 @@
);
left: 15px;
}
&.has-trace-operator {
&::before {
height: 0px;
}
}
}
.formula-name {
@ -347,7 +357,7 @@
&::before {
content: '';
height: 65px;
height: 128px;
content: '';
position: absolute;
left: 0;

View File

@ -5,11 +5,13 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@ -54,6 +58,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
newPanelType,
]);
const isMultiQueryAllowed = useMemo(
() =>
!showOnlyWhereClause ||
!isListViewPanel ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[showOnlyWhereClause, currentDataSource, showTraceOperator, isListViewPanel],
);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
@ -97,11 +109,45 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
listViewTracesFilterConfigs,
]);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0
) {
return currentQuery.builder.queryTraceOperator[0];
}
return undefined;
}, [currentQuery.builder.queryTraceOperator]);
const shouldShowTraceOperator = useMemo(
() =>
showTraceOperator &&
currentDataSource === DataSource.TRACES &&
Boolean(traceOperator),
[currentDataSource, showTraceOperator, traceOperator],
);
const shouldShowFooter = useMemo(
() =>
(!showOnlyWhereClause && !isListViewPanel) ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
);
const showFormula = useMemo(() => {
if (currentDataSource === DataSource.TRACES) {
return !isListViewPanel;
}
return true;
}, [isListViewPanel, currentDataSource]);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{isListViewPanel && (
{!isMultiQueryAllowed ? (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@ -109,15 +155,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={shouldShowTraceOperator}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
)}
{!isListViewPanel &&
) : (
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
@ -127,13 +173,16 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
showTraceOperator={shouldShowTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))}
))
)}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
@ -158,15 +207,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
{shouldShowFooter && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showTraceOperator && !traceOperator}
/>
)}
{shouldShowTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
{isMultiQueryAllowed && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

View File

@ -1,7 +1,11 @@
.query-add-ons {
width: 100%;
}
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;

View File

@ -144,6 +144,8 @@ function QueryAddOns({
showReduceTo,
panelType,
index,
isForTraceOperator = false,
children,
}: {
query: IBuilderQuery;
version: string;
@ -151,6 +153,8 @@ function QueryAddOns({
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
children?: React.ReactNode;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
@ -160,6 +164,7 @@ function QueryAddOns({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();
@ -486,6 +491,7 @@ function QueryAddOns({
</Tooltip>
))}
</Radio.Group>
{children}
</div>
</div>
);

View File

@ -4,7 +4,10 @@ import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@ -20,7 +23,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery;
queryData: IBuilderQuery | IBuilderTraceOperator;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return

View File

@ -4,9 +4,15 @@ import { Plus, Sigma } from 'lucide-react';
export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
showAddFormula = true,
showAddTraceOperator = false,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
addTraceOperator?: () => void;
showAddTraceOperator: boolean;
showAddFormula?: boolean;
}): JSX.Element {
return (
<div className="qb-footer">
@ -22,32 +28,62 @@ export default function QueryFooter({
</Tooltip>
</div>
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
{showAddFormula && (
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
Add Formula
</Button>
</Tooltip>
</div>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
Add Formula
</Button>
</Tooltip>
</div>
)}
{showAddTraceOperator && (
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add Trace Matching
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={() => addTraceOperator?.()}
>
Add Trace Matching
</Button>
</Tooltip>
</div>
)}
</div>
</div>
);

View File

@ -7,6 +7,7 @@
'Helvetica Neue', sans-serif;
.query-where-clause-editor-container {
position: relative;
display: flex;
flex-direction: row;

View File

@ -26,9 +26,11 @@ export const QueryV2 = memo(function QueryV2({
query,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
version,
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
@ -108,11 +110,15 @@ export const QueryV2 = memo(function QueryV2({
ref={ref}
>
<div className="qb-content-section">
{!showOnlyWhereClause && (
{isMultiQueryAllowed && (
<div className="qb-header-container">
<div className="query-actions-container">
<div className="query-actions-left-container">
<QBEntityOptions
hasTraceOperator={
showTraceOperator ||
(isListViewPanel && dataSource === DataSource.TRACES)
}
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
(version && version === ENTITY_VERSION_V4) ||
@ -139,7 +145,30 @@ export const QueryV2 = memo(function QueryV2({
/>
</div>
{!isListViewPanel && (
{!isCollapsed &&
(showTraceOperator ||
(isListViewPanel && dataSource === DataSource.TRACES)) && (
<div className="qb-search-filter-container" style={{ flex: 1 }}>
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
{isMultiQueryAllowed && (
<Dropdown
className="query-actions-dropdown"
menu={{
@ -181,28 +210,32 @@ export const QueryV2 = memo(function QueryV2({
</div>
)}
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{!showTraceOperator &&
!(isListViewPanel && dataSource === DataSource.TRACES) && (
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
!showTraceOperator &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
@ -225,7 +258,7 @@ export const QueryV2 = memo(function QueryV2({
/>
)}
{!showOnlyWhereClause && (
{!showOnlyWhereClause && !isListViewPanel && !showTraceOperator && (
<QueryAddOns
index={index}
query={query}

View File

@ -0,0 +1,180 @@
.qb-trace-operator {
padding: 8px;
display: flex;
gap: 8px;
&.non-list-view {
padding-left: 40px;
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 12px;
height: calc(100% - 48px);
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
&-span-source-label {
display: flex;
align-items: center;
gap: 8px;
height: 24px;
&-query {
font-size: 14px;
font-weight: 400;
color: var(--bg-vanilla-100);
}
&-query-name {
width: 18px;
height: 18px;
display: grid;
place-content: center;
padding: 2px;
border-radius: 2px;
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
color: var(--Sakura-400, #f56c87);
font-size: 12px;
}
}
&-arrow {
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: -26px;
height: 1px;
width: 20px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
&::after {
content: '';
position: absolute;
top: 50%;
left: -10px;
transform: translateY(-50%);
height: 4px;
width: 4px;
border-radius: 50%;
background-color: var(--bg-slate-400);
}
}
&-input {
width: 100%;
}
&-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
&-aggregation-container {
display: flex;
flex-direction: column;
gap: 8px;
}
&-add-ons-container {
width: 100%;
display: flex;
flex-direction: row;
gap: 16px;
}
&-add-ons-input {
position: relative;
display: flex;
align-items: center;
flex-direction: row;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
&::before {
content: '';
position: absolute;
left: -16px;
top: 50%;
height: 1px;
width: 16px;
background-color: var(--bg-slate-400);
}
.label {
color: var(--bg-vanilla-400);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 8px;
border-right: 1px solid var(--bg-slate-400);
}
}
}
.lightMode {
.qb-trace-operator {
&-arrow {
&::before {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
&::after {
background-color: var(--bg-vanilla-300);
}
}
&.non-list-view {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
&-add-ons-input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
.label {
color: var(--bg-ink-500) !important;
border-right: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@ -0,0 +1,157 @@
/* eslint-disable react/require-default-props */
/* eslint-disable sonarjs/no-duplicate-string */
import './TraceOperator.styles.scss';
import { Button, Select, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Trash2 } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAddOns from '../QueryAddOns/QueryAddOns';
import QueryAggregation from '../QueryAggregation/QueryAggregation';
export default function TraceOperator({
traceOperator,
isListViewPanel = false,
}: {
traceOperator: IBuilderTraceOperator;
isListViewPanel?: boolean;
}): JSX.Element {
const { panelType, currentQuery, removeTraceOperator } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: traceOperator,
entityVersion: '',
isForTraceOperator: true,
});
const handleTraceOperatorChange = useCallback(
(traceOperatorExpression: string) => {
handleChangeQueryData('expression', traceOperatorExpression);
},
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
(value: string) => {
handleChangeQueryData('aggregations', [
{
expression: value,
},
]);
},
[handleChangeQueryData],
);
const handleChangeSpanSource = useCallback(
(value: string) => {
handleChangeQueryData('returnSpansFrom', value);
},
[handleChangeQueryData],
);
const defaultSpanSource = useMemo(
() =>
traceOperator.returnSpansFrom ||
currentQuery.builder.queryData[0].queryName ||
'',
[currentQuery.builder.queryData, traceOperator?.returnSpansFrom],
);
const spanSourceOptions = useMemo(
() =>
currentQuery.builder.queryData.map((query) => ({
value: query.queryName,
label: (
<div className="qb-trace-operator-span-source-label">
<span className="qb-trace-operator-span-source-label-query">Query</span>
<p className="qb-trace-operator-span-source-label-query-name">
{query.queryName}
</p>
</div>
),
})),
[currentQuery.builder.queryData],
);
return (
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
<div className="qb-trace-operator-container">
<InputWithLabel
className={cx(
'qb-trace-operator-input',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
initialValue={traceOperator?.expression || ''}
label="TRACES MATCHING"
placeholder="Add condition..."
type="text"
onChange={handleTraceOperatorChange}
/>
{!isListViewPanel && (
<div className="qb-trace-operator-aggregation-container">
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
<QueryAggregation
dataSource={DataSource.TRACES}
key={`query-search-${traceOperator.queryName}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={traceOperator}
/>
</div>
<div
className={cx(
'qb-trace-operator-add-ons-container',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<QueryAddOns
index={0}
query={traceOperator}
version="v3"
isForTraceOperator
isListViewPanel={false}
showReduceTo={false}
panelType={panelType}
>
<div className="qb-trace-operator-add-ons-input">
<Typography.Text className="label">Using spans from</Typography.Text>
<Select
bordered={false}
defaultValue={defaultSpanSource}
style={{ minWidth: 120 }}
onChange={handleChangeSpanSource}
options={spanSourceOptions}
listItemHeight={24}
/>
</div>
</QueryAddOns>
</div>
</div>
)}
</div>
<Tooltip title="Remove Trace Operator" placement="topLeft">
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
<Trash2 size={14} />
</Button>
</Tooltip>
</div>
);
}

View File

@ -17,6 +17,19 @@
font-weight: var(--font-weight-normal);
}
.view-title-container {
display: flex;
align-items: center;
gap: 6px;
justify-content: center;
.icon-container {
display: flex;
align-items: center;
justify-content: center;
}
}
.tab {
border: 1px solid var(--bg-slate-400);
&:hover {

View File

@ -6,6 +6,7 @@ import { RadioChangeEvent } from 'antd/es/radio';
interface Option {
value: string;
label: string;
icon?: React.ReactNode;
}
interface SignozRadioGroupProps {
@ -37,7 +38,10 @@ function SignozRadioGroup({
value={option.value}
className={value === option.value ? 'selected_view tab' : 'tab'}
>
{option.label}
<div className="view-title-container">
{option.icon && <div className="icon-container">{option.icon}</div>}
{option.label}
</div>
</Radio.Button>
))}
</Radio.Group>

View File

@ -12,6 +12,7 @@ import {
HavingForm,
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@ -50,6 +51,8 @@ import {
export const MAX_FORMULAS = 20;
export const MAX_QUERIES = 26;
export const TRACE_OPERATOR_QUERY_NAME = 'T1';
export const idDivider = '--';
export const selectValueDivider = '__';
@ -265,6 +268,11 @@ export const initialFormulaBuilderFormValues: IBuilderFormula = {
legend: '',
};
export const initialQueryBuilderFormTraceOperatorValues: IBuilderTraceOperator = {
...initialQueryBuilderFormTracesValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
};
export const initialQueryPromQLData: IPromQLQuery = {
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
query: '',
@ -282,6 +290,7 @@ export const initialClickHouseData: IClickHouseQuery = {
export const initialQueryBuilderData: QueryBuilderData = {
queryData: [initialQueryBuilderFormValues],
queryFormulas: [],
queryTraceOperator: [],
};
export const initialSingleQueryMap: Record<

View File

@ -54,6 +54,7 @@ function QuerySection({
queryVariant: 'static',
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
}}
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
showFunctions={
(alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4) ||

View File

@ -150,6 +150,7 @@ function FormAlertRules({
const queryOptions = useMemo(() => {
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
// TODO: Filter out queries who are used in trace operator
[EQueryType.QUERY_BUILDER]: () => [
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),

View File

@ -30,5 +30,14 @@ export type QueryBuilderProps = {
isListViewPanel?: boolean;
showFunctions?: boolean;
showOnlyWhereClause?: boolean;
showOnlyTraceOperator?: boolean;
showTraceViewSelector?: boolean;
showTraceOperator?: boolean;
version: string;
onChangeTraceView?: (view: TraceView) => void;
};
export enum TraceView {
SPANS = 'spans',
TRACES = 'traces',
}

View File

@ -39,6 +39,7 @@ interface QBEntityOptionsProps {
showCloneOption?: boolean;
isListViewPanel?: boolean;
index?: number;
hasTraceOperator?: boolean;
queryVariant?: 'dropdown' | 'static';
onChangeDataSource?: (value: DataSource) => void;
}
@ -61,6 +62,7 @@ export default function QBEntityOptions({
onCloneQuery,
index,
queryVariant,
hasTraceOperator = false,
onChangeDataSource,
}: QBEntityOptionsProps): JSX.Element {
const handleCloneEntity = (): void => {
@ -97,7 +99,7 @@ export default function QBEntityOptions({
value="query-builder"
className="periscope-btn visibility-toggle"
onClick={onToggleVisibility}
disabled={isListViewPanel}
disabled={isListViewPanel && query?.dataSource !== DataSource.TRACES}
>
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
</Button>
@ -115,6 +117,7 @@ export default function QBEntityOptions({
className={cx(
'periscope-btn',
entityType === 'query' ? 'query-name' : 'formula-name',
hasTraceOperator && 'has-trace-operator',
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
)}
>

View File

@ -11,5 +11,7 @@ export type QueryProps = {
version: string;
showSpanScopeSelector?: boolean;
showOnlyWhereClause?: boolean;
showTraceOperator?: boolean;
signalSource?: string;
isMultiQueryAllowed?: boolean;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@ -37,11 +37,15 @@ function QuerySection(): JSX.Element {
};
}, [panelTypes, renderOrderBy]);
const isListViewPanel = useMemo(
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
[panelTypes],
);
return (
<QueryBuilderV2
isListViewPanel={
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
}
isListViewPanel={isListViewPanel}
showTraceOperator
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
queryComponents={queryComponents}
panelType={panelTypes}

View File

@ -54,9 +54,11 @@ export const useQueryOperations: UseQueryOperations = ({
formula,
isListViewPanel = false,
entityVersion,
isForTraceOperator = false,
}) => {
const {
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
removeQueryBuilderEntityByIndex,
panelType,
@ -400,9 +402,19 @@ export const useQueryOperations: UseQueryOperations = ({
: value,
};
handleSetQueryData(index, newQuery);
if (isForTraceOperator) {
handleSetTraceOperatorData(index, newQuery);
} else {
handleSetQueryData(index, newQuery);
}
},
[query, index, handleSetQueryData],
[
query,
index,
handleSetQueryData,
handleSetTraceOperatorData,
isForTraceOperator,
],
);
const handleChangeFormulaData: HandleChangeFormulaData = useCallback(

View File

@ -53,7 +53,6 @@ function TracesExplorer(): JSX.Element {
handleRunQuery,
stagedQuery,
handleSetConfig,
updateQueriesData,
} = useQueryBuilder();
const { options } = useOptionsMenu({
@ -112,48 +111,14 @@ function TracesExplorer(): JSX.Element {
handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES);
}
if (view === ExplorerViews.LIST) {
if (
selectedView !== ExplorerViews.LIST &&
currentQuery?.builder?.queryData?.[0]
) {
const filterToRetain = currentQuery.builder.queryData[0].filter;
const newDefaultQuery = updateAllQueriesOperators(
initialQueriesMap.traces,
PANEL_TYPES.LIST,
DataSource.TRACES,
);
const newListQuery = updateQueriesData(
newDefaultQuery,
'queryData',
(item, index) => {
if (index === 0) {
return { ...item, filter: filterToRetain };
}
return item;
},
);
setDefaultQuery(newListQuery);
}
setShouldReset(true);
}
// TODO: remove formula when switching to List view
setSelectedView(view);
handleExplorerTabChange(
view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view,
);
},
[
handleSetConfig,
handleExplorerTabChange,
selectedView,
currentQuery,
updateAllQueriesOperators,
updateQueriesData,
setSelectedView,
],
[handleSetConfig, handleExplorerTabChange, selectedView, setSelectedView],
);
const listQuery = useMemo(() => {

View File

@ -7,6 +7,7 @@ import {
initialClickHouseData,
initialFormulaBuilderFormValues,
initialQueriesMap,
initialQueryBuilderFormTraceOperatorValues,
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
initialQueryState,
@ -14,6 +15,7 @@ import {
MAX_FORMULAS,
MAX_QUERIES,
PANEL_TYPES,
TRACE_OPERATOR_QUERY_NAME,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
@ -47,6 +49,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@ -75,14 +78,18 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
handleSetQueryData: () => {},
handleSetTraceOperatorData: () => {},
handleSetFormulaData: () => {},
handleSetQueryItemData: () => {},
handleSetConfig: () => {},
removeQueryBuilderEntityByIndex: () => {},
removeAllQueryBuilderEntities: () => {},
removeQueryTypeItemByIndex: () => {},
addNewBuilderQuery: () => {},
cloneQuery: () => {},
addNewFormula: () => {},
addTraceOperator: () => {},
removeTraceOperator: () => {},
addNewQueryItem: () => {},
redirectWithQueryBuilderData: () => {},
handleRunQuery: () => {},
@ -173,6 +180,10 @@ export function QueryBuilderProvider({
...initialFormulaBuilderFormValues,
...item,
})),
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
...initialQueryBuilderFormTraceOperatorValues,
...item,
})),
};
const setupedQueryData = builder.queryData.map((item) => {
@ -385,8 +396,11 @@ export function QueryBuilderProvider({
const removeQueryBuilderEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setCurrentQuery((prevState) => {
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@ -400,8 +414,11 @@ export function QueryBuilderProvider({
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@ -417,6 +434,20 @@ export function QueryBuilderProvider({
[],
);
const removeAllQueryBuilderEntities = useCallback(
(type: keyof QueryBuilderData) => {
setCurrentQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
setSupersetQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
},
[setCurrentQuery, setSupersetQuery],
);
const removeQueryTypeItemByIndex = useCallback(
(type: EQueryType.PROM | EQueryType.CLICKHOUSE, index: number) => {
setCurrentQuery((prevState) => {
@ -639,6 +670,68 @@ export function QueryBuilderProvider({
});
}, [createNewBuilderFormula]);
const addTraceOperator = useCallback((expression = '') => {
const trimmed = (expression || '').trim();
setCurrentQuery((prevState) => {
const existing = prevState.builder.queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
return {
...prevState,
builder: {
...prevState.builder,
// enforce single trace operator and replace only expression
queryTraceOperator: [updated],
},
};
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const existing = prevState.builder.queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
return {
...prevState,
builder: {
...prevState.builder,
// enforce single trace operator and replace only expression
queryTraceOperator: [updated],
},
};
});
}, []);
const removeTraceOperator = useCallback(() => {
setCurrentQuery((prevState) => ({
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: [],
},
}));
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => ({
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: [],
},
}));
}, []);
const updateQueryBuilderData: <T>(
arr: T[],
index: number,
@ -745,6 +838,44 @@ export function QueryBuilderProvider({
},
[updateQueryBuilderData, updateSuperSetQueryBuilderData],
);
const handleSetTraceOperatorData = useCallback(
(index: number, traceOperatorData: IBuilderTraceOperator): void => {
setCurrentQuery((prevState) => {
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
prevState.builder.queryTraceOperator,
index,
traceOperatorData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: updatedTraceOperatorBuilderData,
},
};
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
prevState.builder.queryTraceOperator,
index,
traceOperatorData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: updatedTraceOperatorBuilderData,
},
};
});
},
[updateQueryBuilderData],
);
const handleSetFormulaData = useCallback(
(index: number, formulaData: IBuilderFormula): void => {
setCurrentQuery((prevState) => {
@ -1045,14 +1176,18 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,
@ -1073,14 +1208,18 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,

View File

@ -29,6 +29,10 @@ export interface IBuilderFormula {
orderBy?: OrderByPayload[];
}
export type IBuilderTraceOperator = IBuilderQuery & {
returnSpansFrom?: string;
};
export interface TagFilterItem {
id: string;
key?: BaseAutocompleteData;
@ -124,6 +128,7 @@ export type BuilderQueryDataResourse = Record<
export type MapData =
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
| IClickHouseQuery
| IPromQLQuery;

View File

@ -14,6 +14,7 @@ export type RequestType =
export type QueryType =
| 'builder_query'
| 'builder_trace_operator'
| 'builder_formula'
| 'builder_sub_query'
| 'builder_join'

View File

@ -4,6 +4,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
@ -18,6 +19,7 @@ import { SelectOption } from './select';
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
Pick<QueryBuilderProps, 'filterConfigs'> & {
isForTraceOperator?: boolean;
formula?: IBuilderFormula;
isListViewPanel?: boolean;
entityVersion: string;
@ -32,6 +34,14 @@ export type HandleChangeQueryData<T = IBuilderQuery> = <
value: Value,
) => void;
export type HandleChangeTraceOperatorData<T = IBuilderTraceOperator> = <
Key extends keyof T,
Value extends T[Key]
>(
key: Key,
value: Value,
) => void;
// Legacy version for backward compatibility
export type HandleChangeQueryDataLegacy = HandleChangeQueryData<IBuilderQuery>;

View File

@ -6,6 +6,7 @@ import { Dispatch, SetStateAction } from 'react';
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@ -222,6 +223,7 @@ export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
export type QueryBuilderData = {
queryData: IBuilderQuery[];
queryFormulas: IBuilderFormula[];
queryTraceOperator: IBuilderTraceOperator[];
};
export type QueryBuilderContextType = {
@ -235,6 +237,10 @@ export type QueryBuilderContextType = {
panelType: PANEL_TYPES | null;
isEnabledQuery: boolean;
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
handleSetTraceOperatorData: (
index: number,
traceOperatorData: IBuilderTraceOperator,
) => void;
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
handleSetQueryItemData: (
index: number,
@ -249,12 +255,15 @@ export type QueryBuilderContextType = {
type: keyof QueryBuilderData,
index: number,
) => void;
removeAllQueryBuilderEntities: (type: keyof QueryBuilderData) => void;
removeQueryTypeItemByIndex: (
type: EQueryType.PROM | EQueryType.CLICKHOUSE,
index: number,
) => void;
addNewBuilderQuery: () => void;
addNewFormula: () => void;
removeTraceOperator: () => void;
addTraceOperator: (expression?: string) => void;
cloneQuery: (type: string, query: IBuilderQuery) => void;
addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void;
redirectWithQueryBuilderData: (

View File

@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@ -97,7 +98,12 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
return
}
r = r.WithContext(ctx)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("auth_type", "api_key")
comment.Set("user_id", claims.UserID)
comment.Set("org_id", claims.OrgID)
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
next.ServeHTTP(w, r)

View File

@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@ -50,7 +51,12 @@ func (a *Auth) Wrap(next http.Handler) http.Handler {
return
}
r = r.WithContext(ctx)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("auth_type", "jwt")
comment.Set("user_id", claims.UserID)
comment.Set("org_id", claims.OrgID)
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
next.ServeHTTP(w, r)
})

View File

@ -0,0 +1,24 @@
package middleware
import (
"net/http"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
)
type Comment struct{}
func NewComment() *Comment {
return &Comment{}
}
func (middleware *Comment) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
comment := ctxtypes.CommentFromContext(req.Context())
comment.Merge(ctxtypes.CommentFromHTTPRequest(req))
req = req.WithContext(ctxtypes.NewContextWithComment(req.Context(), comment))
next.ServeHTTP(rw, req)
})
}

View File

@ -2,16 +2,11 @@ package middleware
import (
"bytes"
"context"
"log/slog"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
@ -55,9 +50,6 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
string(semconv.HTTPRouteKey), path,
}
logCommentKVs := middleware.getLogCommentKVs(req)
req = req.WithContext(context.WithValue(req.Context(), common.LogCommentKey, logCommentKVs))
badResponseBuffer := new(bytes.Buffer)
writer := newBadResponseLoggingWriter(rw, badResponseBuffer)
next.ServeHTTP(writer, req)
@ -85,67 +77,3 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
}
})
}
func (middleware *Logging) getLogCommentKVs(r *http.Request) map[string]string {
referrer := r.Header.Get("Referer")
var path, dashboardID, alertID, page, client, viewName, tab string
if referrer != "" {
referrerURL, _ := url.Parse(referrer)
client = "browser"
path = referrerURL.Path
if strings.Contains(path, "/dashboard") {
// Split the path into segments
pathSegments := strings.Split(referrerURL.Path, "/")
// The dashboard ID should be the segment after "/dashboard/"
// Loop through pathSegments to find "dashboard" and then take the next segment as the ID
for i, segment := range pathSegments {
if segment == "dashboard" && i < len(pathSegments)-1 {
// Return the next segment, which should be the dashboard ID
dashboardID = pathSegments[i+1]
}
}
page = "dashboards"
} else if strings.Contains(path, "/alerts") {
urlParams := referrerURL.Query()
alertID = urlParams.Get("ruleId")
page = "alerts"
} else if strings.Contains(path, "logs") && strings.Contains(path, "explorer") {
page = "logs-explorer"
viewName = referrerURL.Query().Get("viewName")
} else if strings.Contains(path, "/trace") || strings.Contains(path, "traces-explorer") {
page = "traces-explorer"
viewName = referrerURL.Query().Get("viewName")
} else if strings.Contains(path, "/services") {
page = "services"
tab = referrerURL.Query().Get("tab")
if tab == "" {
tab = "OVER_METRICS"
}
} else if strings.Contains(path, "/metrics") {
page = "metrics-explorer"
}
} else {
client = "api"
}
var email string
claims, err := authtypes.ClaimsFromContext(r.Context())
if err == nil {
email = claims.Email
}
kvs := map[string]string{
"path": path,
"dashboardID": dashboardID,
"alertID": alertID,
"source": page,
"client": client,
"viewName": viewName,
"servicesTab": tab,
"email": email,
}
return kvs
}

View File

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"net/http"
"regexp"
"runtime/debug"
"github.com/SigNoz/signoz/pkg/analytics"
@ -12,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/variables"
@ -166,49 +166,9 @@ func (a *API) logEvent(ctx context.Context, referrer string, event *qbtypes.QBEv
return
}
properties["referrer"] = referrer
logsExplorerMatched, _ := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
traceExplorerMatched, _ := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
metricsExplorerMatched, _ := regexp.MatchString(`/metrics-explorer/explorer(?:\?.*)?$`, referrer)
dashboardMatched, _ := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
alertMatched, _ := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
switch {
case dashboardMatched:
properties["module_name"] = "dashboard"
case alertMatched:
properties["module_name"] = "rule"
case metricsExplorerMatched:
properties["module_name"] = "metrics-explorer"
case logsExplorerMatched:
properties["module_name"] = "logs-explorer"
case traceExplorerMatched:
properties["module_name"] = "traces-explorer"
default:
return
}
if dashboardMatched {
if dashboardIDRegex, err := regexp.Compile(`/dashboard/([a-f0-9\-]+)/`); err == nil {
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
properties["dashboard_id"] = matches[1]
}
}
if widgetIDRegex, err := regexp.Compile(`widgetId=([a-f0-9\-]+)`); err == nil {
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
properties["widget_id"] = matches[1]
}
}
}
if alertMatched {
if alertIDRegex, err := regexp.Compile(`ruleId=(\d+)`); err == nil {
if matches := alertIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
properties["rule_id"] = matches[1]
}
}
comments := ctxtypes.CommentFromContext(ctx).Map()
for key, value := range comments {
properties[key] = value
}
if !event.HasData {

View File

@ -3640,28 +3640,8 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam
return seriesList, getPersonalisedError(rows.Err())
}
func logCommentKVs(ctx context.Context) map[string]string {
kv := ctx.Value(common.LogCommentKey)
if kv == nil {
return nil
}
logCommentKVs, ok := kv.(map[string]string)
if !ok {
return nil
}
return logCommentKVs
}
// GetTimeSeriesResultV3 runs the query and returns list of time series
func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query string) ([]*v3.Series, error) {
ctxArgs := map[string]interface{}{"query": query}
for k, v := range logCommentKVs(ctx) {
ctxArgs[k] = v
}
defer utils.Elapsed("GetTimeSeriesResultV3", ctxArgs)()
// Hook up query progress reporting if requested.
queryId := ctx.Value("queryId")
if queryId != nil {
@ -3725,20 +3705,12 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
// GetListResultV3 runs the query and returns list of rows
func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) {
ctxArgs := map[string]interface{}{"query": query}
for k, v := range logCommentKVs(ctx) {
ctxArgs[k] = v
}
defer utils.Elapsed("GetListResultV3", ctxArgs)()
rows, err := r.db.Query(ctx, query)
if err != nil {
zap.L().Error("error while reading time series result", zap.Error(err))
return nil, errors.New(err.Error())
}
defer rows.Close()
var (

View File

@ -220,6 +220,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
).Wrap)
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewComment().Wrap)
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())

View File

@ -1,5 +0,0 @@
package common
type LogCommentContextKeyType string
const LogCommentKey LogCommentContextKeyType = "logComment"

View File

@ -7,7 +7,7 @@ import (
"sync"
"time"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
opentracing "github.com/opentracing/opentracing-go"
@ -369,12 +369,10 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
rule.SetEvaluationTimestamp(t)
}(time.Now())
kvs := map[string]string{
"alertID": rule.ID(),
"source": "alerts",
"client": "query-service",
}
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("rule_id", rule.ID())
comment.Set("auth_type", "internal")
ctx = ctxtypes.NewContextWithComment(ctx, comment)
_, err := rule.Eval(ctx, ts)
if err != nil {

View File

@ -7,8 +7,8 @@ import (
"sync"
"time"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
opentracing "github.com/opentracing/opentracing-go"
@ -352,12 +352,10 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
rule.SetEvaluationTimestamp(t)
}(time.Now())
kvs := map[string]string{
"alertID": rule.ID(),
"source": "alerts",
"client": "query-service",
}
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("rule_id", rule.ID())
comment.Set("auth_type", "internal")
ctx = ctxtypes.NewContextWithComment(ctx, comment)
_, err := rule.Eval(ctx, ts)
if err != nil {

View File

@ -2,13 +2,12 @@ package telemetrystorehook
import (
"context"
"encoding/json"
"strings"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
)
type provider struct {
@ -32,11 +31,7 @@ func NewSettings(ctx context.Context, providerSettings factory.ProviderSettings,
func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent) context.Context {
settings := clickhouse.Settings{}
// Apply default settings
logComment := h.getLogComment(ctx)
if logComment != "" {
settings["log_comment"] = logComment
}
settings["log_comment"] = ctxtypes.CommentFromContext(ctx).String()
if ctx.Value("enforce_max_result_rows") != nil {
settings["max_result_rows"] = h.settings.MaxResultRows
@ -91,22 +86,4 @@ func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent
return ctx
}
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {
}
func (h *provider) getLogComment(ctx context.Context) string {
// Get the key-value pairs from context for log comment
kv := ctx.Value(common.LogCommentKey)
if kv == nil {
return ""
}
logCommentKVs, ok := kv.(map[string]string)
if !ok {
return ""
}
logComment, _ := json.Marshal(logCommentKVs)
return string(logComment)
}
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {}

View File

@ -0,0 +1,163 @@
package ctxtypes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"regexp"
"sync"
)
var (
logsExplorerRegex = regexp.MustCompile(`/logs/logs-explorer(?:\?.*)?$`)
traceExplorerRegex = regexp.MustCompile(`/traces-explorer(?:\?.*)?$`)
metricsExplorerRegex = regexp.MustCompile(`/metrics-explorer/explorer(?:\?.*)?$`)
dashboardRegex = regexp.MustCompile(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`)
dashboardIDRegex = regexp.MustCompile(`/dashboard/([a-f0-9\-]+)/`)
widgetIDRegex = regexp.MustCompile(`widgetId=([a-f0-9\-]+)`)
ruleRegex = regexp.MustCompile(`/alerts/(new|edit)(?:\?.*)?$`)
ruleIDRegex = regexp.MustCompile(`ruleId=(\d+)`)
)
type commentCtxKey struct{}
type Comment struct {
vals map[string]string
mtx sync.RWMutex
}
func NewContextWithComment(ctx context.Context, comment *Comment) context.Context {
return context.WithValue(ctx, commentCtxKey{}, comment)
}
func CommentFromContext(ctx context.Context) *Comment {
comment, ok := ctx.Value(commentCtxKey{}).(*Comment)
if !ok {
return NewComment()
}
// Return a deep copy of the comment to prevent mutations from affecting the original
copy := NewComment()
copy.Merge(comment.Map())
return copy
}
func CommentFromHTTPRequest(req *http.Request) map[string]string {
comments := map[string]string{}
referrer := req.Header.Get("Referer")
if referrer == "" {
return comments
}
referrerURL, err := url.Parse(referrer)
if err != nil {
return comments
}
logsExplorerMatched := logsExplorerRegex.MatchString(referrer)
traceExplorerMatched := traceExplorerRegex.MatchString(referrer)
metricsExplorerMatched := metricsExplorerRegex.MatchString(referrer)
dashboardMatched := dashboardRegex.MatchString(referrer)
ruleMatched := ruleRegex.MatchString(referrer)
switch {
case dashboardMatched:
comments["module_name"] = "dashboard"
case ruleMatched:
comments["module_name"] = "rule"
case metricsExplorerMatched:
comments["module_name"] = "metrics-explorer"
case logsExplorerMatched:
comments["module_name"] = "logs-explorer"
case traceExplorerMatched:
comments["module_name"] = "traces-explorer"
default:
return comments
}
if dashboardMatched {
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
comments["dashboard_id"] = matches[1]
}
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
comments["widget_id"] = matches[1]
}
}
if ruleMatched {
if matches := ruleIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
comments["rule_id"] = matches[1]
}
}
comments["http_path"] = referrerURL.Path
return comments
}
// NewComment creates a new Comment with an empty map. It is safe to use concurrently.
func NewComment() *Comment {
return &Comment{vals: map[string]string{}}
}
func (comment *Comment) Set(key, value string) {
comment.mtx.Lock()
defer comment.mtx.Unlock()
comment.vals[key] = value
}
func (comment *Comment) Merge(vals map[string]string) {
comment.mtx.Lock()
defer comment.mtx.Unlock()
// If vals is nil, do nothing. Comment should not panic.
if vals == nil {
return
}
for key, value := range vals {
comment.vals[key] = value
}
}
func (comment *Comment) Map() map[string]string {
comment.mtx.RLock()
defer comment.mtx.RUnlock()
copyOfVals := make(map[string]string)
for key, value := range comment.vals {
copyOfVals[key] = value
}
return copyOfVals
}
func (comment *Comment) String() string {
comment.mtx.RLock()
defer comment.mtx.RUnlock()
commentJSON, err := json.Marshal(comment.vals)
if err != nil {
return "{}"
}
return string(commentJSON)
}
func (comment *Comment) Equal(other *Comment) bool {
if len(comment.vals) != len(other.vals) {
return false
}
for key, value := range comment.vals {
if val, ok := other.vals[key]; !ok || val != value {
return false
}
}
return true
}

View File

@ -0,0 +1,123 @@
package ctxtypes
import (
"context"
"fmt"
"net/http"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommentFromHTTPRequest(t *testing.T) {
testCases := []struct {
name string
req *http.Request
expected map[string]string
}{
{
name: "EmptyReferer",
req: &http.Request{Header: http.Header{"Referer": {""}}},
expected: map[string]string{},
},
{
name: "ControlCharacterInReferer",
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/logs/logs-explorer\x00"}}},
expected: map[string]string{},
},
{
name: "LogsExplorer",
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/logs/logs-explorer"}}},
expected: map[string]string{"http_path": "/logs/logs-explorer", "module_name": "logs-explorer"},
},
{
name: "TracesExplorer",
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/traces-explorer"}}},
expected: map[string]string{"http_path": "/traces-explorer", "module_name": "traces-explorer"},
},
{
name: "MetricsExplorer",
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/metrics-explorer/explorer"}}},
expected: map[string]string{"http_path": "/metrics-explorer/explorer", "module_name": "metrics-explorer"},
},
{
name: "DashboardWithID",
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/dashboard/123/new"}}},
expected: map[string]string{"http_path": "/dashboard/123/new", "module_name": "dashboard", "dashboard_id": "123"},
},
{
name: "Rule",
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/alerts/new"}}},
expected: map[string]string{"http_path": "/alerts/new", "module_name": "rule"},
},
{
name: "RuleWithID",
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/alerts/edit?ruleId=123"}}},
expected: map[string]string{"http_path": "/alerts/edit", "module_name": "rule", "rule_id": "123"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := CommentFromHTTPRequest(tc.req)
assert.True(t, (&Comment{vals: tc.expected}).Equal(&Comment{vals: actual}))
})
}
}
func TestCommentFromContext(t *testing.T) {
ctx := context.Background()
comment1 := CommentFromContext(ctx)
assert.True(t, NewComment().Equal(comment1))
comment1.Set("k1", "v1")
ctx = NewContextWithComment(ctx, comment1)
actual1 := CommentFromContext(ctx)
assert.True(t, comment1.Equal(actual1))
// Get the comment from the context, mutate it, but this time do not set it back in the context
comment2 := CommentFromContext(ctx)
comment2.Set("k2", "v2")
actual2 := CommentFromContext(ctx)
// Since comment2 was not set back in the context, it should not affect the original comment1
assert.True(t, comment1.Equal(actual2))
assert.False(t, comment2.Equal(actual2))
assert.False(t, comment1.Equal(comment2))
}
func TestCommentFromContextConcurrent(t *testing.T) {
comment := NewComment()
comment.Set("k1", "v1")
ctx := context.Background()
ctx = NewContextWithComment(ctx, comment)
var wg sync.WaitGroup
ctxs := make([]context.Context, 10)
var mtx sync.Mutex
wg.Add(10)
for i := 0; i < 10; i++ {
go func(i int) {
defer wg.Done()
comment := CommentFromContext(ctx)
comment.Set("k2", fmt.Sprintf("v%d", i))
newCtx := NewContextWithComment(ctx, comment)
mtx.Lock()
ctxs[i] = newCtx
mtx.Unlock()
}(i)
}
wg.Wait()
for i, ctx := range ctxs {
comment := CommentFromContext(ctx)
assert.Equal(t, len(comment.vals), 2)
assert.Equal(t, comment.vals["k1"], "v1")
assert.Equal(t, comment.vals["k2"], fmt.Sprintf("v%d", i))
}
}