mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-30 03:00:59 +00:00
Compare commits
26 Commits
SIG_3403
...
feat/intro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9966fe5291 | ||
|
|
201d5c24a5 | ||
|
|
103bd160c8 | ||
|
|
c4dbe5822a | ||
|
|
0da94dbd4c | ||
|
|
2eb38d3ad4 | ||
|
|
c570655333 | ||
|
|
9bd37b9d01 | ||
|
|
ab8b42fbbe | ||
|
|
d777a6032a | ||
|
|
75f37664af | ||
|
|
a8aa5c0ded | ||
|
|
3351ddd8fe | ||
|
|
c773382cb4 | ||
|
|
5a6854a126 | ||
|
|
9a1319d9f8 | ||
|
|
cd9e537794 | ||
|
|
b6632f1e53 | ||
|
|
4245261299 | ||
|
|
0340b87d87 | ||
|
|
d11e60f5cc | ||
|
|
7a9d9f333c | ||
|
|
f99821bc40 | ||
|
|
7c051601f2 | ||
|
|
b9f9c00da5 | ||
|
|
49ff86e65a |
@@ -278,3 +278,13 @@ tokenizer:
|
||||
token:
|
||||
# The maximum number of tokens a user can have. This limits the number of concurrent sessions a user can have.
|
||||
max_per_user: 5
|
||||
|
||||
##################### Flagger #####################
|
||||
flagger:
|
||||
# Config are the overrides for the feature flags which come directly from the config file.
|
||||
config:
|
||||
boolean:
|
||||
string:
|
||||
float:
|
||||
integer:
|
||||
object:
|
||||
|
||||
@@ -1726,6 +1726,51 @@ paths:
|
||||
summary: Update user preference
|
||||
tags:
|
||||
- preferences
|
||||
/api/v2/features:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns the supported features and their details
|
||||
operationId: GetFeatures
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/FeaturetypesGettableFeature'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
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: Get features
|
||||
tags:
|
||||
- features
|
||||
/api/v2/orgs/me:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -2173,6 +2218,24 @@ components:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
FeaturetypesGettableFeature:
|
||||
properties:
|
||||
defaultVariant:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
resolvedValue: {}
|
||||
stage:
|
||||
type: string
|
||||
variants:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
PreferencetypesPreference:
|
||||
properties:
|
||||
allowedScopes:
|
||||
|
||||
@@ -132,11 +132,20 @@ function UplotPanelWrapper({
|
||||
[selectedGraph, widget?.panelTypes, widget?.stackedBarChart],
|
||||
);
|
||||
|
||||
const chartData = getUPlotChartData(
|
||||
queryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
stackedBarChart,
|
||||
hiddenGraph,
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getUPlotChartData(
|
||||
queryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
stackedBarChart,
|
||||
hiddenGraph,
|
||||
),
|
||||
[
|
||||
queryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
stackedBarChart,
|
||||
hiddenGraph,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -293,7 +302,7 @@ function UplotPanelWrapper({
|
||||
)}
|
||||
{isFullViewMode && setGraphVisibility && !stackedBarChart && (
|
||||
<GraphManager
|
||||
data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
|
||||
data={chartData}
|
||||
name={widget.id}
|
||||
options={options}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse } from 'react-router-dom-v5-compat';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types';
|
||||
|
||||
export const useGetQueryKeyValueSuggestions = ({
|
||||
@@ -11,13 +9,15 @@ export const useGetQueryKeyValueSuggestions = ({
|
||||
searchText,
|
||||
signalSource,
|
||||
metricName,
|
||||
options,
|
||||
}: {
|
||||
key: string;
|
||||
signal: 'traces' | 'logs' | 'metrics';
|
||||
searchText?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<QueryKeyValueSuggestionsResponseProps> | ErrorResponse
|
||||
AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
|
||||
AxiosError
|
||||
>;
|
||||
metricName?: string;
|
||||
}): UseQueryResult<
|
||||
@@ -41,4 +41,5 @@ export const useGetQueryKeyValueSuggestions = ({
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
metricName: metricName || '',
|
||||
}),
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { cloneDeep, isUndefined } from 'lodash-es';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { normalizePlotValue } from './dataUtils';
|
||||
import { generateColor } from './generateColor';
|
||||
|
||||
function getXAxisTimestamps(seriesList: QueryData[]): number[] {
|
||||
const timestamps = new Set();
|
||||
const timestamps = new Set<number>();
|
||||
|
||||
seriesList.forEach((series: { values?: [number, string][] }) => {
|
||||
if (series?.values) {
|
||||
@@ -18,54 +18,71 @@ function getXAxisTimestamps(seriesList: QueryData[]): number[] {
|
||||
}
|
||||
});
|
||||
|
||||
const timestampsArr: number[] | unknown[] = Array.from(timestamps) || [];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return timestampsArr.sort((a, b) => a - b);
|
||||
const timestampsArr = Array.from(timestamps);
|
||||
timestampsArr.sort((a, b) => a - b);
|
||||
|
||||
return timestampsArr;
|
||||
}
|
||||
|
||||
function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function fillMissingXAxisTimestamps(
|
||||
timestampArr: number[],
|
||||
data: Array<{ values?: [number, string][] }>,
|
||||
): (number | null)[][] {
|
||||
// Generate a set of all timestamps in the range
|
||||
const allTimestampsSet = new Set(timestampArr);
|
||||
const processedData = cloneDeep(data);
|
||||
const result: (number | null)[][] = [];
|
||||
|
||||
// Fill missing timestamps with null values
|
||||
processedData.forEach((entry: { values: (number | null)[][] }) => {
|
||||
const existingTimestamps = new Set(
|
||||
(entry?.values ?? []).map((value) => value[0]),
|
||||
);
|
||||
// Process each series entry
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const entry = data[i];
|
||||
if (!entry?.values) {
|
||||
result.push([]);
|
||||
} else {
|
||||
// Build Set of existing timestamps directly (avoid intermediate array)
|
||||
const existingTimestamps = new Set<number>();
|
||||
const valuesMap = new Map<number, number | null>();
|
||||
|
||||
const missingTimestamps = Array.from(allTimestampsSet).filter(
|
||||
(timestamp) => !existingTimestamps.has(timestamp),
|
||||
);
|
||||
for (let j = 0; j < entry.values.length; j++) {
|
||||
const [timestamp, value] = entry.values[j];
|
||||
existingTimestamps.add(timestamp);
|
||||
valuesMap.set(timestamp, normalizePlotValue(value));
|
||||
}
|
||||
|
||||
missingTimestamps.forEach((timestamp) => {
|
||||
const value = null;
|
||||
// Find missing timestamps by iterating Set directly (avoid Array.from + filter)
|
||||
const missingTimestamps: number[] = [];
|
||||
const allTimestampsArray = Array.from(allTimestampsSet);
|
||||
for (let k = 0; k < allTimestampsArray.length; k++) {
|
||||
const timestamp = allTimestampsArray[k];
|
||||
if (!existingTimestamps.has(timestamp)) {
|
||||
missingTimestamps.push(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
entry?.values?.push([timestamp, value]);
|
||||
});
|
||||
// Add missing timestamps to map
|
||||
for (let j = 0; j < missingTimestamps.length; j++) {
|
||||
valuesMap.set(missingTimestamps[j], null);
|
||||
}
|
||||
|
||||
entry?.values?.forEach((v) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v[1] = normalizePlotValue(v[1]);
|
||||
});
|
||||
// Build sorted array of values
|
||||
const sortedTimestamps = Array.from(valuesMap.keys()).sort((a, b) => a - b);
|
||||
const yValues = sortedTimestamps.map((timestamp) => {
|
||||
const value = valuesMap.get(timestamp);
|
||||
return value !== undefined ? value : null;
|
||||
});
|
||||
result.push(yValues);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
entry?.values?.sort((a, b) => a[0] - b[0]);
|
||||
});
|
||||
|
||||
return processedData.map((entry: { values: [number, string][] }) =>
|
||||
entry?.values?.map((value) => value[1]),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
function getStackedSeries(val: any): any {
|
||||
const series = cloneDeep(val) || [];
|
||||
function getStackedSeries(val: (number | null)[][]): (number | null)[][] {
|
||||
const series = val ? val.map((row: (number | null)[]) => [...row]) : [];
|
||||
|
||||
for (let i = series.length - 2; i >= 0; i--) {
|
||||
for (let j = 0; j < series[i].length; j++) {
|
||||
series[i][j] += series[i + 1][j];
|
||||
series[i][j] = (series[i][j] || 0) + (series[i + 1][j] || 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +127,7 @@ const processAnomalyDetectionData = (
|
||||
queryIndex < anomalyDetectionData.length;
|
||||
queryIndex++
|
||||
) {
|
||||
const queryData = anomalyDetectionData[queryIndex];
|
||||
const {
|
||||
series,
|
||||
predictedSeries,
|
||||
@@ -117,7 +135,7 @@ const processAnomalyDetectionData = (
|
||||
lowerBoundSeries,
|
||||
queryName,
|
||||
legend,
|
||||
} = anomalyDetectionData[queryIndex];
|
||||
} = queryData;
|
||||
|
||||
for (let index = 0; index < series?.length; index++) {
|
||||
const label = getLabelName(
|
||||
@@ -129,14 +147,30 @@ const processAnomalyDetectionData = (
|
||||
const objKey =
|
||||
anomalyDetectionData.length > 1 ? `${queryName}-${label}` : label;
|
||||
|
||||
// Single iteration instead of 5 separate map operations
|
||||
const { values: seriesValues } = series[index];
|
||||
const { values: predictedValues } = predictedSeries[index];
|
||||
const { values: upperBoundValues } = upperBoundSeries[index];
|
||||
const { values: lowerBoundValues } = lowerBoundSeries[index];
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const length = seriesValues.length;
|
||||
|
||||
const timestamps: number[] = new Array(length);
|
||||
const values: number[] = new Array(length);
|
||||
const predicted: number[] = new Array(length);
|
||||
const upperBound: number[] = new Array(length);
|
||||
const lowerBound: number[] = new Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
timestamps[i] = seriesValues[i].timestamp / 1000;
|
||||
values[i] = seriesValues[i].value;
|
||||
predicted[i] = predictedValues[i].value;
|
||||
upperBound[i] = upperBoundValues[i].value;
|
||||
lowerBound[i] = lowerBoundValues[i].value;
|
||||
}
|
||||
|
||||
processedData[objKey] = {
|
||||
data: [
|
||||
series[index].values.map((v: { timestamp: number }) => v.timestamp / 1000),
|
||||
series[index].values.map((v: { value: number }) => v.value),
|
||||
predictedSeries[index].values.map((v: { value: number }) => v.value),
|
||||
upperBoundSeries[index].values.map((v: { value: number }) => v.value),
|
||||
lowerBoundSeries[index].values.map((v: { value: number }) => v.value),
|
||||
],
|
||||
data: [timestamps, values, predicted, upperBound, lowerBound],
|
||||
color: generateColor(
|
||||
objKey,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
@@ -152,14 +186,7 @@ const processAnomalyDetectionData = (
|
||||
export const getUplotChartDataForAnomalyDetection = (
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
isDarkMode: boolean,
|
||||
): Record<
|
||||
string,
|
||||
{
|
||||
[x: string]: any;
|
||||
data: number[][];
|
||||
color: string;
|
||||
}
|
||||
> => {
|
||||
): Record<string, { [x: string]: any; data: number[][]; color: string }> => {
|
||||
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
|
||||
return processAnomalyDetectionData(anomalyDetectionData, isDarkMode);
|
||||
};
|
||||
|
||||
16
go.mod
16
go.mod
@@ -74,12 +74,12 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/text v0.28.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/text v0.32.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -103,6 +103,7 @@ require (
|
||||
go.opentelemetry.io/collector/config/configretry v1.34.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
@@ -223,6 +224,7 @@ require (
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/open-feature/go-sdk v1.17.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.128.0 // indirect
|
||||
@@ -336,10 +338,10 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
google.golang.org/api v0.236.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -762,6 +762,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk=
|
||||
github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw=
|
||||
github.com/open-telemetry/opamp-go v0.19.0 h1:8LvQKDwqi+BU3Yy159SU31e2XB0vgnk+PN45pnKilPs=
|
||||
github.com/open-telemetry/opamp-go v0.19.0/go.mod h1:9/1G6T5dnJz4cJtoYSr6AX18kHdOxnxxETJPZSHyEUg=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.128.0 h1:T5IE0l1qcIg6dkHui4hHe+qj3VzuMwpnhrUyubyCwO0=
|
||||
@@ -1282,8 +1284,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1321,8 +1323,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1371,8 +1373,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1407,8 +1409,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1495,12 +1497,12 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1511,8 +1513,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1575,8 +1577,10 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -80,6 +80,17 @@ func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, er
|
||||
|
||||
name := r.URL.Query().Get("searchText")
|
||||
|
||||
if name != "" && fieldContext == telemetrytypes.FieldContextUnspecified {
|
||||
parsedFieldKey := telemetrytypes.GetFieldKeyFromKeyText(name)
|
||||
if parsedFieldKey.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(parsedFieldKey.FieldContext, signal) {
|
||||
name = parsedFieldKey.Name
|
||||
fieldContext = parsedFieldKey.FieldContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req = telemetrytypes.FieldKeySelector{
|
||||
StartUnixMilli: startUnixMilli,
|
||||
EndUnixMilli: endUnixMilli,
|
||||
@@ -102,6 +113,16 @@ func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector
|
||||
}
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
if name != "" && keySelector.FieldContext == telemetrytypes.FieldContextUnspecified {
|
||||
parsedFieldKey := telemetrytypes.GetFieldKeyFromKeyText(name)
|
||||
if parsedFieldKey.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(parsedFieldKey.FieldContext, keySelector.Signal) {
|
||||
name = parsedFieldKey.Name
|
||||
keySelector.FieldContext = parsedFieldKey.FieldContext
|
||||
}
|
||||
}
|
||||
}
|
||||
keySelector.Name = name
|
||||
existingQuery := r.URL.Query().Get("existingQuery")
|
||||
value := r.URL.Query().Get("searchText")
|
||||
@@ -121,3 +142,21 @@ func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func isContextValidForSignal(ctx telemetrytypes.FieldContext, signal telemetrytypes.Signal) bool {
|
||||
if ctx == telemetrytypes.FieldContextResource ||
|
||||
ctx == telemetrytypes.FieldContextAttribute ||
|
||||
ctx == telemetrytypes.FieldContextScope {
|
||||
return true
|
||||
}
|
||||
|
||||
switch signal.StringValue() {
|
||||
case telemetrytypes.SignalLogs.StringValue():
|
||||
return ctx == telemetrytypes.FieldContextLog || ctx == telemetrytypes.FieldContextBody
|
||||
case telemetrytypes.SignalTraces.StringValue():
|
||||
return ctx == telemetrytypes.FieldContextSpan || ctx == telemetrytypes.FieldContextEvent || ctx == telemetrytypes.FieldContextTrace
|
||||
case telemetrytypes.SignalMetrics.StringValue():
|
||||
return ctx == telemetrytypes.FieldContextMetric
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
31
pkg/apiserver/signozapiserver/flagger.go
Normal file
31
pkg/apiserver/signozapiserver/flagger.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addFlaggerRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/features", handler.New(provider.authZ.ViewAccess(provider.flaggerHandler.GetFeatures), handler.OpenAPIDef{
|
||||
ID: "GetFeatures",
|
||||
Tags: []string{"features"},
|
||||
Summary: "Get features",
|
||||
Description: "This endpoint returns the supported features and their details",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*featuretypes.GettableFeature, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
@@ -32,6 +33,7 @@ type provider struct {
|
||||
preferenceHandler preference.Handler
|
||||
globalHandler global.Handler
|
||||
promoteHandler promote.Handler
|
||||
flaggerHandler flagger.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -44,9 +46,10 @@ func NewFactory(
|
||||
preferenceHandler preference.Handler,
|
||||
globalHandler global.Handler,
|
||||
promoteHandler promote.Handler,
|
||||
flaggerHandler flagger.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
|
||||
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler)
|
||||
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,6 +66,7 @@ func newProvider(
|
||||
preferenceHandler preference.Handler,
|
||||
globalHandler global.Handler,
|
||||
promoteHandler promote.Handler,
|
||||
flaggerHandler flagger.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
@@ -78,6 +82,7 @@ func newProvider(
|
||||
preferenceHandler: preferenceHandler,
|
||||
globalHandler: globalHandler,
|
||||
promoteHandler: promoteHandler,
|
||||
flaggerHandler: flaggerHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -122,6 +127,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addFlaggerRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
32
pkg/flagger/config.go
Normal file
32
pkg/flagger/config.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package flagger
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/factory"
|
||||
|
||||
type Config struct {
|
||||
Config ConfigProvider `mapstructure:"config"`
|
||||
}
|
||||
|
||||
type ConfigProvider struct {
|
||||
Boolean map[string]bool `mapstructure:"boolean"`
|
||||
String map[string]string `mapstructure:"string"`
|
||||
Float map[string]float64 `mapstructure:"float"`
|
||||
Integer map[string]int64 `mapstructure:"integer"`
|
||||
Object map[string]any `mapstructure:"object"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(
|
||||
factory.MustNewName("flagger"), newConfig,
|
||||
)
|
||||
}
|
||||
|
||||
// newConfig creates a new config with the default values.
|
||||
func newConfig() factory.Config {
|
||||
return &Config{
|
||||
Config: ConfigProvider{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
320
pkg/flagger/configflagger/configflagger.go
Normal file
320
pkg/flagger/configflagger/configflagger.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package configflagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
config flagger.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
// This is the default registry that will be containing all the supported features along with there all possible variants
|
||||
registry featuretypes.Registry
|
||||
// These are the feature variants that are configured in the config file and will be used as overrides
|
||||
featureVariants map[featuretypes.Name]*featuretypes.FeatureVariant
|
||||
}
|
||||
|
||||
func NewFactory(registry featuretypes.Registry) factory.ProviderFactory[flagger.FlaggerProvider, flagger.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("config"), func(ctx context.Context, ps factory.ProviderSettings, c flagger.Config) (flagger.FlaggerProvider, error) {
|
||||
return New(ctx, ps, c, registry)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, registry featuretypes.Registry) (flagger.FlaggerProvider, error) {
|
||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger/configflagger")
|
||||
|
||||
featureVariants := make(map[featuretypes.Name]*featuretypes.FeatureVariant)
|
||||
|
||||
for name, value := range c.Config.Boolean {
|
||||
feature, _, err := registry.GetByString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variant, err := featuretypes.VariantByValue(feature, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
featureVariants[feature.Name] = variant
|
||||
}
|
||||
|
||||
for name, value := range c.Config.String {
|
||||
feature, _, err := registry.GetByString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variant, err := featuretypes.VariantByValue(feature, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
featureVariants[feature.Name] = variant
|
||||
}
|
||||
|
||||
for name, value := range c.Config.Float {
|
||||
feature, _, err := registry.GetByString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variant, err := featuretypes.VariantByValue(feature, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
featureVariants[feature.Name] = variant
|
||||
}
|
||||
|
||||
for name, value := range c.Config.Integer {
|
||||
feature, _, err := registry.GetByString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variant, err := featuretypes.VariantByValue(feature, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
featureVariants[feature.Name] = variant
|
||||
}
|
||||
|
||||
for name, value := range c.Config.Object {
|
||||
feature, _, err := registry.GetByString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variant, err := featuretypes.VariantByValue(feature, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
featureVariants[feature.Name] = variant
|
||||
}
|
||||
|
||||
return &provider{
|
||||
config: c,
|
||||
settings: settings,
|
||||
registry: registry,
|
||||
featureVariants: featureVariants,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Metadata() openfeature.Metadata {
|
||||
return openfeature.Metadata{
|
||||
Name: "config",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[bool](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: variant.Value.(bool),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[float64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: variant.Value.(float64),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[string](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: variant.Value.(string),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[int64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: variant.Value.(int64),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[any](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: variant.Value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) Hooks() []openfeature.Hook {
|
||||
return []openfeature.Hook{}
|
||||
}
|
||||
|
||||
func (p *provider) List(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
|
||||
result := make([]*featuretypes.GettableFeature, 0, len(p.featureVariants))
|
||||
|
||||
for featureName, variant := range p.featureVariants {
|
||||
feature, _, err := p.registry.Get(featureName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, &featuretypes.GettableFeature{
|
||||
Name: feature.Name.String(),
|
||||
Kind: feature.Kind.StringValue(),
|
||||
Stage: feature.Stage.StringValue(),
|
||||
Description: feature.Description,
|
||||
DefaultVariant: feature.DefaultVariant.String(),
|
||||
Variants: nil,
|
||||
ResolvedValue: variant.Value,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
282
pkg/flagger/flagger.go
Normal file
282
pkg/flagger/flagger.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package flagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// Any feature flag provider has to implement this interface.
|
||||
type FlaggerProvider interface {
|
||||
openfeature.FeatureProvider
|
||||
|
||||
// List returns all the feature flags
|
||||
List(ctx context.Context) ([]*featuretypes.GettableFeature, error)
|
||||
}
|
||||
|
||||
// This is the consumer facing interface for the Flagger service.
|
||||
type Flagger interface {
|
||||
Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, error)
|
||||
String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, error)
|
||||
Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, error)
|
||||
Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, error)
|
||||
Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, error)
|
||||
List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeature, error)
|
||||
}
|
||||
|
||||
// This is the concrete implementation of the Flagger interface.
|
||||
type flagger struct {
|
||||
registry featuretypes.Registry
|
||||
settings factory.ScopedProviderSettings
|
||||
providers map[string]FlaggerProvider
|
||||
clients map[string]*openfeature.Client
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, config Config, registry featuretypes.Registry, factories ...factory.ProviderFactory[FlaggerProvider, Config]) (Flagger, error) {
|
||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger")
|
||||
|
||||
providers := make(map[string]FlaggerProvider)
|
||||
clients := make(map[string]*openfeature.Client)
|
||||
|
||||
for _, factory := range factories {
|
||||
provider, err := factory.New(ctx, ps, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers[provider.Metadata().Name] = provider
|
||||
|
||||
openfeatureClient := openfeature.NewClient(provider.Metadata().Name)
|
||||
|
||||
if err := openfeature.SetNamedProviderAndWait(provider.Metadata().Name, provider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clients[provider.Metadata().Name] = openfeatureClient
|
||||
}
|
||||
|
||||
return &flagger{
|
||||
registry: registry,
|
||||
settings: settings,
|
||||
providers: providers,
|
||||
clients: clients,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[bool](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.BooleanValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[string](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.StringValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[float64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.FloatValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[int64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.IntValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[any](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.ObjectValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// ! for object we do not compare with the default value for now, we will figure this out better in future coming releases
|
||||
// if value != defaultValue {
|
||||
// return value, nil
|
||||
// }
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeature, error) {
|
||||
// get all the feature from the default registry
|
||||
allFeatures := f.registry.List()
|
||||
|
||||
// make a map of name of feature -> the dict we want to create from all features
|
||||
featureMap := make(map[string]*featuretypes.GettableFeature, len(allFeatures))
|
||||
|
||||
for _, feature := range allFeatures {
|
||||
variants := make(map[string]any, len(feature.Variants))
|
||||
for name, value := range feature.Variants {
|
||||
variants[name.String()] = value.Value
|
||||
}
|
||||
|
||||
featureMap[feature.Name.String()] = &featuretypes.GettableFeature{
|
||||
Name: feature.Name.String(),
|
||||
Kind: feature.Kind.StringValue(),
|
||||
Stage: feature.Stage.StringValue(),
|
||||
Description: feature.Description,
|
||||
DefaultVariant: feature.DefaultVariant.String(),
|
||||
Variants: variants,
|
||||
ResolvedValue: feature.Variants[feature.DefaultVariant].Value,
|
||||
}
|
||||
}
|
||||
|
||||
// now call each provider and fix the value in feature map
|
||||
for _, provider := range f.providers {
|
||||
pFeatures, err := provider.List(ctx)
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get features from provider", "error", err, "provider", provider.Metadata().Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// merge
|
||||
for _, pFeature := range pFeatures {
|
||||
if existing, ok := featureMap[pFeature.Name]; ok {
|
||||
existing.ResolvedValue = pFeature.ResolvedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]*featuretypes.GettableFeature, 0, len(allFeatures))
|
||||
|
||||
for _, f := range featureMap {
|
||||
result = append(result, f)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
53
pkg/flagger/handler.go
Normal file
53
pkg/flagger/handler.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package flagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
GetFeatures(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
flagger Flagger
|
||||
}
|
||||
|
||||
func NewHandler(flagger Flagger) Handler {
|
||||
return &handler{
|
||||
flagger: flagger,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *handler) GetFeatures(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
|
||||
features, err := handler.flagger.List(ctx, evalCtx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, features)
|
||||
}
|
||||
12
pkg/flagger/registry.go
Normal file
12
pkg/flagger/registry.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package flagger
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
registry, err := featuretypes.NewRegistry()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
@@ -520,7 +520,7 @@ func (h *HostsRepo) GetHostList(ctx context.Context, orgID valuer.UUID, req mode
|
||||
if _, ok := hostAttrs[record.HostName]; ok {
|
||||
record.Meta = hostAttrs[record.HostName]
|
||||
}
|
||||
if osType, ok := record.Meta["os_type"]; ok {
|
||||
if osType, ok := record.Meta[GetDotMetrics("os_type")]; ok {
|
||||
record.OS = osType
|
||||
}
|
||||
record.Active = activeHosts[record.HostName]
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
@@ -105,6 +106,9 @@ type Config struct {
|
||||
|
||||
// MetricsExplorer config
|
||||
MetricsExplorer metricsexplorer.Config `mapstructure:"metricsexplorer"`
|
||||
|
||||
// Flagger config
|
||||
Flagger flagger.Config `mapstructure:"flagger"`
|
||||
}
|
||||
|
||||
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
|
||||
@@ -166,6 +170,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
|
||||
gateway.NewConfigFactory(),
|
||||
tokenizer.NewConfigFactory(),
|
||||
metricsexplorer.NewConfigFactory(),
|
||||
flagger.NewConfigFactory(),
|
||||
}
|
||||
|
||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||
|
||||
@@ -2,6 +2,7 @@ package signoz
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/global/signozglobal"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
@@ -37,9 +38,10 @@ type Handlers struct {
|
||||
Services services.Handler
|
||||
MetricsExplorer metricsexplorer.Handler
|
||||
Global global.Handler
|
||||
FlaggerHandler flagger.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing, global global.Global) Handlers {
|
||||
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing, global global.Global, flaggerService flagger.Flagger) Handlers {
|
||||
return Handlers{
|
||||
SavedView: implsavedview.NewHandler(modules.SavedView),
|
||||
Apdex: implapdex.NewHandler(modules.Apdex),
|
||||
@@ -51,5 +53,6 @@ func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, que
|
||||
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
|
||||
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),
|
||||
Global: signozglobal.NewHandler(global),
|
||||
FlaggerHandler: flagger.NewHandler(flaggerService),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{})
|
||||
|
||||
handlers := NewHandlers(modules, providerSettings, nil, nil, nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil)
|
||||
|
||||
reflectVal := reflect.ValueOf(handlers)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
@@ -40,6 +41,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ preference.Handler }{},
|
||||
struct{ global.Handler }{},
|
||||
struct{ promote.Handler }{},
|
||||
struct{ flagger.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/smtpemailing"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/configflagger"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/global/signozglobal"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
|
||||
@@ -54,6 +56,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/tokenizer/opaquetokenizer"
|
||||
"github.com/SigNoz/signoz/pkg/tokenizer/tokenizerstore/sqltokenizerstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/SigNoz/signoz/pkg/web/noopweb"
|
||||
@@ -236,6 +239,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
implpreference.NewHandler(modules.Preference),
|
||||
signozglobal.NewHandler(global),
|
||||
implpromote.NewHandler(modules.Promote),
|
||||
handlers.FlaggerHandler,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -253,3 +257,9 @@ func NewGlobalProviderFactories() factory.NamedMap[factory.ProviderFactory[globa
|
||||
signozglobal.NewFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
func NewFlaggerProviderFactories(registry featuretypes.Registry) factory.NamedMap[factory.ProviderFactory[flagger.FlaggerProvider, flagger.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
configflagger.NewFactory(registry),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
@@ -66,6 +67,7 @@ type SigNoz struct {
|
||||
Modules Modules
|
||||
Handlers Handlers
|
||||
QueryParser queryparser.QueryParser
|
||||
Flagger flagger.Flagger
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -356,11 +358,25 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize flagger from the available flagger provider factories
|
||||
flaggerRegistry := flagger.MustNewRegistry()
|
||||
flaggerProviderFactories := NewFlaggerProviderFactories(flaggerRegistry)
|
||||
flagger, err := flagger.New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.Flagger,
|
||||
flaggerRegistry,
|
||||
flaggerProviderFactories.GetInOrder()...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config)
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
handlers := NewHandlers(modules, providerSettings, querier, licensing, global)
|
||||
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger)
|
||||
|
||||
// Initialize the API server
|
||||
apiserver, err := factory.NewProviderFromNamedMap(
|
||||
@@ -434,5 +450,6 @@ func New(
|
||||
Modules: modules,
|
||||
Handlers: handlers,
|
||||
QueryParser: queryParser,
|
||||
Flagger: flagger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
23
pkg/types/featuretypes/context.go
Normal file
23
pkg/types/featuretypes/context.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// A concrete wrapper around the openfeature.EvaluationContext
|
||||
type FlaggerEvaluationContext struct {
|
||||
ctx openfeature.EvaluationContext
|
||||
}
|
||||
|
||||
// Creates a new FlaggerEvaluationContext with given details
|
||||
func NewFlaggerEvaluationContext(orgID valuer.UUID) FlaggerEvaluationContext {
|
||||
ctx := openfeature.NewTargetlessEvaluationContext(map[string]any{
|
||||
"orgId": orgID.String(),
|
||||
})
|
||||
return FlaggerEvaluationContext{ctx: ctx}
|
||||
}
|
||||
|
||||
func (ctx FlaggerEvaluationContext) Ctx() openfeature.EvaluationContext {
|
||||
return ctx.ctx
|
||||
}
|
||||
113
pkg/types/featuretypes/feature.go
Normal file
113
pkg/types/featuretypes/feature.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeFeatureVariantNotFound = errors.MustNewCode("feature_variant_not_found")
|
||||
ErrCodeFeatureValueNotFound = errors.MustNewCode("feature_value_not_found")
|
||||
ErrCodeFeatureVariantKindMismatch = errors.MustNewCode("feature_variant_kind_mismatch")
|
||||
ErrCodeFeatureDefaultVariantNotFound = errors.MustNewCode("feature_default_variant_not_found")
|
||||
ErrCodeFeatureNotFound = errors.MustNewCode("feature_not_found")
|
||||
)
|
||||
|
||||
// A concrete type for a feature flag
|
||||
type Feature struct {
|
||||
// Name of the feature
|
||||
Name Name `json:"name"`
|
||||
// Kind of the feature
|
||||
Kind Kind `json:"kind"`
|
||||
// Stage of the feature
|
||||
Stage Stage `json:"stage"`
|
||||
// Description of the feature
|
||||
Description string `json:"description"`
|
||||
// DefaultVariant of the feature
|
||||
DefaultVariant Name `json:"defaultVariant"`
|
||||
// Variants of the feature
|
||||
Variants map[Name]FeatureVariant `json:"variants"`
|
||||
}
|
||||
|
||||
// A concrete type for a feature flag variant
|
||||
type FeatureVariant struct {
|
||||
// Name of the variant
|
||||
Variant Name `json:"variant"`
|
||||
// Value of the variant
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
type GettableFeature struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Stage string `json:"stage"`
|
||||
Description string `json:"description"`
|
||||
DefaultVariant string `json:"defaultVariant"`
|
||||
Variants map[string]any `json:"variants"`
|
||||
ResolvedValue any `json:"resolvedValue"`
|
||||
}
|
||||
|
||||
// This is the helper function to get the value of a variant of a feature
|
||||
func VariantValue[T any](feature *Feature, variant Name) (t T, detail openfeature.ProviderResolutionDetail, err error) {
|
||||
value, ok := feature.Variants[variant]
|
||||
if !ok {
|
||||
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureVariantNotFound, "variant %s not found for feature %s in variants %v", variant.String(), feature.Name.String(), feature.Variants)
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
Variant: feature.DefaultVariant.String(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
t, ok = value.Value.(T)
|
||||
if !ok {
|
||||
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureVariantKindMismatch, "variant %s for feature %s has type %T, expected %T", variant.String(), feature.Name.String(), value.Value, t)
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewTypeMismatchResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
Variant: variant.String(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
Reason: openfeature.StaticReason,
|
||||
Variant: variant.String(),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// This is the helper function to get the variant by value for the given feature
|
||||
func VariantByValue[T comparable](feature *Feature, value T) (featureVariant *FeatureVariant, err error) {
|
||||
|
||||
// technically this method should not be called for object kind
|
||||
// but just for fallback
|
||||
if feature.Kind == KindObject {
|
||||
// return the default variant - just for fallback
|
||||
// ? think more on this
|
||||
return &FeatureVariant{Variant: feature.DefaultVariant, Value: value}, nil
|
||||
}
|
||||
|
||||
for _, variant := range feature.Variants {
|
||||
if variant.Value == value {
|
||||
return &variant, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureVariantNotFound, "no variant found for value %v for feature %s in variants %v", value, feature.Name.String(), feature.Variants)
|
||||
}
|
||||
|
||||
func NewBooleanVariants() map[Name]FeatureVariant {
|
||||
return map[Name]FeatureVariant{
|
||||
MustNewName("disabled"): {
|
||||
Variant: MustNewName("disabled"),
|
||||
Value: false,
|
||||
},
|
||||
MustNewName("enabled"): {
|
||||
Variant: MustNewName("enabled"),
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
14
pkg/types/featuretypes/kind.go
Normal file
14
pkg/types/featuretypes/kind.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package featuretypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
// A concrete type for a feature flag kind
|
||||
type Kind struct{ valuer.String }
|
||||
|
||||
var (
|
||||
KindBoolean = Kind{valuer.NewString("boolean")}
|
||||
KindString = Kind{valuer.NewString("string")}
|
||||
KindFloat = Kind{valuer.NewString("float")}
|
||||
KindInt = Kind{valuer.NewString("int")}
|
||||
KindObject = Kind{valuer.NewString("object")}
|
||||
)
|
||||
37
pkg/types/featuretypes/name.go
Normal file
37
pkg/types/featuretypes/name.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
var nameRegex = regexp.MustCompile(`^[a-z][a-z0-9_]+$`)
|
||||
|
||||
// Name is a concrete type for a feature name.
|
||||
// We make this abstract to avoid direct use of strings and enforce
|
||||
// a consistent way to create and validate feature names.
|
||||
type Name struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func NewName(s string) (Name, error) {
|
||||
if !nameRegex.MatchString(s) {
|
||||
return Name{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid feature name: %s", s)
|
||||
}
|
||||
|
||||
return Name{s: s}, nil
|
||||
}
|
||||
|
||||
func MustNewName(s string) Name {
|
||||
name, err := NewName(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (n Name) String() string {
|
||||
return n.s
|
||||
}
|
||||
129
pkg/types/featuretypes/registry.go
Normal file
129
pkg/types/featuretypes/registry.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// Consumer facing interface for the feature registry
|
||||
type Registry interface {
|
||||
// Returns the feature and the resolution detail for the given name
|
||||
Get(name Name) (*Feature, openfeature.ProviderResolutionDetail, error)
|
||||
|
||||
// Returns the feature and the resolution detail for the given string name
|
||||
GetByString(name string) (*Feature, openfeature.ProviderResolutionDetail, error)
|
||||
|
||||
// Returns all the features in the registry
|
||||
List() []*Feature
|
||||
}
|
||||
|
||||
// Concrete implementation of the Registry interface
|
||||
type registry struct {
|
||||
features map[Name]*Feature
|
||||
}
|
||||
|
||||
// Validates and builds a new registry from a list of features
|
||||
func NewRegistry(features ...*Feature) (Registry, error) {
|
||||
registry := ®istry{features: make(map[Name]*Feature)}
|
||||
|
||||
for _, feature := range features {
|
||||
// Check if the name is unique
|
||||
if _, ok := registry.features[feature.Name]; ok {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "feature name %s already exists", feature.Name.String())
|
||||
}
|
||||
|
||||
// Default variant should always be present
|
||||
if _, ok := feature.Variants[feature.DefaultVariant]; !ok {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "default variant %s not found for feature %s in variants %v", feature.DefaultVariant.String(), feature.Name.String(), feature.Variants)
|
||||
}
|
||||
|
||||
switch feature.Kind {
|
||||
|
||||
case KindBoolean:
|
||||
err := validateFeature[bool](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindString:
|
||||
err := validateFeature[string](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindFloat:
|
||||
err := validateFeature[float64](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindInt:
|
||||
err := validateFeature[int64](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindObject:
|
||||
err := validateFeature[any](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registry.features[feature.Name] = feature
|
||||
}
|
||||
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
func validateFeature[T any](feature *Feature) error {
|
||||
_, _, err := VariantValue[T](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for variant := range feature.Variants {
|
||||
_, _, err := VariantValue[T](feature, variant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) Get(name Name) (f *Feature, detail openfeature.ProviderResolutionDetail, err error) {
|
||||
feature, ok := r.features[name]
|
||||
if !ok {
|
||||
err = errors.Newf(errors.TypeNotFound, ErrCodeFeatureNotFound, "feature %s not found", name.String())
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return feature, openfeature.ProviderResolutionDetail{}, nil
|
||||
}
|
||||
|
||||
func (r *registry) GetByString(name string) (f *Feature, detail openfeature.ProviderResolutionDetail, err error) {
|
||||
featureName, err := NewName(name)
|
||||
if err != nil {
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewFlagNotFoundResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return r.Get(featureName)
|
||||
}
|
||||
|
||||
func (r *registry) List() []*Feature {
|
||||
features := make([]*Feature, 0, len(r.features))
|
||||
for _, f := range r.features {
|
||||
features = append(features, f)
|
||||
}
|
||||
return features
|
||||
}
|
||||
20
pkg/types/featuretypes/stage.go
Normal file
20
pkg/types/featuretypes/stage.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package featuretypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
// A concrete type for a feature flag stage
|
||||
type Stage struct{ valuer.String }
|
||||
|
||||
var (
|
||||
// Used when the feature is experimental
|
||||
StageExperimental = Stage{valuer.NewString("experimental")}
|
||||
|
||||
// Used when the feature works and in preview stage but is not ready for production
|
||||
StagePreview = Stage{valuer.NewString("preview")}
|
||||
|
||||
// Used when the feature is stable and ready for production
|
||||
StageStable = Stage{valuer.NewString("stable")}
|
||||
|
||||
// Used when the feature is deprecated and will be removed in the future
|
||||
StageDeprecated = Stage{valuer.NewString("deprecated")}
|
||||
)
|
||||
@@ -153,10 +153,28 @@ func NewFormulaEvaluator(expressionStr string, canDefaultZero map[string]bool) (
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to parse expression")
|
||||
}
|
||||
|
||||
// Normalize canDefaultZero keys to match variable casing from expression
|
||||
normalizedCanDefaultZero := make(map[string]bool)
|
||||
vars := expression.Vars()
|
||||
for _, variable := range vars {
|
||||
// If exact match exists, use it
|
||||
if val, ok := canDefaultZero[variable]; ok {
|
||||
normalizedCanDefaultZero[variable] = val
|
||||
continue
|
||||
}
|
||||
// Otherwise try case-insensitive lookup
|
||||
for k, v := range canDefaultZero {
|
||||
if strings.EqualFold(k, variable) {
|
||||
normalizedCanDefaultZero[variable] = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evaluator := &FormulaEvaluator{
|
||||
expression: expression,
|
||||
variables: expression.Vars(),
|
||||
canDefaultZero: canDefaultZero,
|
||||
variables: vars,
|
||||
canDefaultZero: normalizedCanDefaultZero,
|
||||
aggRefs: make(map[string]aggregationRef),
|
||||
}
|
||||
|
||||
@@ -281,6 +299,16 @@ func (fe *FormulaEvaluator) buildSeriesLookup(timeSeriesData map[string]*TimeSer
|
||||
// We are only interested in the time series data for the queries that are
|
||||
// involved in the formula expression.
|
||||
data, exists := timeSeriesData[aggRef.QueryName]
|
||||
if !exists {
|
||||
// try case-insensitive lookup
|
||||
for k, v := range timeSeriesData {
|
||||
if strings.EqualFold(k, aggRef.QueryName) {
|
||||
data = v
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -864,6 +864,158 @@ func TestComplexExpression(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseInsensitiveQueryNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
tsData map[string]*TimeSeriesData
|
||||
expectedValues []float64
|
||||
}{
|
||||
{
|
||||
name: "lowercase query names",
|
||||
expression: "a / b",
|
||||
tsData: map[string]*TimeSeriesData{
|
||||
"A": createFormulaTestTimeSeriesData("A", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 10}),
|
||||
},
|
||||
}),
|
||||
"B": createFormulaTestTimeSeriesData("B", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 2}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectedValues: []float64{5.0},
|
||||
},
|
||||
{
|
||||
name: "mixed case query names",
|
||||
expression: "A / b",
|
||||
tsData: map[string]*TimeSeriesData{
|
||||
"A": createFormulaTestTimeSeriesData("A", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 10}),
|
||||
},
|
||||
}),
|
||||
"B": createFormulaTestTimeSeriesData("B", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 2}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectedValues: []float64{5.0},
|
||||
},
|
||||
{
|
||||
name: "uppercase query names with lowercase data keys",
|
||||
expression: "A / B",
|
||||
tsData: map[string]*TimeSeriesData{
|
||||
"a": createFormulaTestTimeSeriesData("a", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 10}),
|
||||
},
|
||||
}),
|
||||
"b": createFormulaTestTimeSeriesData("b", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 2}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectedValues: []float64{5.0},
|
||||
},
|
||||
{
|
||||
name: "all lowercase",
|
||||
expression: "a/b",
|
||||
tsData: map[string]*TimeSeriesData{
|
||||
"a": createFormulaTestTimeSeriesData("a", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 100}),
|
||||
},
|
||||
}),
|
||||
"b": createFormulaTestTimeSeriesData("b", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 10}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectedValues: []float64{10.0},
|
||||
},
|
||||
{
|
||||
name: "complex expression with mixed case",
|
||||
expression: "a + B * c",
|
||||
tsData: map[string]*TimeSeriesData{
|
||||
"A": createFormulaTestTimeSeriesData("A", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 5}),
|
||||
},
|
||||
}),
|
||||
"b": createFormulaTestTimeSeriesData("b", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 3}),
|
||||
},
|
||||
}),
|
||||
"C": createFormulaTestTimeSeriesData("C", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{1: 2}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectedValues: []float64{11.0}, // 5 + 3 * 2 = 11
|
||||
},
|
||||
{
|
||||
name: "lowercase variables with default zero missing point",
|
||||
expression: "a + b",
|
||||
tsData: map[string]*TimeSeriesData{
|
||||
"A": createFormulaTestTimeSeriesData("A", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{
|
||||
1: 10,
|
||||
2: 20,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
"B": createFormulaTestTimeSeriesData("B", []*TimeSeries{
|
||||
{
|
||||
Labels: createLabels(map[string]string{}),
|
||||
Values: createValues(map[int64]float64{
|
||||
1: 5,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
expectedValues: []float64{15.0, 20.0}, // t1: 10+5, t2: 20+0
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
evaluator, err := NewFormulaEvaluator(tt.expression, map[string]bool{"a": true, "A": true, "b": true, "B": true, "c": true, "C": true})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := evaluator.EvaluateFormula(tt.tsData)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
assert.Equal(t, 1, len(result), "should have exactly one result series")
|
||||
assert.Equal(t, len(tt.expectedValues), len(result[0].Values), "should match expected number of values")
|
||||
for i, v := range tt.expectedValues {
|
||||
assert.InDelta(t, v, result[0].Values[i].Value, 0.0001, "value at index %d should match", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsValueExpression(t *testing.T) {
|
||||
tsData := map[string]*TimeSeriesData{
|
||||
"A": createFormulaTestTimeSeriesData("A", []*TimeSeries{
|
||||
|
||||
@@ -81,7 +81,7 @@ class TracesResourceOrAttributeKeys(ABC):
|
||||
self.is_column = is_column
|
||||
|
||||
def np_arr(self) -> np.array:
|
||||
return np.array([self.name, self.tag_type, self.datatype, self.is_column])
|
||||
return np.array([self.name, self.tag_type, self.datatype, self.is_column], dtype=object)
|
||||
|
||||
|
||||
class TracesTagAttributes(ABC):
|
||||
@@ -636,8 +636,10 @@ def insert_traces(
|
||||
)
|
||||
|
||||
attribute_keys: List[TracesResourceOrAttributeKeys] = []
|
||||
resource_keys: List[TracesResourceOrAttributeKeys] = []
|
||||
for trace in traces:
|
||||
attribute_keys.extend(trace.attribute_keys)
|
||||
resource_keys.extend(trace.resource_keys)
|
||||
|
||||
if len(attribute_keys) > 0:
|
||||
clickhouse.conn.insert(
|
||||
@@ -646,6 +648,13 @@ def insert_traces(
|
||||
data=[attribute_key.np_arr() for attribute_key in attribute_keys],
|
||||
)
|
||||
|
||||
if len(resource_keys) > 0:
|
||||
clickhouse.conn.insert(
|
||||
database="signoz_traces",
|
||||
table="distributed_span_attributes_keys",
|
||||
data=[resource_key.np_arr() for resource_key in resource_keys],
|
||||
)
|
||||
|
||||
# Insert main traces
|
||||
clickhouse.conn.insert(
|
||||
database="signoz_traces",
|
||||
|
||||
@@ -67,6 +67,7 @@ def test_logs_list(
|
||||
"code.file": "/opt/integration.go",
|
||||
"code.function": "com.example.Integration.process",
|
||||
"code.line": 120,
|
||||
"metric.domain_id": "d-001",
|
||||
"telemetry.sdk.language": "go",
|
||||
},
|
||||
body="This is a log message, coming from a go application",
|
||||
@@ -141,6 +142,7 @@ def test_logs_list(
|
||||
"code.function": "com.example.Integration.process",
|
||||
"log.iostream": "stdout",
|
||||
"logtag": "F",
|
||||
"metric.domain_id": "d-001",
|
||||
"telemetry.sdk.language": "go",
|
||||
}
|
||||
assert rows[0]["data"]["attributes_number"] == {"code.line": 120}
|
||||
@@ -308,6 +310,86 @@ def test_logs_list(
|
||||
assert len(values) == 1
|
||||
assert 120 in values
|
||||
|
||||
# Query keys from the fields API with context specified in the key
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/keys"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"authorization": f"Bearer {token}",
|
||||
},
|
||||
params={
|
||||
"signal": "logs",
|
||||
"searchText": "resource.servic",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
keys = response.json()["data"]["keys"]
|
||||
assert "service.name" in keys
|
||||
assert any(k["fieldContext"] == "resource" for k in keys["service.name"])
|
||||
|
||||
# Do not treat `metric.` as a context prefix for logs
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/keys"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"authorization": f"Bearer {token}",
|
||||
},
|
||||
params={
|
||||
"signal": "logs",
|
||||
"searchText": "metric.do",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
keys = response.json()["data"]["keys"]
|
||||
assert "metric.domain_id" in keys
|
||||
|
||||
# Query values of service.name resource attribute using context-prefixed key
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"authorization": f"Bearer {token}",
|
||||
},
|
||||
params={
|
||||
"signal": "logs",
|
||||
"name": "resource.service.name",
|
||||
"searchText": "",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
values = response.json()["data"]["values"]["stringValues"]
|
||||
assert "go" in values
|
||||
assert "java" in values
|
||||
|
||||
# Query values of metric.domain_id (string attribute) and ensure context collision doesn't break it
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"authorization": f"Bearer {token}",
|
||||
},
|
||||
params={
|
||||
"signal": "logs",
|
||||
"name": "metric.domain_id",
|
||||
"searchText": "",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
values = response.json()["data"]["values"]["stringValues"]
|
||||
assert "d-001" in values
|
||||
|
||||
|
||||
def test_logs_time_series_count(
|
||||
signoz: types.SigNoz,
|
||||
|
||||
@@ -373,3 +373,43 @@ def test_traces_list(
|
||||
assert len(values) == 2
|
||||
|
||||
assert set(values) == set(["POST", "PATCH"])
|
||||
|
||||
# Query keys from the fields API with context specified in the key
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/keys"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"authorization": f"Bearer {token}",
|
||||
},
|
||||
params={
|
||||
"signal": "traces",
|
||||
"searchText": "resource.servic",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
keys = response.json()["data"]["keys"]
|
||||
assert "service.name" in keys
|
||||
assert any(k["fieldContext"] == "resource" for k in keys["service.name"])
|
||||
|
||||
# Query values of service.name resource attribute using context-prefixed key
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"authorization": f"Bearer {token}",
|
||||
},
|
||||
params={
|
||||
"signal": "traces",
|
||||
"name": "resource.service.name",
|
||||
"searchText": "",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
values = response.json()["data"]["values"]["stringValues"]
|
||||
assert set(values) == set(["topic-service", "http-service"])
|
||||
|
||||
Reference in New Issue
Block a user