Compare commits

..

10 Commits

Author SHA1 Message Date
Abhishek Kumar Singh
e10c1b9723 feat: added handling for log and traces based aggregation for QB in CalculateEvalDelay 2025-12-18 13:43:25 +05:30
Abhishek Kumar Singh
06ae762070 fix: use matchType and compare operator from threshold rather than ruleCondition 2025-12-17 21:03:54 +05:30
Abhishek Kumar Singh
3cc34baf74 chore: remove eval delay for safe eval rules 2025-12-17 20:39:32 +05:30
primus-bot[bot]
1110864549 chore(release): bump to v0.105.1 (#9819)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-12-17 14:17:54 +05:30
Vikrant Gupta
5cb515cade fix(apiserver): preference routes registeration before user routes (#9818) 2025-12-17 14:01:28 +05:30
primus-bot[bot]
41d5f6a00c chore(release): bump to v0.105.0 (#9815)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-12-17 12:33:56 +05:30
shubham-signoz
61ec1ef28e chore(onboardingv2/config): python traces changes (#9808)
Signed-off-by: Shubham Dubey <shubham@signoz.io>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2025-12-17 08:35:53 +05:30
Yunus M
529a9e7009 fix: handle default columns in logs and traces explorer (#9722)
* fix: handle default columns in logs and traces explorer

* fix: filter out selected columns based on signal in logs and traces explorer
2025-12-16 13:32:18 +05:30
Nikhil Mantri
b00687b43f chore(metrics-explorer): API for the dashboards with metric_name (#9638) 2025-12-16 12:08:00 +05:30
Pandey
8771919de6 feat(gen): add cobra command for generating openapi spec (#9803)
add cobra command for auto-generating openapi spec
2025-12-15 17:48:30 +05:30
84 changed files with 6047 additions and 777 deletions

View File

@@ -73,3 +73,19 @@ jobs:
shell: bash
run: |
make docker-build-enterprise
openapi:
if: |
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: generate-openapi
run: |
go run cmd/enterprise/*.go generate openapi
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)

View File

@@ -13,6 +13,7 @@ func main() {
// register a list of commands to the root command
registerServer(cmd.RootCmd, logger)
cmd.RegisterGenerate(cmd.RootCmd, logger)
cmd.Execute(logger)
}

View File

@@ -13,6 +13,7 @@ func main() {
// register a list of commands to the root command
registerServer(cmd.RootCmd, logger)
cmd.RegisterGenerate(cmd.RootCmd, logger)
cmd.Execute(logger)
}

21
cmd/generate.go Normal file
View File

@@ -0,0 +1,21 @@
package cmd
import (
"log/slog"
"github.com/spf13/cobra"
)
func RegisterGenerate(parentCmd *cobra.Command, logger *slog.Logger) {
var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate artifacts",
SilenceUsage: true,
SilenceErrors: true,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
}
registerGenerateOpenAPI(generateCmd)
parentCmd.AddCommand(generateCmd)
}

41
cmd/openapi.go Normal file
View File

@@ -0,0 +1,41 @@
package cmd
import (
"context"
"log/slog"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/version"
"github.com/spf13/cobra"
)
func registerGenerateOpenAPI(parentCmd *cobra.Command) {
openapiCmd := &cobra.Command{
Use: "openapi",
Short: "Generate OpenAPI schema for SigNoz",
RunE: func(currCmd *cobra.Command, args []string) error {
return runGenerateOpenAPI(currCmd.Context())
},
}
parentCmd.AddCommand(openapiCmd)
}
func runGenerateOpenAPI(ctx context.Context) error {
instrumentation, err := instrumentation.New(ctx, instrumentation.Config{Logs: instrumentation.LogsConfig{Level: slog.LevelInfo}}, version.Info, "signoz")
if err != nil {
return err
}
openapi, err := signoz.NewOpenAPI(ctx, instrumentation)
if err != nil {
return err
}
if err := openapi.CreateAndWrite("docs/api/openapi.yml"); err != nil {
return err
}
return nil
}

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.104.0
image: signoz/signoz:v0.105.1
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.104.0
image: signoz/signoz:v0.105.1
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.104.0}
image: signoz/signoz:${VERSION:-v0.105.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.104.0}
image: signoz/signoz:${VERSION:-v0.105.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

2293
docs/api/openapi.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -94,10 +94,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// routes available only in ee version
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.Signoz.Handlers.Session.CreateSessionBySAMLCallback)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/complete/oidc", am.OpenAccess(ah.Signoz.Handlers.Session.CreateSessionByOIDCCallback)).Methods(http.MethodGet)
// base overrides
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)

View File

@@ -243,6 +243,11 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
err := s.signoz.APIServer.AddToRouter(r)
if err != nil {
return nil, err
}
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
@@ -253,7 +258,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
handler = handlers.CompressHandler(handler)
err := web.AddToRouter(r)
err = web.AddToRouter(r)
if err != nil {
return nil, err
}

View File

@@ -26,6 +26,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
// calculate eval delay based on rule config
evalDelay := baserules.CalculateEvalDelay(opts.Rule, opts.ManagerOpts.EvalDelay)
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
@@ -35,7 +39,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Reader,
opts.Querier,
opts.SLogger,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithEvalDelay(evalDelay),
baserules.WithSQLStore(opts.SQLStore),
)
@@ -80,7 +84,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Querier,
opts.SLogger,
opts.Cache,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithEvalDelay(evalDelay),
baserules.WithSQLStore(opts.SQLStore),
)
if err != nil {

View File

@@ -47,7 +47,6 @@ import { AppState } from 'store/reducers';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
@@ -134,7 +133,7 @@ function LogDetailInner({
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (event: React.MouseEvent): void => {
const handleOpenInExplorer = (): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
@@ -147,16 +146,7 @@ function LogDetailInner({
),
),
};
if (isCtrlOrMMetaKey(event)) {
window.open(
`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`,
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
}
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(

View File

@@ -28,7 +28,6 @@ import React, {
useState,
} from 'react';
import { Virtuoso } from 'react-virtuoso';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { popupContainer } from 'utils/selectPopupContainer';
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
@@ -903,7 +902,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const lastVisibleChipIndex = getLastVisibleChipIndex();
// Handle special keyboard combinations
const isCtrlOrCmd = isCtrlOrMMetaKey(e);
const isCtrlOrCmd = e.ctrlKey || e.metaKey;
// Handle Ctrl+A (select all)
if (isCtrlOrCmd && e.key === 'a') {

View File

@@ -5,12 +5,12 @@ import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Channels } from 'types/api/channels/getAll';
import { genericNavigate } from 'utils/genericNavigate';
import Delete from './Delete';
@@ -20,15 +20,13 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const { user } = useAppContext();
const [action] = useComponentPermission(['new_alert_action'], user.role);
const onClickEditHandler = useCallback(
(id: string, event: React.MouseEvent): void => {
genericNavigate(
generatePath(ROUTES.CHANNELS_EDIT, { channelId: id }),
event,
);
},
[],
);
const onClickEditHandler = useCallback((id: string) => {
history.push(
generatePath(ROUTES.CHANNELS_EDIT, {
channelId: id,
}),
);
}, []);
const columns: ColumnsType<Channels> = [
{
@@ -54,10 +52,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
width: 80,
render: (id: string): JSX.Element => (
<>
<Button
onClick={(event: React.MouseEvent): void => onClickEditHandler(id, event)}
type="link"
>
<Button onClick={(): void => onClickEditHandler(id)} type="link">
{t('column_channel_edit')}
</Button>
<Delete id={id} notifications={notifications} />

View File

@@ -8,6 +8,7 @@ import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react';
@@ -16,7 +17,6 @@ import { useQuery } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import { genericNavigate } from 'utils/genericNavigate';
import AlertChannelsComponent from './AlertChannels';
import { Button, ButtonContainer, RightActionContainer } from './styles';
@@ -30,8 +30,8 @@ function AlertChannels(): JSX.Element {
['add_new_channel'],
user.role,
);
const onToggleHandler = useCallback((event: React.MouseEvent) => {
genericNavigate(ROUTES.CHANNELS_NEW, event);
const onToggleHandler = useCallback(() => {
history.push(ROUTES.CHANNELS_NEW);
}, []);
const { isLoading, data, error } = useQuery<
@@ -78,7 +78,7 @@ function AlertChannels(): JSX.Element {
}
>
<Button
onClick={(event: React.MouseEvent): void => onToggleHandler(event)}
onClick={onToggleHandler}
icon={<PlusOutlined />}
disabled={!addNewChannelPermission}
>

View File

@@ -18,7 +18,6 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
import { genericNavigate } from 'utils/genericNavigate';
import { keyToExclude } from './config';
import { DashedContainer, EditorContainer, EventContainer } from './styles';
@@ -112,18 +111,14 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
}));
const onClickTraceHandler = (event: React.MouseEvent): void => {
const onClickTraceHandler = (): void => {
logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
genericNavigate(
`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`,
event,
);
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
};
const logEventCalledRef = useRef(false);
@@ -190,10 +185,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<DashedContainer>
<Typography>{t('see_trace_graph')}</Typography>
<Button
onClick={(event: React.MouseEvent): void => onClickTraceHandler(event)}
type="primary"
>
<Button onClick={onClickTraceHandler} type="primary">
{t('see_error_in_trace_graph')}
</Button>
</DashedContainer>

View File

@@ -73,7 +73,6 @@ import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { panelTypeToExplorerView } from 'utils/explorerUtils';
import { genericNavigate } from 'utils/genericNavigate';
import { PreservedViewsTypes } from './constants';
import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
@@ -193,7 +192,7 @@ function ExplorerOptions({
);
const onCreateAlertsHandler = useCallback(
(defaultQuery: Query | null, event?: React.MouseEvent) => {
(defaultQuery: Query | null) => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Create alert', {
panelType,
@@ -212,11 +211,10 @@ function ExplorerOptions({
const stringifiedQuery = handleConditionalQueryModification(defaultQuery);
genericNavigate(
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
stringifiedQuery,
)}`,
event,
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -395,15 +393,21 @@ function ExplorerOptions({
backwardCompatibleOptions = omit(options, 'version');
}
// Use the correct default columns based on the current data source
const defaultColumns =
sourcepage === DataSource.TRACES
? defaultTraceSelectedColumns
: defaultLogsSelectedColumns;
if (extraData.selectColumns?.length) {
handleOptionsChange({
...backwardCompatibleOptions,
selectColumns: extraData.selectColumns,
});
} else if (!isEqual(defaultTraceSelectedColumns, options.selectColumns)) {
} else if (!isEqual(defaultColumns, options.selectColumns)) {
handleOptionsChange({
...backwardCompatibleOptions,
selectColumns: defaultTraceSelectedColumns,
selectColumns: defaultColumns,
});
}
};
@@ -743,9 +747,7 @@ function ExplorerOptions({
<Button
disabled={disabled}
shape="round"
onClick={(event: React.MouseEvent): void =>
onCreateAlertsHandler(query, event)
}
onClick={(): void => onCreateAlertsHandler(query)}
icon={<ConciergeBell size={16} />}
>
Create an Alert

View File

@@ -3,6 +3,7 @@ import getAll from 'api/alerts/getAll';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { ArrowRight, ArrowUpRight, Plus } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
@@ -12,7 +13,6 @@ import { useQuery } from 'react-query';
import { Link, useLocation } from 'react-router-dom';
import { GettableAlert } from 'types/api/alerts/get';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
export default function AlertRules({
onUpdateChecklistDoneItem,
@@ -114,10 +114,7 @@ export default function AlertRules({
</div>
);
const onEditHandler = (
record: GettableAlert,
event?: React.MouseEvent | React.KeyboardEvent,
): void => {
const onEditHandler = (record: GettableAlert) => (): void => {
logEvent('Homepage: Alert clicked', {
ruleId: record.id,
ruleName: record.alert,
@@ -134,7 +131,7 @@ export default function AlertRules({
params.set(QueryParams.ruleId, record.id.toString());
genericNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, event);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
};
const renderAlertRules = (): JSX.Element => (
@@ -146,10 +143,10 @@ export default function AlertRules({
tabIndex={0}
className="alert-rule-item home-data-item"
key={rule.id}
onClick={(event: React.MouseEvent): void => onEditHandler(rule, event)}
onClick={onEditHandler(rule)}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
onEditHandler(rule, e);
onEditHandler(rule);
}
}}
>

View File

@@ -1,5 +1,4 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable sonarjs/cognitive-complexity */
import { Button, Skeleton, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
@@ -10,7 +9,6 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { genericNavigate } from 'utils/genericNavigate';
import { DOCS_LINKS } from '../constants';
@@ -86,16 +84,16 @@ function DataSourceInfo({
icon={<img src="/Icons/container-plus.svg" alt="plus" />}
role="button"
tabIndex={0}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window.open(
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',

View File

@@ -30,7 +30,6 @@ import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { isIngestionActive } from 'utils/app';
import { genericNavigate } from 'utils/genericNavigate';
import { popupContainer } from 'utils/selectPopupContainer';
import AlertRules from './AlertRules/AlertRules';
@@ -551,11 +550,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Logs',
});
genericNavigate(ROUTES.LOGS_EXPLORER, event);
history.push(ROUTES.LOGS_EXPLORER);
}}
>
Open Logs Explorer
@@ -565,11 +564,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Traces',
});
genericNavigate(ROUTES.TRACES_EXPLORER, event);
history.push(ROUTES.TRACES_EXPLORER);
}}
>
Open Traces Explorer
@@ -579,11 +578,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Metrics',
});
genericNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, event);
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
}}
>
Open Metrics Explorer
@@ -620,11 +619,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
});
genericNavigate(ROUTES.ALL_DASHBOARD, event);
history.push(ROUTES.ALL_DASHBOARD);
}}
>
Create dashboard
@@ -662,11 +661,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Alerts',
});
genericNavigate(ROUTES.ALERTS_NEW, event);
history.push(ROUTES.ALERTS_NEW);
}}
>
Create an alert

View File

@@ -4,12 +4,12 @@ import './HomeChecklist.styles.scss';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
export type ChecklistItem = {
id: string;
@@ -86,22 +86,18 @@ function HomeChecklist({
<Button
type="default"
className="periscope-btn secondary"
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Welcome Checklist: Get started clicked', {
step: item.id,
});
const checkForNewTabAndNavigate = (): void => {
genericNavigate(item.toRoute || '', event);
};
if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) {
checkForNewTabAndNavigate();
history.push(item.toRoute || '');
} else if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
checkForNewTabAndNavigate();
history.push(item.toRoute || '');
} else {
window?.open(
item.docsLink || '',

View File

@@ -11,6 +11,7 @@ import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { ArrowRight, ArrowUpRight } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
@@ -28,8 +29,6 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { FeatureKeys } from '../../../constants/features';
import { DOCS_LINKS } from '../constants';
@@ -65,7 +64,7 @@ const EmptyState = memo(
<Button
type="default"
className="periscope-btn secondary"
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Get Started clicked', {
source: 'Service Metrics',
});
@@ -74,7 +73,7 @@ const EmptyState = memo(
activeLicenseV3 &&
activeLicenseV3.platform === LicensePlatform.CLOUD
) {
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
@@ -117,7 +116,7 @@ const ServicesListTable = memo(
onRowClick,
}: {
services: ServicesList[];
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
onRowClick: (record: ServicesList) => void;
}): JSX.Element => (
<div className="services-list-container home-data-item-container metrics-services-list">
<div className="services-list">
@@ -126,8 +125,8 @@ const ServicesListTable = memo(
dataSource={services}
pagination={false}
className="services-table"
onRow={(record): { onClick: (event: React.MouseEvent) => void } => ({
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
onRow={(record): { onClick: () => void } => ({
onClick: (): void => onRowClick(record),
})}
/>
</div>
@@ -285,19 +284,11 @@ function ServiceMetrics({
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
const handleRowClick = useCallback(
(record: ServicesList, event: React.MouseEvent) => {
(record: ServicesList) => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
if (event && isCtrlOrMMetaKey(event)) {
window.open(
`${ROUTES.APPLICATION}/${record.serviceName}`,
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
}
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
},
[safeNavigate],
);

View File

@@ -3,6 +3,7 @@ import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { useQueryService } from 'hooks/useQueryService';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { ArrowRight, ArrowUpRight } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
@@ -14,8 +15,6 @@ import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { DOCS_LINKS } from '../constants';
import { columns, TIME_PICKER_OPTIONS } from './constants';
@@ -119,7 +118,7 @@ export default function ServiceTraces({
<Button
type="default"
className="periscope-btn secondary"
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Get Started clicked', {
source: 'Service Traces',
});
@@ -128,7 +127,7 @@ export default function ServiceTraces({
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
@@ -173,21 +172,13 @@ export default function ServiceTraces({
dataSource={top5Services}
pagination={false}
className="services-table"
onRow={(record): { onClick: (event: React.MouseEvent) => void } => ({
onClick: (event: React.MouseEvent): void => {
onRow={(record): { onClick: () => void } => ({
onClick: (): void => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
if (event && isCtrlOrMMetaKey(event)) {
window.open(
`${ROUTES.APPLICATION}/${record.serviceName}`,
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
}
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
},
})}
/>

View File

@@ -5,10 +5,10 @@ import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { genericNavigate } from 'utils/genericNavigate';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
@@ -36,9 +36,9 @@ export function AlertsEmptyState(): JSX.Element {
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback((event: React.MouseEvent): void => {
const onClickNewAlertHandler = useCallback(() => {
setLoading(false);
genericNavigate(ROUTES.ALERTS_NEW, event);
history.push(ROUTES.ALERTS_NEW);
}, []);
return (
@@ -70,9 +70,7 @@ export function AlertsEmptyState(): JSX.Element {
<div className="action-container">
<Button
className="add-alert-btn"
onClick={(event: React.MouseEvent): void =>
onClickNewAlertHandler(event)
}
onClick={onClickNewAlertHandler}
icon={<PlusOutlined />}
disabled={!addNewAlert}
loading={loading}

View File

@@ -39,7 +39,6 @@ import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import DeleteAlert from './DeleteAlert';
import { ColumnButton, SearchContainer } from './styles';
@@ -300,7 +299,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
e.stopPropagation();
e.preventDefault();
onEditHandler(record, isCtrlOrMMetaKey(e));
onEditHandler(record, e.metaKey || e.ctrlKey);
};
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;

View File

@@ -118,7 +118,7 @@ const templatesList: DashboardTemplate[] = [
interface DashboardTemplatesModalProps {
showNewDashboardTemplatesModal: boolean;
onCreateNewDashboard: (event: React.MouseEvent) => void;
onCreateNewDashboard: () => void;
onCancel: () => void;
}
@@ -204,9 +204,7 @@ export default function DashboardTemplatesModal({
type="primary"
className="periscope-btn primary"
icon={<Plus size={14} />}
onClick={(event: React.MouseEvent): void =>
onCreateNewDashboard(event)
}
onClick={onCreateNewDashboard}
>
New dashboard
</Button>

View File

@@ -86,7 +86,6 @@ import {
Widgets,
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
@@ -283,48 +282,35 @@ function DashboardsList(): JSX.Element {
refetchDashboardList,
})) || [];
const onNewDashboardHandler = useCallback(
async (event: React.MouseEvent): Promise<void> => {
try {
logEvent('Dashboard List: Create dashboard clicked', {});
setNewDashboardState({
...newDashboardState,
loading: true,
});
const response = await createDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V5,
});
const onNewDashboardHandler = useCallback(async () => {
try {
logEvent('Dashboard List: Create dashboard clicked', {});
setNewDashboardState({
...newDashboardState,
loading: true,
});
const response = await createDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V5,
});
if (event && isCtrlOrMMetaKey(event)) {
window.open(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
}
} catch (error) {
showErrorModal(error as APIError);
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
},
[newDashboardState, safeNavigate, showErrorModal, t],
);
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
} catch (error) {
showErrorModal(error as APIError);
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, safeNavigate, showErrorModal, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
@@ -426,8 +412,8 @@ function DashboardsList(): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (isCtrlOrMMetaKey(event)) {
window.open(getLink(), '_blank', 'noopener,noreferrer');
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}
@@ -653,8 +639,8 @@ function DashboardsList(): JSX.Element {
label: (
<div
className="create-dashboard-menu-item"
onClick={(event: React.MouseEvent): void => {
onNewDashboardHandler(event);
onClick={(): void => {
onNewDashboardHandler();
}}
>
<LayoutGrid size={14} /> Create dashboard
@@ -941,9 +927,7 @@ function DashboardsList(): JSX.Element {
<DashboardTemplatesModal
showNewDashboardTemplatesModal={showNewDashboardTemplatesModal}
onCreateNewDashboard={(event: React.MouseEvent): Promise<void> =>
onNewDashboardHandler(event)
}
onCreateNewDashboard={onNewDashboardHandler}
onCancel={(): void => {
setShowNewDashboardTemplatesModal(false);
}}

View File

@@ -1,6 +1,6 @@
import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import { genericNavigate } from 'utils/genericNavigate';
import history from 'lib/history';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
@@ -11,7 +11,11 @@ function Name(name: Data['name'], data: Data): JSX.Element {
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${DashboardId}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
genericNavigate(getLink(), event);
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
history.push(getLink());
}
};
return (

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -180,17 +179,14 @@ function DBCall(): JSX.Element {
type="default"
size="small"
id="database_call_rps_button"
onClick={(event): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
@@ -219,17 +215,14 @@ function DBCall(): JSX.Element {
type="default"
size="small"
id="database_call_avg_duration_button"
onClick={(event): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -245,28 +244,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_error_percentage_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address || '',
isError: true,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address || '',
isError: true,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_error_percentage">
<GraphContainer>
@@ -293,28 +286,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_duration_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_duration">
@@ -344,28 +331,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_rps_by_address_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_rps_by_address">
<GraphContainer>
@@ -392,28 +373,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_duration_by_address_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_duration_by_address">

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/no-identical-functions */
import logEvent from 'api/common/logEvent';
import getTopLevelOperations, {
ServiceDataProps,
@@ -32,7 +31,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { genericNavigate } from 'utils/genericNavigate';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -230,16 +228,14 @@ function Application(): JSX.Element {
* @param timestamp - The timestamp in seconds
* @param apmToTraceQuery - query object
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
* @param event - Click event to handle opening in new tab
* @returns A callback function that handles the navigation when executed
*/
const onErrorTrackHandler = useCallback(
(
timestamp: number,
apmToTraceQuery: Query,
event: React.MouseEvent,
isViewLogsClicked?: boolean,
): void => {
): (() => void) => (): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - stepInterval);
@@ -263,7 +259,7 @@ function Application(): JSX.Element {
queryString,
);
genericNavigate(newPath, event);
history.push(newPath);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[stepInterval],
@@ -323,17 +319,14 @@ function Application(): JSX.Element {
type="default"
size="small"
id="Rate_button"
onClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
@@ -356,17 +349,14 @@ function Application(): JSX.Element {
type="default"
size="small"
id="ApDex_button"
onClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
@@ -380,12 +370,15 @@ function Application(): JSX.Element {
<ColErrorContainer>
<GraphControlsPanel
id="Error_button"
onViewLogsClick={(event: React.MouseEvent): void =>
onErrorTrackHandler(selectedTimeStamp, logErrorQuery, event, true)
}
onViewTracesClick={(event: React.MouseEvent): void =>
onErrorTrackHandler(selectedTimeStamp, errorTrackQuery, event)
}
onViewLogsClick={onErrorTrackHandler(
selectedTimeStamp,
logErrorQuery,
true,
)}
onViewTracesClick={onErrorTrackHandler(
selectedTimeStamp,
errorTrackQuery,
)}
/>
<TopLevelOperation

View File

@@ -6,9 +6,9 @@ import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
interface GraphControlsPanelProps {
id: string;
onViewLogsClick?: (event: React.MouseEvent) => void;
onViewTracesClick: (event: React.MouseEvent) => void;
onViewAPIMonitoringClick?: (event: React.MouseEvent) => void;
onViewLogsClick?: () => void;
onViewTracesClick: () => void;
onViewAPIMonitoringClick?: () => void;
}
function GraphControlsPanel({
@@ -23,7 +23,7 @@ function GraphControlsPanel({
type="link"
icon={<DraftingCompass size={14} />}
size="small"
onClick={(event: React.MouseEvent): void => onViewTracesClick(event)}
onClick={onViewTracesClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View traces
@@ -33,7 +33,7 @@ function GraphControlsPanel({
type="link"
icon={<ScrollText size={14} />}
size="small"
onClick={(event: React.MouseEvent): void => onViewLogsClick(event)}
onClick={onViewLogsClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View logs
@@ -44,9 +44,7 @@ function GraphControlsPanel({
type="link"
icon={<Binoculars size={14} />}
size="small"
onClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringClick(event)
}
onClick={onViewAPIMonitoringClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View External APIs

View File

@@ -103,29 +103,23 @@ function ServiceOverview({
<>
<GraphControlsPanel
id="Service_button"
onViewLogsClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: apmToLogQuery,
isViewLogsClicked: true,
stepInterval,
safeNavigate,
event,
})
}
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewLogsClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: apmToLogQuery,
isViewLogsClicked: true,
stepInterval,
safeNavigate,
})}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
/>
<Card data-testid="service_latency">
<GraphContainer>

View File

@@ -3,7 +3,6 @@ import { navigateToTrace } from 'container/MetricsApplication/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { v4 as uuid } from 'uuid';
import { useGetAPMToTracesQueries } from '../../util';
@@ -51,7 +50,7 @@ function ColumnWithLink({
return (
<Tooltip placement="topLeft" title={text}>
<Typography.Link
onClick={(e): void => handleOnClick(text, isCtrlOrMMetaKey(e))}
onClick={(e): void => handleOnClick(text, e.metaKey || e.ctrlKey)}
>
{text}
</Typography.Link>

View File

@@ -22,7 +22,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -44,7 +43,6 @@ interface OnViewTracePopupClickProps {
isViewLogsClicked?: boolean;
stepInterval?: number;
safeNavigate: (url: string) => void;
event: React.MouseEvent;
}
interface OnViewAPIMonitoringPopupClickProps {
@@ -55,7 +53,6 @@ interface OnViewAPIMonitoringPopupClickProps {
isError: boolean;
safeNavigate: (url: string) => void;
event: React.MouseEvent;
}
export function generateExplorerPath(
@@ -86,7 +83,7 @@ export function generateExplorerPath(
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
* @param stepInterval - Time interval in seconds
* @param safeNavigate - Navigation function
* @param event - Click event to handle opening in new tab
*/
export function onViewTracePopupClick({
selectedTraceTags,
@@ -96,34 +93,33 @@ export function onViewTracePopupClick({
isViewLogsClicked,
stepInterval,
safeNavigate,
event,
}: OnViewTracePopupClickProps): void {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(apmToTraceQuery));
const JSONCompositeQuery = encodeURIComponent(
JSON.stringify(apmToTraceQuery),
);
const newPath = generateExplorerPath(
isViewLogsClicked,
urlParams,
servicename,
selectedTraceTags,
JSONCompositeQuery,
queryString,
);
const newPath = generateExplorerPath(
isViewLogsClicked,
urlParams,
servicename,
selectedTraceTags,
JSONCompositeQuery,
queryString,
);
if (event && isCtrlOrMMetaKey(event)) {
window.open(newPath, '_blank', 'noopener,noreferrer');
} else {
safeNavigate(newPath);
}
};
}
const generateAPIMonitoringPath = (
@@ -153,52 +149,49 @@ export function onViewAPIMonitoringPopupClick({
isError,
stepInterval,
safeNavigate,
event,
}: OnViewAPIMonitoringPopupClickProps): void {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
items: [
...(isError
? [
{
id: uuid().slice(0, 8),
key: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
id: 'hasError--bool--tag--true',
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
return (): void => {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
items: [
...(isError
? [
{
id: uuid().slice(0, 8),
key: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
id: 'hasError--bool--tag--true',
},
op: 'in',
value: ['true'],
},
op: 'in',
value: ['true'],
},
]
: []),
{
id: uuid().slice(0, 8),
key: {
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
]
: []),
{
id: uuid().slice(0, 8),
key: {
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
},
op: '=',
value: servicename,
},
op: '=',
value: servicename,
},
],
op: 'AND',
};
const newPath = generateAPIMonitoringPath(
domainName,
startTime,
endTime,
filters,
);
],
op: 'AND',
};
const newPath = generateAPIMonitoringPath(
domainName,
startTime,
endTime,
filters,
);
if (event && isCtrlOrMMetaKey(event)) {
window.open(newPath, '_blank', 'noopener,noreferrer');
} else {
safeNavigate(newPath);
}
};
}
export function useGraphClickHandler(

View File

@@ -17,7 +17,6 @@ import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { v4 as uuid } from 'uuid';
import { IServiceName } from './Tabs/types';
@@ -116,7 +115,7 @@ function TopOperationsTable({
e.stopPropagation();
e.preventDefault();
if (isCtrlOrMMetaKey(e)) {
if (e.metaKey || e.ctrlKey) {
handleOnClick(text, true); // open in new tab
} else {
handleOnClick(text, false); // open in current tab

View File

@@ -1,14 +1,13 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './NoLogs.styles.scss';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { ArrowUpRight } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
import DOCLINKS from 'utils/docLinks';
import { genericNavigate } from 'utils/genericNavigate';
export default function NoLogs({
dataSource,
@@ -17,8 +16,6 @@ export default function NoLogs({
}): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const REL_NOOPENER_NOREFERRER = 'noopener,noreferrer';
const handleLinkClick = (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
): void => {
@@ -41,25 +38,13 @@ export default function NoLogs({
} else {
link = ROUTES.GET_STARTED_LOGS_MANAGEMENT;
}
genericNavigate(link, e);
history.push(link);
} else if (dataSource === 'traces') {
window.open(
DOCLINKS.TRACES_EXPLORER_EMPTY_STATE,
'_blank',
REL_NOOPENER_NOREFERRER,
);
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
} else if (dataSource === DataSource.METRICS) {
window.open(
DOCLINKS.METRICS_EXPLORER_EMPTY_STATE,
'_blank',
REL_NOOPENER_NOREFERRER,
);
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
} else {
window.open(
`${DOCLINKS.USER_GUIDE}${dataSource}/`,
'_blank',
REL_NOOPENER_NOREFERRER,
);
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
}
};
return (
@@ -74,12 +59,7 @@ export default function NoLogs({
</span>
</Typography>
<Typography.Link
className="send-logs-link"
onClick={(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): void =>
handleLinkClick(e)
}
>
<Typography.Link className="send-logs-link" onClick={handleLinkClick}>
Sending {dataSource} to SigNoz <ArrowUpRight size={16} />
</Typography.Link>
</div>

View File

@@ -516,6 +516,8 @@
"falcon",
"fastapi",
"flask",
"celery",
"gunicorn",
"monitor python application",
"monitor python backend",
"opentelemetry python",
@@ -540,134 +542,33 @@
],
"id": "python",
"question": {
"desc": "Which Python framework do you use?",
"desc": "What is your Environment?",
"type": "select",
"entityID": "framework",
"entityID": "environment",
"options": [
{
"key": "flask",
"label": "Flask",
"imgUrl": "/Logos/flask.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-flask/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-flask/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-flask/"
}
]
}
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-python/"
},
{
"key": "django",
"label": "Django",
"imgUrl": "/Logos/django.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-django/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-django/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-django/"
}
]
}
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-python/"
},
{
"key": "fastapi",
"label": "FastAPI",
"imgUrl": "/Logos/fastapi.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-fastapi/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-fastapi/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-fastapi/"
}
]
}
"key": "windows",
"label": "Windows",
"imgUrl": "/Logos/windows.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-python/"
},
{
"key": "falcon",
"label": "Falcon",
"imgUrl": "/Logos/falcon.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-falcon/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-falcon/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-falcon/"
}
]
}
},
{
"key": "others",
"label": "Others",
"imgUrl": "/Logos/python-others.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-python/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-python/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-python/"
}
]
}
"key": "docker",
"label": "Docker",
"imgUrl": "/Logos/docker.svg",
"link": "https://signoz.io/docs/instrumentation/opentelemetry-python/"
}
]
}
@@ -5422,7 +5323,7 @@
"imgUrl": "/Logos/logs.svg",
"tags": [
"Frontend Monitoring",
"logs"
"logs"
],
"module": "logs",
"relatedSearchKeywords": [

View File

@@ -48,7 +48,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -218,7 +217,7 @@ function QueryBuilderSearch({
if (
!disableNavigationShortcuts &&
isCtrlOrMMetaKey(event) &&
(event.ctrlKey || event.metaKey) &&
event.key === 'Enter'
) {
event.preventDefault();
@@ -229,7 +228,7 @@ function QueryBuilderSearch({
if (
!disableNavigationShortcuts &&
isCtrlOrMMetaKey(event) &&
(event.ctrlKey || event.metaKey) &&
event.key === '/'
) {
event.preventDefault();

View File

@@ -52,7 +52,6 @@ import {
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -457,12 +456,12 @@ function QueryBuilderSearchV2(
setTags((prev) => prev.slice(0, -1));
}
if (isCtrlOrMMetaKey(event) && event.key === '/') {
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
event.preventDefault();
event.stopPropagation();
setShowAllFilters((prev) => !prev);
}
if (isCtrlOrMMetaKey(event) && event.key === 'Enter') {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
handleRunQuery();

View File

@@ -67,8 +67,6 @@ import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { genericNavigate } from 'utils/genericNavigate';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { useCmdK } from '../../providers/cmdKProvider';
import { routeConfig } from './config';
@@ -294,6 +292,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
icon: <Cog size={16} />,
};
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
const [
@@ -411,7 +411,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const openInNewTab = (path: string): void => {
window.open(path, '_blank', 'noopener,noreferrer');
window.open(path, '_blank');
};
const onClickGetStarted = (event: MouseEvent): void => {
@@ -424,7 +424,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
if (isCtrlOrMMetaKey(event)) {
if (isCtrlMetaKey(event)) {
openInNewTab(onboaringRoute);
} else {
history.push(onboaringRoute);
@@ -439,7 +439,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlOrMMetaKey(event)) {
if (event && isCtrlMetaKey(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {
@@ -634,7 +634,11 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
if (item.key === ROUTES.SETTINGS) {
genericNavigate(settingsRoute, event);
if (isCtrlMetaKey(event)) {
openInNewTab(settingsRoute);
} else {
history.push(settingsRoute);
}
} else if (item.key === 'quick-search') {
openCmdK();
} else if (item) {
@@ -758,7 +762,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
);
if (item && !('type' in item) && item.isExternal && item.url) {
window.open(item.url, '_blank', 'noopener,noreferrer');
window.open(item.url, '_blank');
}
if (item && !('type' in item)) {

View File

@@ -10,7 +10,9 @@ import {
import { formUrlParams } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import history from 'lib/history';
import omit from 'lodash-es/omit';
import { HTMLAttributes } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { updateURL } from 'store/actions/trace/util';
@@ -23,7 +25,6 @@ import {
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import { genericNavigate } from 'utils/genericNavigate';
import { v4 } from 'uuid';
dayjs.extend(duration);
@@ -202,13 +203,15 @@ function TraceTable(): JSX.Element {
style={{
cursor: 'pointer',
}}
onRow={(
record: TableType,
): { onClick: (event: React.MouseEvent) => void } => ({
onClick: (event: React.MouseEvent): void => {
onRow={(record: TableType): HTMLAttributes<TableType> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
genericNavigate(getLink(record), event);
if (event.metaKey || event.ctrlKey) {
window.open(getLink(record), '_blank');
} else {
history.push(getLink(record));
}
},
})}
pagination={{

View File

@@ -84,8 +84,8 @@ function TracesTableComponent({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
if (event.ctrlKey || event.metaKey) {
window.open(getTraceLink(record), '_blank', 'noopener,noreferrer');
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
} else {
history.push(getTraceLink(record));
}

View File

@@ -27,7 +27,6 @@ import {
within,
} from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import * as genericNavigate from 'utils/genericNavigate';
import TracesExplorer from '..';
import { Filter } from '../Filter/Filter';
@@ -773,12 +772,6 @@ describe('TracesExplorer - ', () => {
});
it('create an alert btn assert', async () => {
const historyPush = jest.fn();
jest.spyOn(genericNavigate, 'genericNavigate').mockImplementation((link) => {
historyPush(link);
});
const { getByText } = renderWithTracesExplorerRouter(<TracesExplorer />, [
'/traces-explorer/?panelType=list&selectedExplorerView=list',
]);

View File

@@ -18,6 +18,16 @@ jest.mock('api/browser/localstorage/get', () => ({
default: jest.fn((key: string) => mockLocalStorage[key] || null),
}));
const mockLogsColumns = [
{ name: 'timestamp', signal: 'logs', fieldContext: 'log' },
{ name: 'body', signal: 'logs', fieldContext: 'log' },
];
const mockTracesColumns = [
{ name: 'service.name', signal: 'traces', fieldContext: 'resource' },
{ name: 'name', signal: 'traces', fieldContext: 'span' },
];
describe('logsLoaderConfig', () => {
// Save original location object
const originalWindowLocation = window.location;
@@ -157,4 +167,83 @@ describe('logsLoaderConfig', () => {
} as FormattingOptions,
});
});
describe('Column validation - filtering Traces columns', () => {
it('should filter out Traces columns (name with traces signal) from URL', async () => {
const mixedColumns = [...mockLogsColumns, ...mockTracesColumns];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: mixedColumns,
}),
)}`;
const result = await logsLoaderConfig.url();
// Should only keep logs columns
expect(result.columns).toEqual(mockLogsColumns);
});
it('should filter out Traces columns from localStorage', async () => {
const tracesColumns = [...mockTracesColumns];
mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS] = JSON.stringify({
selectColumns: tracesColumns,
});
const result = await logsLoaderConfig.local();
// Should filter out all Traces columns
expect(result.columns).toEqual([]);
});
it('should accept valid Logs columns from URL', async () => {
const logsColumns = [...mockLogsColumns];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: logsColumns,
}),
)}`;
const result = await logsLoaderConfig.url();
expect(result.columns).toEqual(logsColumns);
});
it('should fall back to defaults when all columns are filtered out from URL', async () => {
const tracesColumns = [...mockTracesColumns];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: tracesColumns,
}),
)}`;
const result = await logsLoaderConfig.url();
// Should return empty array, which triggers fallback to defaults in preferencesLoader
expect(result.columns).toEqual([]);
});
it('should handle columns without signal field (legacy data)', async () => {
const columnsWithoutSignal = [
{ name: 'body', fieldContext: 'log' },
{ name: 'service.name', fieldContext: 'resource' },
];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: columnsWithoutSignal,
}),
)}`;
const result = await logsLoaderConfig.url();
// Without signal field, columns pass through validation
// This matches the current implementation behavior where only columns
// with signal !== 'logs' are filtered out
expect(result.columns).toEqual(columnsWithoutSignal);
});
});
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { LOCALSTORAGE } from 'constants/localStorage';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import {
@@ -126,4 +127,112 @@ describe('tracesLoaderConfig', () => {
columns: defaultTraceSelectedColumns as TelemetryFieldKey[],
});
});
describe('Column validation - filtering Logs columns', () => {
it('should filter out Logs columns (body) from URL', async () => {
const logsColumns = [
{ name: 'timestamp', signal: 'logs', fieldContext: 'log' },
{ name: 'body', signal: 'logs', fieldContext: 'log' },
];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: logsColumns,
}),
)}`;
const result = await tracesLoaderConfig.url();
// Should filter out all Logs columns
expect(result.columns).toEqual([]);
});
it('should filter out Logs columns (timestamp with logs signal) from URL', async () => {
const mixedColumns = [
{ name: 'timestamp', signal: 'logs', fieldContext: 'log' },
{ name: 'service.name', signal: 'traces', fieldContext: 'resource' },
];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: mixedColumns,
}),
)}`;
const result = await tracesLoaderConfig.url();
// Should only keep trace columns
expect(result.columns).toEqual([
{ name: 'service.name', signal: 'traces', fieldContext: 'resource' },
]);
});
it('should filter out Logs columns from localStorage', async () => {
const logsColumns = [
{ name: 'body', signal: 'logs', fieldContext: 'log' },
{ name: 'timestamp', signal: 'logs', fieldContext: 'log' },
];
mockLocalStorage[LOCALSTORAGE.TRACES_LIST_OPTIONS] = JSON.stringify({
selectColumns: logsColumns,
});
const result = await tracesLoaderConfig.local();
// Should filter out all Logs columns
expect(result.columns).toEqual([]);
});
it('should accept valid Trace columns from URL', async () => {
const traceColumns = [
{ name: 'service.name', signal: 'traces', fieldContext: 'resource' },
{ name: 'name', signal: 'traces', fieldContext: 'span' },
];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: traceColumns,
}),
)}`;
const result = await tracesLoaderConfig.url();
expect(result.columns).toEqual(traceColumns);
});
it('should fall back to defaults when all columns are filtered out from URL', async () => {
const logsColumns = [{ name: 'body', signal: 'logs' }];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: logsColumns,
}),
)}`;
const result = await tracesLoaderConfig.url();
// Should return empty array, which triggers fallback to defaults in preferencesLoader
expect(result.columns).toEqual([]);
});
it('should handle columns without signal field (legacy data)', async () => {
const columnsWithoutSignal = [
{ name: 'service.name', fieldContext: 'resource' },
{ name: 'body', fieldContext: 'log' },
];
mockedLocation.search = `?options=${encodeURIComponent(
JSON.stringify({
selectColumns: columnsWithoutSignal,
}),
)}`;
const result = await tracesLoaderConfig.url();
// Without signal field, columns pass through validation
// This matches the current implementation behavior where only columns
// with signal !== 'traces' are filtered out
expect(result.columns).toEqual(columnsWithoutSignal);
});
});
});

View File

@@ -8,6 +8,18 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import { FormattingOptions } from '../types';
/**
* Validates if a column is valid for Logs Explorer
* Filters out Traces-specific columns that would cause query failures
*/
const isValidLogColumn = (col: {
name?: string;
signal?: string;
[key: string]: unknown;
}): boolean =>
// If column has signal field, it must be 'logs'
!(col?.signal && col.signal !== 'logs');
// --- LOGS preferences loader config ---
const logsLoaders = {
local: (): {
@@ -18,8 +30,14 @@ const logsLoaders = {
if (local) {
try {
const parsed = JSON.parse(local);
const localColumns = parsed.selectColumns || [];
// Filter out invalid columns (e.g., Logs columns that might have been incorrectly stored)
const validLogColumns = localColumns.filter(isValidLogColumn);
return {
columns: parsed.selectColumns || [],
columns: validLogColumns.length > 0 ? validLogColumns : [],
formatting: {
maxLines: parsed.maxLines ?? 2,
format: parsed.format ?? 'table',
@@ -38,8 +56,14 @@ const logsLoaders = {
const urlParams = new URLSearchParams(window.location.search);
try {
const options = JSON.parse(urlParams.get('options') || '{}');
const urlColumns = options.selectColumns || [];
// Filter out invalid columns (e.g., Logs columns that might have been incorrectly stored)
const validLogColumns = urlColumns.filter(isValidLogColumn);
return {
columns: options.selectColumns || [],
columns: validLogColumns.length > 0 ? validLogColumns : [],
formatting: {
maxLines: options.maxLines ?? 2,
format: options.format ?? 'table',

View File

@@ -5,6 +5,18 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
/**
* Validates if a column is valid for Traces Explorer
* Filters out Logs-specific columns that would cause query failures
*/
const isValidTraceColumn = (col: {
name?: string;
signal?: string;
[key: string]: unknown;
}): boolean =>
// If column has signal field, it must be 'traces'
!(col?.signal && col.signal !== 'traces');
// --- TRACES preferences loader config ---
const tracesLoaders = {
local: (): {
@@ -14,8 +26,13 @@ const tracesLoaders = {
if (local) {
try {
const parsed = JSON.parse(local);
const localColumns = parsed.selectColumns || [];
// Filter out invalid columns (e.g., Logs columns that might have been incorrectly stored)
const validTraceColumns = localColumns.filter(isValidTraceColumn);
return {
columns: parsed.selectColumns || [],
columns: validTraceColumns.length > 0 ? validTraceColumns : [],
};
} catch {}
}
@@ -27,8 +44,15 @@ const tracesLoaders = {
const urlParams = new URLSearchParams(window.location.search);
try {
const options = JSON.parse(urlParams.get('options') || '{}');
const urlColumns = options.selectColumns || [];
// Filter out invalid columns (e.g., Logs columns)
// Only accept columns that are valid for Traces (signal='traces' or columns without signal that aren't logs-specific)
const validTraceColumns = urlColumns.filter(isValidTraceColumn);
// Only return columns if we have valid trace columns, otherwise return empty to fall back to defaults
return {
columns: options.selectColumns || [],
columns: validTraceColumns.length > 0 ? validTraceColumns : [],
};
} catch {}
return { columns: [] };

View File

@@ -1,18 +0,0 @@
import history from 'lib/history';
import { KeyboardEvent, MouseEvent } from 'react';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
export const genericNavigate = (
link: string,
event?:
| MouseEvent
| KeyboardEvent
| globalThis.MouseEvent
| globalThis.KeyboardEvent,
): void => {
if (event && isCtrlOrMMetaKey(event)) {
window.open(link, '_blank', 'noopener,noreferrer');
} else {
history.push(link);
}
};

View File

@@ -1,9 +0,0 @@
import { KeyboardEvent, MouseEvent } from 'react';
export const isCtrlOrMMetaKey = (
event:
| MouseEvent
| KeyboardEvent
| globalThis.MouseEvent
| globalThis.KeyboardEvent,
): boolean => event.metaKey || event.ctrlKey;

5
go.mod
View File

@@ -55,6 +55,8 @@ require (
github.com/spf13/cobra v1.10.1
github.com/srikanthccv/ClickHouse-go-mock v0.12.0
github.com/stretchr/testify v1.11.1
github.com/swaggest/jsonschema-go v0.3.78
github.com/swaggest/rest v0.2.75
github.com/tidwall/gjson v1.18.0
github.com/uptrace/bun v1.2.9
github.com/uptrace/bun/dialect/pgdialect v1.2.9
@@ -94,6 +96,8 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/swaggest/refl v1.4.0 // indirect
github.com/swaggest/usecase v1.3.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/collector/config/configretry v1.34.0 // indirect
@@ -256,6 +260,7 @@ require (
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggest/openapi-go v0.2.60
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect

28
go.sum
View File

@@ -158,6 +158,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bool64/dev v0.2.40 h1:LUSD+Aq+WB3KwVntqXstevJ0wB12ig1bEgoG8ZafsZU=
github.com/bool64/dev v0.2.40/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -578,6 +582,8 @@ github.com/huandu/go-sqlbuilder v1.35.0 h1:ESvxFHN8vxCTudY1Vq63zYpU5yJBESn19sf6k
github.com/huandu/go-sqlbuilder v1.35.0/go.mod h1:mS0GAtrtW+XL6nM2/gXHRJax2RwSW1TraavWDFAc1JA=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -898,6 +904,8 @@ github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFT
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 h1:levPcBfnazlA1CyCMC3asL/QLZkq9pa8tQZOH513zQw=
github.com/santhosh-tekuri/jsonschema/v3 v3.1.0/go.mod h1:8kzK2TC0k0YjOForaAHdNEa7ik0fokNa2k30BKJ/W7Y=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 h1:KhF0WejiUTDbL5X55nXowP7zNopwpowa6qaMAWyIE+0=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33/go.mod h1:792k1RTU+5JeMXm35/e2Wgp71qPH/DmDoZrRc+EFZDk=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
@@ -910,8 +918,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
@@ -983,6 +991,18 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
github.com/swaggest/jsonschema-go v0.3.78 h1:5+YFQrLxOR8z6CHvgtZc42WRy/Q9zRQQ4HoAxlinlHw=
github.com/swaggest/jsonschema-go v0.3.78/go.mod h1:4nniXBuE+FIGkOGuidjOINMH7OEqZK3HCSbfDuLRI0g=
github.com/swaggest/openapi-go v0.2.60 h1:kglHH/WIfqAglfuWL4tu0LPakqNYySzklUWx06SjSKo=
github.com/swaggest/openapi-go v0.2.60/go.mod h1:jmFOuYdsWGtHU0BOuILlHZQJxLqHiAE6en+baE+QQUk=
github.com/swaggest/refl v1.4.0 h1:CftOSdTqRqs100xpFOT/Rifss5xBV/CT0S/FN60Xe9k=
github.com/swaggest/refl v1.4.0/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA=
github.com/swaggest/rest v0.2.75 h1:MW9zZ3d0kduJ2KdWnSYZIIrZJ1v3Kg+S7QZrDCZcXws=
github.com/swaggest/rest v0.2.75/go.mod h1:yw+PNgpNSdD6W46r60keVXdsBB+7SKt64i2qpeuBsq4=
github.com/swaggest/usecase v1.3.1 h1:JdKV30MTSsDxAXxkldLNcEn8O2uf565khyo6gr5sS+w=
github.com/swaggest/usecase v1.3.1/go.mod h1:cae3lDd5VDmM36OQcOOOdAlEDg40TiQYIp99S9ejWqA=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -1029,6 +1049,10 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@@ -0,0 +1,13 @@
package apiserver
import (
"github.com/gorilla/mux"
)
type APIServer interface {
// Returns the mux router for the API server. Primarily used for collecting OpenAPI operations.
Router() *mux.Router
// Adds the API server routes to an existing router. This is a backwards compatible method for adding routes to the input router.
AddToRouter(router *mux.Router) error
}

View File

@@ -0,0 +1,82 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/domains", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.List), handler.OpenAPIDef{
ID: "ListAuthDomains",
Tags: []string{"authdomains"},
Summary: "List all auth domains",
Description: "This endpoint lists all auth domains",
Request: nil,
RequestContentType: "",
Response: make([]*authtypes.GettableAuthDomain, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/domains", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
ID: "CreateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Create auth domain",
Description: "This endpoint creates an auth domain",
Request: new(authtypes.PostableAuthDomain),
RequestContentType: "application/json",
Response: new(authtypes.GettableAuthDomain),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
ID: "UpdateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Update auth domain",
Description: "This endpoint updates an auth domain",
Request: new(authtypes.UpdateableAuthDomain),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
ID: "DeleteAuthDomain",
Tags: []string{"authdomains"},
Summary: "Delete auth domain",
Description: "This endpoint deletes an auth domain",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,47 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/gorilla/mux"
)
func (provider *provider) addOrgRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authZ.AdminAccess(provider.orgHandler.Get), handler.OpenAPIDef{
ID: "GetMyOrganization",
Tags: []string{"orgs"},
Summary: "Get my organization",
Description: "This endpoint returns the organization I belong to",
Request: nil,
RequestContentType: "",
Response: new(types.Organization),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authZ.AdminAccess(provider.orgHandler.Update), handler.OpenAPIDef{
ID: "UpdateMyOrganization",
Tags: []string{"orgs"},
Summary: "Update my organization",
Description: "This endpoint updates the organization I belong to",
Request: new(types.Organization),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,116 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/preferencetypes"
"github.com/gorilla/mux"
)
func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/user/preferences", handler.New(provider.authZ.ViewAccess(provider.preferenceHandler.ListByUser), handler.OpenAPIDef{
ID: "ListUserPreferences",
Tags: []string{"preferences"},
Summary: "List user preferences",
Description: "This endpoint lists all user preferences",
Request: nil,
RequestContentType: "",
Response: make([]*preferencetypes.Preference, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authZ.ViewAccess(provider.preferenceHandler.GetByUser), handler.OpenAPIDef{
ID: "GetUserPreference",
Tags: []string{"preferences"},
Summary: "Get user preference",
Description: "This endpoint returns the user preference by name",
Request: nil,
RequestContentType: "",
Response: new(preferencetypes.Preference),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authZ.ViewAccess(provider.preferenceHandler.UpdateByUser), handler.OpenAPIDef{
ID: "UpdateUserPreference",
Tags: []string{"preferences"},
Summary: "Update user preference",
Description: "This endpoint updates the user preference by name",
Request: new(preferencetypes.UpdatablePreference),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/org/preferences", handler.New(provider.authZ.AdminAccess(provider.preferenceHandler.ListByOrg), handler.OpenAPIDef{
ID: "ListOrgPreferences",
Tags: []string{"preferences"},
Summary: "List org preferences",
Description: "This endpoint lists all org preferences",
Request: nil,
RequestContentType: "",
Response: make([]*preferencetypes.Preference, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authZ.AdminAccess(provider.preferenceHandler.GetByOrg), handler.OpenAPIDef{
ID: "GetOrgPreference",
Tags: []string{"preferences"},
Summary: "Get org preference",
Description: "This endpoint returns the org preference by name",
Request: nil,
RequestContentType: "",
Response: new(preferencetypes.Preference),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authZ.AdminAccess(provider.preferenceHandler.UpdateByOrg), handler.OpenAPIDef{
ID: "UpdateOrgPreference",
Tags: []string{"preferences"},
Summary: "Update org preference",
Description: "This endpoint updates the org preference by name",
Request: new(preferencetypes.UpdatablePreference),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,115 @@
package signozapiserver
import (
"context"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/gorilla/mux"
)
type provider struct {
config apiserver.Config
settings factory.ScopedProviderSettings
router *mux.Router
authZ *middleware.AuthZ
orgHandler organization.Handler
userHandler user.Handler
sessionHandler session.Handler
authDomainHandler authdomain.Handler
preferenceHandler preference.Handler
}
func NewFactory(
orgGetter organization.Getter,
authz authz.AuthZ,
orgHandler organization.Handler,
userHandler user.Handler,
sessionHandler session.Handler,
authDomainHandler authdomain.Handler,
preferenceHandler preference.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)
})
}
func newProvider(
_ context.Context,
providerSettings factory.ProviderSettings,
config apiserver.Config,
orgGetter organization.Getter,
authz authz.AuthZ,
orgHandler organization.Handler,
userHandler user.Handler,
sessionHandler session.Handler,
authDomainHandler authdomain.Handler,
preferenceHandler preference.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
provider := &provider{
config: config,
settings: settings,
router: router,
orgHandler: orgHandler,
userHandler: userHandler,
sessionHandler: sessionHandler,
authDomainHandler: authDomainHandler,
preferenceHandler: preferenceHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
if err := provider.AddToRouter(router); err != nil {
return nil, err
}
return provider, nil
}
func (provider *provider) Router() *mux.Router {
return provider.router
}
func (provider *provider) AddToRouter(router *mux.Router) error {
if err := provider.addOrgRoutes(router); err != nil {
return err
}
if err := provider.addSessionRoutes(router); err != nil {
return err
}
if err := provider.addAuthDomainRoutes(router); err != nil {
return err
}
if err := provider.addPreferenceRoutes(router); err != nil {
return err
}
if err := provider.addUserRoutes(router); err != nil {
return err
}
return nil
}
func newSecuritySchemes(role types.Role) []handler.OpenAPISecurityScheme {
return []handler.OpenAPISecurityScheme{
{Name: ctxtypes.AuthTypeAPIKey.StringValue(), Scopes: []string{role.String()}},
{Name: ctxtypes.AuthTypeTokenizer.StringValue(), Scopes: []string{role.String()}},
}
}

View File

@@ -0,0 +1,153 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addSessionRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/login", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.DeprecatedCreateSessionByEmailPassword), handler.OpenAPIDef{
ID: "DeprecatedCreateSessionByEmailPassword",
Tags: []string{"sessions"},
Summary: "Deprecated create session by email password",
Description: "This endpoint is deprecated and will be removed in the future",
Request: new(authtypes.DeprecatedPostableLogin),
RequestContentType: "application/json",
Response: new(authtypes.DeprecatedGettableLogin),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: true,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/sessions/email_password", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionByEmailPassword), handler.OpenAPIDef{
ID: "CreateSessionByEmailPassword",
Tags: []string{"sessions"},
Summary: "Create session by email and password",
Description: "This endpoint creates a session for a user using email and password.",
Request: new(authtypes.PostableEmailPasswordSession),
RequestContentType: "application/json",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/sessions/context", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.GetSessionContext), handler.OpenAPIDef{
ID: "GetSessionContext",
Tags: []string{"sessions"},
Summary: "Get session context",
Description: "This endpoint returns the context for the session",
Request: nil,
RequestContentType: "",
Response: new(authtypes.SessionContext),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/sessions/rotate", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.RotateSession), handler.OpenAPIDef{
ID: "RotateSession",
Tags: []string{"sessions"},
Summary: "Rotate session",
Description: "This endpoint rotates the session",
Request: new(authtypes.PostableRotateToken),
RequestContentType: "application/json",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/sessions", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.DeleteSession), handler.OpenAPIDef{
ID: "DeleteSession",
Tags: []string{"sessions"},
Summary: "Delete session",
Description: "This endpoint deletes the session",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: ctxtypes.AuthTypeTokenizer.StringValue()}},
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/complete/google", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionByGoogleCallback), handler.OpenAPIDef{
ID: "CreateSessionByGoogleCallback",
Tags: []string{"sessions"},
Summary: "Create session by google callback",
Description: "This endpoint creates a session for a user using google callback",
Request: nil,
RequestContentType: "",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusSeeOther,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/complete/saml", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionBySAMLCallback), handler.OpenAPIDef{
ID: "CreateSessionBySAMLCallback",
Tags: []string{"sessions"},
Summary: "Create session by saml callback",
Description: "This endpoint creates a session for a user using saml callback",
Request: struct {
RelayState string `form:"RelayState"`
SAMLResponse string `form:"SAMLResponse"`
}{},
RequestContentType: "application/x-www-form-urlencoded",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusSeeOther,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/complete/oidc", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionByOIDCCallback), handler.OpenAPIDef{
ID: "CreateSessionByOIDCCallback",
Tags: []string{"sessions"},
Summary: "Create session by oidc callback",
Description: "This endpoint creates a session for a user using oidc callback",
Request: nil,
RequestContentType: "",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusSeeOther,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,319 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addUserRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateInvite), handler.OpenAPIDef{
ID: "CreateInvite",
Tags: []string{"users"},
Summary: "Create invite",
Description: "This endpoint creates an invite for a user",
Request: new(types.PostableInvite),
RequestContentType: "application/json",
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/invite/bulk", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateBulkInvite), handler.OpenAPIDef{
ID: "CreateBulkInvite",
Tags: []string{"users"},
Summary: "Create bulk invite",
Description: "This endpoint creates a bulk invite for a user",
Request: make([]*types.PostableInvite, 0),
RequestContentType: "application/json",
Response: nil,
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/invite/{token}", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetInvite), handler.OpenAPIDef{
ID: "GetInvite",
Tags: []string{"users"},
Summary: "Get invite",
Description: "This endpoint gets an invite by token",
Request: nil,
RequestContentType: "",
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/invite/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteInvite), handler.OpenAPIDef{
ID: "DeleteInvite",
Tags: []string{"users"},
Summary: "Delete invite",
Description: "This endpoint deletes an invite by id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListInvite), handler.OpenAPIDef{
ID: "ListInvite",
Tags: []string{"users"},
Summary: "List invites",
Description: "This endpoint lists all invites",
Request: nil,
RequestContentType: "",
Response: make([]*types.Invite, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/invite/accept", handler.New(provider.authZ.OpenAccess(provider.userHandler.AcceptInvite), handler.OpenAPIDef{
ID: "AcceptInvite",
Tags: []string{"users"},
Summary: "Accept invite",
Description: "This endpoint accepts an invite by token",
Request: new(types.PostableAcceptInvite),
RequestContentType: "application/json",
Response: new(types.User),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateAPIKey), handler.OpenAPIDef{
ID: "CreateAPIKey",
Tags: []string{"users"},
Summary: "Create api key",
Description: "This endpoint creates an api key",
Request: new(types.PostableAPIKey),
RequestContentType: "application/json",
Response: new(types.GettableAPIKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListAPIKeys), handler.OpenAPIDef{
ID: "ListAPIKeys",
Tags: []string{"users"},
Summary: "List api keys",
Description: "This endpoint lists all api keys",
Request: nil,
RequestContentType: "",
Response: make([]*types.GettableAPIKey, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.UpdateAPIKey), handler.OpenAPIDef{
ID: "UpdateAPIKey",
Tags: []string{"users"},
Summary: "Update api key",
Description: "This endpoint updates an api key",
Request: new(types.StorableAPIKey),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.RevokeAPIKey), handler.OpenAPIDef{
ID: "RevokeAPIKey",
Tags: []string{"users"},
Summary: "Revoke api key",
Description: "This endpoint revokes an api key",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{
ID: "ListUsers",
Tags: []string{"users"},
Summary: "List users",
Description: "This endpoint lists all users",
Request: nil,
RequestContentType: "",
Response: make([]*types.GettableUser, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetMyUser), handler.OpenAPIDef{
ID: "GetMyUser",
Tags: []string{"users"},
Summary: "Get my user",
Description: "This endpoint returns the user I belong to",
Request: nil,
RequestContentType: "",
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: ctxtypes.AuthTypeTokenizer.StringValue()}},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
ID: "GetUser",
Tags: []string{"users"},
Summary: "Get user",
Description: "This endpoint returns the user by id",
Request: nil,
RequestContentType: "",
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
ID: "UpdateUser",
Tags: []string{"users"},
Summary: "Update user",
Description: "This endpoint updates the user by id",
Request: new(types.User),
RequestContentType: "application/json",
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
ID: "DeleteUser",
Tags: []string{"users"},
Summary: "Delete user",
Description: "This endpoint deletes the user by id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/getResetPasswordToken/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetResetPasswordToken), handler.OpenAPIDef{
ID: "GetResetPasswordToken",
Tags: []string{"users"},
Summary: "Get reset password token",
Description: "This endpoint returns the reset password token by id",
Request: nil,
RequestContentType: "",
Response: new(types.ResetPasswordToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/resetPassword", handler.New(provider.authZ.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
ID: "ResetPassword",
Tags: []string{"users"},
Summary: "Reset password",
Description: "This endpoint resets the password by token",
Request: new(types.PostableResetPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/changePassword/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.ChangePassword), handler.OpenAPIDef{
ID: "ChangePassword",
Tags: []string{"users"},
Summary: "Change password",
Description: "This endpoint changes the password by id",
Request: new(types.ChangePasswordRequest),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,88 @@
package handler
import (
"net/http"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/swaggest/openapi-go"
)
type ServeOpenAPIFunc func(openapi.OperationContext)
type Handler interface {
http.Handler
ServeOpenAPI(openapi.OperationContext)
}
type handler struct {
handlerFunc http.HandlerFunc
openAPIDef OpenAPIDef
}
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef) Handler {
// Remove duplicate error status codes
openAPIDef.ErrorStatusCodes = slices.DeleteFunc(openAPIDef.ErrorStatusCodes, func(statusCode int) bool {
return statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden || statusCode == http.StatusInternalServerError
})
// Add internal server error
openAPIDef.ErrorStatusCodes = append(openAPIDef.ErrorStatusCodes, http.StatusInternalServerError)
// Add unauthorized and forbidden status codes
if len(openAPIDef.SecuritySchemes) > 0 {
openAPIDef.ErrorStatusCodes = append(openAPIDef.ErrorStatusCodes, http.StatusUnauthorized, http.StatusForbidden)
}
return &handler{
handlerFunc: handlerFunc,
openAPIDef: openAPIDef,
}
}
func (handler *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
handler.handlerFunc.ServeHTTP(rw, req)
}
func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) {
// Add meta information
opCtx.SetID(handler.openAPIDef.ID)
opCtx.SetTags(handler.openAPIDef.Tags...)
opCtx.SetSummary(handler.openAPIDef.Summary)
opCtx.SetDescription(handler.openAPIDef.Description)
opCtx.SetIsDeprecated(handler.openAPIDef.Deprecated)
// Add security schemes
for _, securityScheme := range handler.openAPIDef.SecuritySchemes {
opCtx.AddSecurity(securityScheme.Name, securityScheme.Scopes...)
}
// Add request structure
opCtx.AddReqStructure(handler.openAPIDef.Request, openapi.WithContentType(handler.openAPIDef.RequestContentType))
// Add success response
if handler.openAPIDef.Response != nil {
opCtx.AddRespStructure(
render.SuccessResponse{Status: render.StatusSuccess.String(), Data: handler.openAPIDef.Response},
openapi.WithContentType(handler.openAPIDef.ResponseContentType),
openapi.WithHTTPStatus(handler.openAPIDef.SuccessStatusCode),
)
} else {
opCtx.AddRespStructure(
nil,
openapi.WithContentType(handler.openAPIDef.ResponseContentType),
openapi.WithHTTPStatus(handler.openAPIDef.SuccessStatusCode),
)
}
// Add error responses
for _, statusCode := range handler.openAPIDef.ErrorStatusCodes {
opCtx.AddRespStructure(
render.ErrorResponse{Status: render.StatusError.String(), Error: &errors.JSON{}},
openapi.WithContentType("application/json"),
openapi.WithHTTPStatus(statusCode),
)
}
}

109
pkg/http/handler/openapi.go Normal file
View File

@@ -0,0 +1,109 @@
package handler
import (
"reflect"
"github.com/gorilla/mux"
"github.com/swaggest/jsonschema-go"
openapigo "github.com/swaggest/openapi-go"
"github.com/swaggest/rest/openapi"
)
// Def is the definition of an OpenAPI operation
type OpenAPIDef struct {
ID string
Tags []string
Summary string
Description string
Request any
RequestContentType string
Response any
ResponseContentType string
SuccessStatusCode int
ErrorStatusCodes []int
Deprecated bool
SecuritySchemes []OpenAPISecurityScheme
}
type OpenAPISecurityScheme struct {
Name string
Scopes []string
}
// Collector is a collector for OpenAPI operations
type OpenAPICollector struct {
collector *openapi.Collector
}
func NewOpenAPICollector(reflector openapigo.Reflector) *OpenAPICollector {
c := openapi.NewCollector(reflector)
return &OpenAPICollector{
collector: c,
}
}
func (c *OpenAPICollector) Walker(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
httpHandler := route.GetHandler()
if httpHandler == nil {
return nil
}
path, err := route.GetPathTemplate()
if err != nil && path == "" {
// If there is no path, skip the route
return nil
}
methods, err := route.GetMethods()
if err != nil {
// If there is no methods, skip the route
return nil
}
if handler, ok := httpHandler.(Handler); ok {
for _, method := range methods {
if err := c.collector.CollectOperation(method, path, c.collect(method, path, handler.ServeOpenAPI)); err != nil {
return err
}
}
return nil
}
return nil
}
func (c *OpenAPICollector) collect(method string, path string, serveOpenAPIFunc ServeOpenAPIFunc) func(oc openapigo.OperationContext) error {
return func(oc openapigo.OperationContext) error {
// Serve the OpenAPI documentation for the handler
serveOpenAPIFunc(oc)
// If the handler has annotations, skip the collection
if c.collector.HasAnnotation(method, path) {
return nil
}
// Automatically sanitize the method and path
_, _, pathItems, err := openapigo.SanitizeMethodPath(method, path)
if err != nil {
return err
}
// If there are path items, add them to the request structure
if len(pathItems) > 0 {
req := jsonschema.Struct{}
for _, p := range pathItems {
req.Fields = append(req.Fields, jsonschema.Field{
Name: "F" + p,
Tag: reflect.StructTag(`path:"` + p + `"`),
Value: "",
})
}
oc.AddReqStructure(req)
}
return nil
}
}

View File

@@ -15,14 +15,18 @@ const (
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type response struct {
type SuccessResponse struct {
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
}
type ErrorResponse struct {
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
Error *errors.JSON `json:"error,omitempty"`
Error *errors.JSON `json:"error"`
}
func Success(rw http.ResponseWriter, httpCode int, data interface{}) {
body, err := json.Marshal(&response{Status: StatusSuccess.s, Data: data})
body, err := json.Marshal(&SuccessResponse{Status: StatusSuccess.s, Data: data})
if err != nil {
Error(rw, err)
return
@@ -64,7 +68,7 @@ func Error(rw http.ResponseWriter, cause error) {
httpCode = http.StatusUnavailableForLegalReasons
}
body, err := json.Marshal(&response{Status: StatusError.s, Error: errors.AsJSON(cause)})
body, err := json.Marshal(&ErrorResponse{Status: StatusError.s, Error: errors.AsJSON(cause)})
if err != nil {
// this should never be the case
http.Error(rw, err.Error(), http.StatusInternalServerError)

View File

@@ -7,3 +7,7 @@ var (
// Defines custom error types
type status struct{ s string }
func (s status) String() string {
return s.s
}

View File

@@ -3,7 +3,7 @@ package impldashboard
import (
"context"
"maps"
"strings"
"slices"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/errors"
@@ -11,30 +11,34 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/queryparser"
"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/dashboardtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store dashboardtypes.Store
settings factory.ScopedProviderSettings
analytics analytics.Analytics
orgGetter organization.Getter
role role.Module
store dashboardtypes.Store
settings factory.ScopedProviderSettings
analytics analytics.Analytics
orgGetter organization.Getter
role role.Module
queryParser queryparser.QueryParser
}
func NewModule(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module) dashboard.Module {
func NewModule(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/impldashboard")
return &module{
store: NewStore(sqlstore),
settings: scopedProviderSettings,
analytics: analytics,
orgGetter: orgGetter,
role: role,
store: NewStore(sqlstore),
settings: scopedProviderSettings,
analytics: analytics,
orgGetter: orgGetter,
role: role,
queryParser: queryParser,
}
}
@@ -269,13 +273,10 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, m
return nil, err
}
// Initialize result map for each metric
result := make(map[string][]map[string]string)
// Process the JSON data in Go
for _, dashboard := range dashboards {
var dashData = dashboard.Data
dashData := dashboard.Data
dashTitle, _ := dashData["title"].(string)
widgets, ok := dashData["widgets"].([]interface{})
if !ok {
@@ -296,44 +297,22 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, m
continue
}
builder, ok := query["builder"].(map[string]interface{})
if !ok {
continue
}
// Track which metrics were found in this widget
foundMetrics := make(map[string]bool)
queryData, ok := builder["queryData"].([]interface{})
if !ok {
continue
}
// Check all three query types
module.checkBuilderQueriesForMetricNames(query, metricNames, foundMetrics)
module.checkClickHouseQueriesForMetricNames(ctx, query, metricNames, foundMetrics)
module.checkPromQLQueriesForMetricNames(ctx, query, metricNames, foundMetrics)
for _, qd := range queryData {
data, ok := qd.(map[string]interface{})
if !ok {
continue
}
if dataSource, ok := data["dataSource"].(string); !ok || dataSource != "metrics" {
continue
}
aggregateAttr, ok := data["aggregateAttribute"].(map[string]interface{})
if !ok {
continue
}
if key, ok := aggregateAttr["key"].(string); ok {
// Check if this metric is in our list of interest
for _, metricName := range metricNames {
if strings.TrimSpace(key) == metricName {
result[metricName] = append(result[metricName], map[string]string{
"dashboard_id": dashboard.ID,
"widget_name": widgetTitle,
"widget_id": widgetID,
"dashboard_name": dashTitle,
})
}
}
}
// Add widget to results for all found metrics
for metricName := range foundMetrics {
result[metricName] = append(result[metricName], map[string]string{
"dashboard_id": dashboard.ID,
"widget_name": widgetTitle,
"widget_id": widgetID,
"dashboard_name": dashTitle,
})
}
}
}
@@ -361,3 +340,120 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
func (module *module) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{dashboardtypes.TypeableMetaResourceDashboard, dashboardtypes.TypeableMetaResourcesDashboards}
}
// checkBuilderQueriesForMetricNames checks builder.queryData[] for aggregations[].metricName
func (module *module) checkBuilderQueriesForMetricNames(query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
builder, ok := query["builder"].(map[string]interface{})
if !ok {
return
}
queryData, ok := builder["queryData"].([]interface{})
if !ok {
return
}
for _, qd := range queryData {
data, ok := qd.(map[string]interface{})
if !ok {
continue
}
// Check dataSource is metrics
if dataSource, ok := data["dataSource"].(string); !ok || dataSource != "metrics" {
continue
}
// Check aggregations[].metricName
aggregations, ok := data["aggregations"].([]interface{})
if !ok {
continue
}
for _, agg := range aggregations {
aggMap, ok := agg.(map[string]interface{})
if !ok {
continue
}
metricName, ok := aggMap["metricName"].(string)
if !ok || metricName == "" {
continue
}
if slices.Contains(metricNames, metricName) {
foundMetrics[metricName] = true
}
}
}
}
// checkClickHouseQueriesForMetricNames checks clickhouse_sql[] array for metric names in query strings
func (module *module) checkClickHouseQueriesForMetricNames(ctx context.Context, query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
clickhouseSQL, ok := query["clickhouse_sql"].([]interface{})
if !ok {
return
}
for _, chQuery := range clickhouseSQL {
chQueryMap, ok := chQuery.(map[string]interface{})
if !ok {
continue
}
queryStr, ok := chQueryMap["query"].(string)
if !ok || queryStr == "" {
continue
}
// Parse query to extract metric names
result, err := module.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypeClickHouseSQL, queryStr)
if err != nil {
// Log warning and continue - parsing errors shouldn't break the search
module.settings.Logger().WarnContext(ctx, "failed to parse ClickHouse query", "query", queryStr, "error", err)
continue
}
// Check if any of the search metric names are in the extracted metric names
for _, metricName := range metricNames {
if slices.Contains(result.MetricNames, metricName) {
foundMetrics[metricName] = true
}
}
}
}
// checkPromQLQueriesForMetricNames checks promql[] array for metric names in query strings
func (module *module) checkPromQLQueriesForMetricNames(ctx context.Context, query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
promQL, ok := query["promql"].([]interface{})
if !ok {
return
}
for _, promQuery := range promQL {
promQueryMap, ok := promQuery.(map[string]interface{})
if !ok {
continue
}
queryStr, ok := promQueryMap["query"].(string)
if !ok || queryStr == "" {
continue
}
// Parse query to extract metric names
result, err := module.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypePromQL, queryStr)
if err != nil {
// Log warning and continue - parsing errors shouldn't break the search
module.settings.Logger().WarnContext(ctx, "failed to parse PromQL query", "query", queryStr, "error", err)
continue
}
// Check if any of the search metric names are in the extracted metric names
for _, metricName := range metricNames {
if slices.Contains(result.MetricNames, metricName) {
foundMetrics[metricName] = true
}
}
}
}

View File

@@ -137,6 +137,28 @@ func (h *handler) GetMetricMetadata(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, metadata)
}
func (h *handler) GetMetricDashboards(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
metricName := strings.TrimSpace(req.URL.Query().Get("metricName"))
if metricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName query parameter is required"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
out, err := h.module.GetMetricDashboards(req.Context(), orgID, metricName)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, out)
}
func (h *handler) GetMetricHighlights(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
@@ -165,7 +187,6 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
render.Error(rw, err)
return
}
var in metricsexplorertypes.MetricAttributesRequest
if err := binding.JSON.BindBody(req.Body, &in); err != nil {
render.Error(rw, err)

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
@@ -32,11 +33,12 @@ type module struct {
condBuilder qbtypes.ConditionBuilder
logger *slog.Logger
cache cache.Cache
dashboardModule dashboard.Module
config metricsexplorer.Config
}
// NewModule constructs the metrics module with the provided dependencies.
func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetrytypes.MetadataStore, cache cache.Cache, providerSettings factory.ProviderSettings, cfg metricsexplorer.Config) metricsexplorer.Module {
func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetrytypes.MetadataStore, cache cache.Cache, dashboardModule dashboard.Module, providerSettings factory.ProviderSettings, cfg metricsexplorer.Config) metricsexplorer.Module {
fieldMapper := telemetrymetrics.NewFieldMapper()
condBuilder := telemetrymetrics.NewConditionBuilder(fieldMapper)
return &module{
@@ -46,6 +48,7 @@ func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetr
logger: providerSettings.Logger,
telemetryMetadataStore: telemetryMetadataStore,
cache: cache,
dashboardModule: dashboardModule,
config: cfg,
}
}
@@ -194,6 +197,34 @@ func (m *module) UpdateMetricMetadata(ctx context.Context, orgID valuer.UUID, re
return nil
}
func (m *module) GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error) {
if metricName == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName is required")
}
data, err := m.dashboardModule.GetByMetricNames(ctx, orgID, []string{metricName})
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get dashboards for metric")
}
dashboards := make([]metricsexplorertypes.MetricDashboard, 0)
if dashboardList, ok := data[metricName]; ok {
dashboards = make([]metricsexplorertypes.MetricDashboard, 0, len(dashboardList))
for _, item := range dashboardList {
dashboards = append(dashboards, metricsexplorertypes.MetricDashboard{
DashboardName: item["dashboard_name"],
DashboardID: item["dashboard_id"],
WidgetID: item["widget_id"],
WidgetName: item["widget_name"],
})
}
}
return &metricsexplorertypes.MetricDashboardsResponse{
Dashboards: dashboards,
}, nil
}
// GetMetricHighlights returns highlights for a metric including data points, last received, total time series, and active time series.
func (m *module) GetMetricHighlights(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricHighlightsResponse, error) {
if metricName == "" {

View File

@@ -15,6 +15,7 @@ type Handler interface {
GetMetricMetadata(http.ResponseWriter, *http.Request)
GetMetricAttributes(http.ResponseWriter, *http.Request)
UpdateMetricMetadata(http.ResponseWriter, *http.Request)
GetMetricDashboards(http.ResponseWriter, *http.Request)
GetMetricHighlights(http.ResponseWriter, *http.Request)
}
@@ -24,6 +25,7 @@ type Module interface {
GetTreemap(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.TreemapRequest) (*metricsexplorertypes.TreemapResponse, error)
GetMetricMetadataMulti(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]*metricsexplorertypes.MetricMetadata, error)
UpdateMetricMetadata(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.UpdateMetricMetadataRequest) error
GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error)
GetMetricHighlights(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricHighlightsResponse, error)
GetMetricAttributes(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.MetricAttributesRequest) (*metricsexplorertypes.MetricAttributesResponse, error)
}

View File

@@ -575,56 +575,12 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/preferences", am.ViewAccess(aH.Signoz.Handlers.Preference.ListByUser)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/preferences/{name}", am.ViewAccess(aH.Signoz.Handlers.Preference.GetByUser)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/preferences/{name}", am.ViewAccess(aH.Signoz.Handlers.Preference.UpdateByUser)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/org/preferences", am.AdminAccess(aH.Signoz.Handlers.Preference.ListByOrg)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/org/preferences/{name}", am.AdminAccess(aH.Signoz.Handlers.Preference.GetByOrg)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/org/preferences/{name}", am.AdminAccess(aH.Signoz.Handlers.Preference.UpdateByOrg)).Methods(http.MethodPut)
// Quick Filters
router.HandleFunc("/api/v1/orgs/me/filters", am.ViewAccess(aH.Signoz.Handlers.QuickFilter.GetQuickFilters)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/orgs/me/filters/{signal}", am.ViewAccess(aH.Signoz.Handlers.QuickFilter.GetSignalFilters)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/orgs/me/filters", am.AdminAccess(aH.Signoz.Handlers.QuickFilter.UpdateQuickFilters)).Methods(http.MethodPut)
// === Authentication APIs ===
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.Signoz.Handlers.User.CreateInvite)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/invite/bulk", am.AdminAccess(aH.Signoz.Handlers.User.CreateBulkInvite)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/{id}", am.AdminAccess(aH.Signoz.Handlers.User.DeleteInvite)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.Signoz.Handlers.User.ListInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(aH.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/register", am.OpenAccess(aH.registerUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/login", am.OpenAccess(aH.Signoz.Handlers.Session.DeprecatedCreateSessionByEmailPassword)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/sessions/email_password", am.OpenAccess(aH.Signoz.Handlers.Session.CreateSessionByEmailPassword)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/sessions/context", am.OpenAccess(aH.Signoz.Handlers.Session.GetSessionContext)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/sessions/rotate", am.OpenAccess(aH.Signoz.Handlers.Session.RotateSession)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/sessions", am.OpenAccess(aH.Signoz.Handlers.Session.DeleteSession)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/complete/google", am.OpenAccess(aH.Signoz.Handlers.Session.CreateSessionByGoogleCallback)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/domains", am.AdminAccess(aH.Signoz.Handlers.AuthDomain.List)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/domains", am.AdminAccess(aH.Signoz.Handlers.AuthDomain.Create)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(aH.Signoz.Handlers.AuthDomain.Update)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(aH.Signoz.Handlers.AuthDomain.Delete)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/pats", am.AdminAccess(aH.Signoz.Handlers.User.CreateAPIKey)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/pats", am.AdminAccess(aH.Signoz.Handlers.User.ListAPIKeys)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(aH.Signoz.Handlers.User.UpdateAPIKey)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(aH.Signoz.Handlers.User.RevokeAPIKey)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/user", am.AdminAccess(aH.Signoz.Handlers.User.ListUsers)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/me", am.OpenAccess(aH.Signoz.Handlers.User.GetMyUser)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/{id}", am.SelfAccess(aH.Signoz.Handlers.User.GetUser)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/{id}", am.SelfAccess(aH.Signoz.Handlers.User.UpdateUser)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/user/{id}", am.AdminAccess(aH.Signoz.Handlers.User.DeleteUser)).Methods(http.MethodDelete)
router.HandleFunc("/api/v2/orgs/me", am.AdminAccess(aH.Signoz.Handlers.Organization.Get)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/orgs/me", am.AdminAccess(aH.Signoz.Handlers.Organization.Update)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/getResetPasswordToken/{id}", am.AdminAccess(aH.Signoz.Handlers.User.GetResetPasswordToken)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/resetPassword", am.OpenAccess(aH.Signoz.Handlers.User.ResetPassword)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/changePassword/{id}", am.SelfAccess(aH.Signoz.Handlers.User.ChangePassword)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, []any{})
@@ -674,6 +630,7 @@ func (ah *APIHandler) MetricExplorerRoutes(router *mux.Router, am *middleware.Au
router.HandleFunc("/api/v2/metrics/metadata", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricMetadata)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metrics/{metric_name}/metadata", am.EditAccess(ah.Signoz.Handlers.MetricsExplorer.UpdateMetricMetadata)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/metric/highlights", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricHighlights)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metric/dashboards", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricDashboards)).Methods(http.MethodGet)
}
func Intersection(a, b []int) (c []int) {

View File

@@ -223,6 +223,11 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
api.MetricExplorerRoutes(r, am)
api.RegisterTraceFunnelsRoutes(r, am)
err := s.signoz.APIServer.AddToRouter(r)
if err != nil {
return nil, err
}
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
@@ -233,7 +238,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
handler = handlers.CompressHandler(handler)
err := web.AddToRouter(r)
err = web.AddToRouter(r)
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,224 @@
package rules
import (
"strings"
"time"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
)
// CalculateEvalDelay determines if the default evaluation delay can be removed (set to 0)
// based on the rule's match type, compare operator, and aggregation type.
// If the combination ensures that new data will not invalidate the alert condition
// (e.g. values only increase for a "Greater Than" check), the delay is removed.
//
// A combination is considered "safe" if new data arriving late cannot invalidate
// a previously triggered alert condition. This happens when:
// - The aggregation function is monotonic (only increases or only decreases)
// - The comparison operator aligns with the monotonic direction
// - The match type allows the safety property to hold
//
// Safe combinations include:
// - Min aggregation + Below/BelowOrEq operators (Min can only decrease)
// - Max aggregation + Above/AboveOrEq operators (Max can only increase)
// - Count/CountDistinct + Above/AboveOrEq operators (Count can only increase)
//
// Returns 0 if all queries are safe, otherwise returns defaultDelay.
func CalculateEvalDelay(rule *ruletypes.PostableRule, defaultDelay time.Duration) time.Duration {
// Phase 1: Validate rule condition
if !isRuleConditionValid(rule) {
return defaultDelay
}
// Phase 2: Get match type and compare operator from thresholds
matchType, compareOp, ok := getThresholdMatchTypeAndCompareOp(rule)
if !ok {
return defaultDelay
}
// Phase 3: Check if all queries are safe
for _, query := range rule.RuleCondition.CompositeQuery.Queries {
if !isQuerySafe(query, matchType, compareOp) {
return defaultDelay
}
}
// Phase 4: All queries are safe, delay can be removed
return 0
}
// isRuleConditionValid checks if the rule condition is valid for delay calculation.
// Returns false if the rule condition is nil, has no queries, or has invalid thresholds.
func isRuleConditionValid(rule *ruletypes.PostableRule) bool {
if rule.RuleCondition == nil || rule.RuleCondition.CompositeQuery == nil {
return false
}
// BuilderQueries, PromQL Queries, ClickHouse SQL Queries attributes of CompositeQuery
// are not supported for now, only Queries attribute is supported
if len(rule.RuleCondition.CompositeQuery.Queries) == 0 {
return false
}
// Validate that thresholds exist and contain valid match type and compare operator
matchType, compareOp, ok := getThresholdMatchTypeAndCompareOp(rule)
if !ok {
return false
}
if matchType == ruletypes.MatchTypeNone || compareOp == ruletypes.CompareOpNone {
return false
}
return true
}
// getThresholdMatchTypeAndCompareOp extracts match type and compare operator from the rule's thresholds.
// Returns the match type, compare operator, and a boolean indicating success.
// All thresholds share the same match type and compare operator, so we use the first threshold's values.
func getThresholdMatchTypeAndCompareOp(rule *ruletypes.PostableRule) (ruletypes.MatchType, ruletypes.CompareOp, bool) {
if rule.RuleCondition == nil || rule.RuleCondition.Thresholds == nil {
return ruletypes.MatchTypeNone, ruletypes.CompareOpNone, false
}
// Get the threshold interface
threshold, err := rule.RuleCondition.Thresholds.GetRuleThreshold()
if err != nil {
return ruletypes.MatchTypeNone, ruletypes.CompareOpNone, false
}
// Cast to BasicRuleThresholds (only supported kind)
basicThresholds, ok := threshold.(ruletypes.BasicRuleThresholds)
if !ok || len(basicThresholds) == 0 {
return ruletypes.MatchTypeNone, ruletypes.CompareOpNone, false
}
// Use first threshold's MatchType and CompareOp (all thresholds share the same values)
matchType := basicThresholds[0].MatchType
compareOp := basicThresholds[0].CompareOp
return matchType, compareOp, true
}
// aggregationExpressionToTimeAggregation converts the aggregation expression to the corresponding time aggregation
// based on the expression
// if the expression is not a valid aggregation expression, it returns the unspecified time aggregation
// Note: Longer/more specific prefixes (e.g., "count_distinct") must be checked before shorter ones (e.g., "count")
func aggregationExpressionToTimeAggregation(expression string) metrictypes.TimeAggregation {
expression = strings.TrimSpace(strings.ToLower(expression))
switch {
case strings.HasPrefix(expression, "count_distinct"):
return metrictypes.TimeAggregationCountDistinct
case strings.HasPrefix(expression, "count"):
return metrictypes.TimeAggregationCount
case strings.HasPrefix(expression, "min"):
return metrictypes.TimeAggregationMin
case strings.HasPrefix(expression, "max"):
return metrictypes.TimeAggregationMax
case strings.HasPrefix(expression, "avg"):
return metrictypes.TimeAggregationAvg
case strings.HasPrefix(expression, "sum"):
return metrictypes.TimeAggregationSum
case strings.HasPrefix(expression, "rate"):
return metrictypes.TimeAggregationRate
case strings.HasPrefix(expression, "increase"):
return metrictypes.TimeAggregationIncrease
case strings.HasPrefix(expression, "latest"):
return metrictypes.TimeAggregationLatest
default:
return metrictypes.TimeAggregationUnspecified
}
}
// extractTimeAggFromQuerySpec extracts the time aggregation from the query spec of the QueryEnvelope
func extractTimeAggFromQuerySpec(spec any) []metrictypes.TimeAggregation {
timeAggs := []metrictypes.TimeAggregation{}
// Extract the time aggregation from the query spec
// based on different types of query spec
switch spec := spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
for _, agg := range spec.Aggregations {
timeAggs = append(timeAggs, agg.TimeAggregation)
}
// the log and trace aggregations don't store the time aggregation directly but expression for the aggregation
// so we need to convert the expression to the corresponding time aggregation
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
for _, agg := range spec.Aggregations {
timeAggs = append(timeAggs, aggregationExpressionToTimeAggregation(agg.Expression))
}
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
for _, agg := range spec.Aggregations {
timeAggs = append(timeAggs, aggregationExpressionToTimeAggregation(agg.Expression))
}
}
return timeAggs
}
// isQuerySafe determines if a single query is safe to remove the eval delay.
// A query is safe only if it's a Builder query (with MetricAggregation, LogAggregation, or TraceAggregation type)
// and all its aggregations are safe.
func isQuerySafe(query qbtypes.QueryEnvelope, matchType ruletypes.MatchType, compareOp ruletypes.CompareOp) bool {
// We only handle Builder Queries for now
if query.Type != qbtypes.QueryTypeBuilder {
return false
}
// extract time aggregations from the query spec
timeAggs := extractTimeAggFromQuerySpec(query.Spec)
// A query must have at least one aggregation
if len(timeAggs) == 0 {
return false
}
// All aggregations in the query must be safe
for _, timeAgg := range timeAggs {
if !isAggregationSafe(timeAgg, matchType, compareOp) {
return false
}
}
return true
}
// isAggregationSafe checks if the aggregation is safe to remove the eval delay
func isAggregationSafe(timeAgg metrictypes.TimeAggregation, matchType ruletypes.MatchType, compareOp ruletypes.CompareOp) bool {
switch timeAgg {
case metrictypes.TimeAggregationMin:
// Group: Min, MinIf
// Value can only go down or remain same.
if matchType == ruletypes.AtleastOnce || matchType == ruletypes.AllTheTimes {
if compareOp == ruletypes.ValueIsBelow || compareOp == ruletypes.ValueBelowOrEq {
return true
}
}
case metrictypes.TimeAggregationMax:
// Group: Max, MaxIf
// Value can only go up or remain same.
if matchType == ruletypes.AtleastOnce || matchType == ruletypes.AllTheTimes {
if compareOp == ruletypes.ValueIsAbove || compareOp == ruletypes.ValueAboveOrEq {
return true
}
}
case metrictypes.TimeAggregationCount, metrictypes.TimeAggregationCountDistinct:
// Group: Count
// Value can only go up or remain same.
if matchType == ruletypes.AtleastOnce || matchType == ruletypes.AllTheTimes {
if compareOp == ruletypes.ValueIsAbove || compareOp == ruletypes.ValueAboveOrEq {
return true
}
}
// Other aggregations (Sum, Avg, Rate, etc.) are not safe.
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@@ -154,6 +154,8 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
// calculate eval delay based on rule config
evalDelay := CalculateEvalDelay(opts.Rule, opts.ManagerOpts.EvalDelay)
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
@@ -164,7 +166,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
opts.Reader,
opts.Querier,
opts.SLogger,
WithEvalDelay(opts.ManagerOpts.EvalDelay),
WithEvalDelay(evalDelay),
WithSQLStore(opts.SQLStore),
)

View File

@@ -5,16 +5,10 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/preference/implpreference"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
@@ -23,29 +17,20 @@ import (
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/services"
"github.com/SigNoz/signoz/pkg/modules/services/implservices"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/session/implsession"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/querier"
)
type Handlers struct {
Organization organization.Handler
Preference preference.Handler
User user.Handler
SavedView savedview.Handler
Apdex apdex.Handler
Dashboard dashboard.Handler
QuickFilter quickfilter.Handler
TraceFunnel tracefunnel.Handler
RawDataExport rawdataexport.Handler
AuthDomain authdomain.Handler
Session session.Handler
SpanPercentile spanpercentile.Handler
Services services.Handler
MetricsExplorer metricsexplorer.Handler
@@ -53,17 +38,12 @@ type Handlers struct {
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing) Handlers {
return Handlers{
Organization: implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter),
Preference: implpreference.NewHandler(modules.Preference),
User: impluser.NewHandler(modules.User, modules.UserGetter),
SavedView: implsavedview.NewHandler(modules.SavedView),
Apdex: implapdex.NewHandler(modules.Apdex),
Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings, querier, licensing),
QuickFilter: implquickfilter.NewHandler(modules.QuickFilter),
TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel),
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),
AuthDomain: implauthdomain.NewHandler(modules.AuthDomain),
Session: implsession.NewHandler(modules.Session),
Services: implservices.NewHandler(modules.Services),
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -35,8 +36,9 @@ func TestNewHandlers(t *testing.T) {
require.NoError(t, err)
tokenizer := tokenizertest.New()
emailing := emailingtest.New()
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, Config{})
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{})
handlers := NewHandlers(modules, providerSettings, nil, nil)

View File

@@ -38,6 +38,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/tokenizer"
@@ -79,12 +80,14 @@ func NewModules(
authNs map[authtypes.AuthNProvider]authn.AuthN,
authz authz.AuthZ,
cache cache.Cache,
queryParser queryparser.QueryParser,
config Config,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics)
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
dashboard := impldashboard.NewModule(sqlstore, providerSettings, analytics, orgGetter, implrole.NewModule(implrole.NewStore(sqlstore), authz, nil), queryParser)
return Modules{
OrgGetter: orgGetter,
@@ -92,7 +95,7 @@ func NewModules(
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: impldashboard.NewModule(sqlstore, providerSettings, analytics, orgGetter, implrole.NewModule(implrole.NewStore(sqlstore), authz, nil)),
Dashboard: dashboard,
User: user,
UserGetter: userGetter,
QuickFilter: quickfilter,
@@ -102,6 +105,6 @@ func NewModules(
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, providerSettings, config.MetricsExplorer),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, dashboard, providerSettings, config.MetricsExplorer),
}
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -35,8 +36,9 @@ func TestNewModules(t *testing.T) {
require.NoError(t, err)
tokenizer := tokenizertest.New()
emailing := emailingtest.New()
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, Config{})
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{})
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

83
pkg/signoz/openapi.go Normal file
View File

@@ -0,0 +1,83 @@
package signoz
import (
"context"
"os"
"reflect"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/swaggest/jsonschema-go"
"github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
)
type OpenAPI struct {
apiserver apiserver.APIServer
reflector *openapi3.Reflector
collector *handler.OpenAPICollector
}
func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumentation) (*OpenAPI, error) {
apiserver, err := signozapiserver.NewFactory(
struct{ organization.Getter }{},
struct{ authz.AuthZ }{},
struct{ organization.Handler }{},
struct{ user.Handler }{},
struct{ session.Handler }{},
struct{ authdomain.Handler }{},
struct{ preference.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err
}
reflector := openapi3.NewReflector()
reflector.JSONSchemaReflector().DefaultOptions = append(reflector.JSONSchemaReflector().DefaultOptions, jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
if defaultDefName == "RenderSuccessResponse" {
field, ok := t.FieldByName("Data")
if !ok {
return defaultDefName
}
return field.Type.Name()
}
return defaultDefName
}))
reflector.SpecSchema().SetTitle("SigNoz")
reflector.SpecSchema().SetDescription("OpenTelemetry-Native Logs, Metrics and Traces in a single pane")
reflector.SpecSchema().SetAPIKeySecurity(ctxtypes.AuthTypeAPIKey.StringValue(), "SigNoz-Api-Key", openapi.InHeader, "API Keys")
reflector.SpecSchema().SetHTTPBearerTokenSecurity(ctxtypes.AuthTypeTokenizer.StringValue(), "Tokenizer", "Tokens generated by the tokenizer")
collector := handler.NewOpenAPICollector(reflector)
return &OpenAPI{
apiserver: apiserver,
reflector: reflector,
collector: collector,
}, nil
}
func (openapi *OpenAPI) CreateAndWrite(path string) error {
if err := openapi.apiserver.Router().Walk(openapi.collector.Walker); err != nil {
return err
}
spec, err := openapi.reflector.Spec.MarshalYAML()
if err != nil {
return err
}
return os.WriteFile(path, spec, 0o600)
}

View File

@@ -8,6 +8,9 @@ import (
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/analytics/noopanalytics"
"github.com/SigNoz/signoz/pkg/analytics/segmentanalytics"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/cache/memorycache"
"github.com/SigNoz/signoz/pkg/cache/rediscache"
@@ -15,8 +18,13 @@ 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/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/preference/implpreference"
"github.com/SigNoz/signoz/pkg/modules/session/implsession"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheus"
"github.com/SigNoz/signoz/pkg/querier"
@@ -213,6 +221,20 @@ func NewQuerierProviderFactories(telemetryStore telemetrystore.TelemetryStore, p
)
}
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
return factory.MustNewNamedMap(
signozapiserver.NewFactory(
orgGetter,
authz,
implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter),
impluser.NewHandler(modules.User, modules.UserGetter),
implsession.NewHandler(modules.Session),
implauthdomain.NewHandler(modules.AuthDomain),
implpreference.NewHandler(modules.Preference),
),
)
}
func NewTokenizerProviderFactories(cache cache.Cache, sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.NamedMap[factory.ProviderFactory[tokenizer.Tokenizer, tokenizer.Config]] {
tokenStore := sqltokenizerstore.NewStore(sqlstore)
return factory.MustNewNamedMap(

View File

@@ -78,4 +78,13 @@ func TestNewProviderFactories(t *testing.T) {
telemetryStore := telemetrystoretest.New(telemetrystore.Config{Provider: "clickhouse"}, sqlmock.QueryMatcherEqual)
NewStatsReporterProviderFactories(telemetryStore, []statsreporter.StatsCollector{}, orgGetter, userGetter, tokenizertest.New(), version.Build{}, analytics.Config{Enabled: true})
})
assert.NotPanics(t, func() {
NewAPIServerProviderFactories(
implorganization.NewGetter(implorganization.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)), nil),
nil,
Modules{},
Handlers{},
)
})
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/sqlroutingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authn/authnstore/sqlauthnstore"
"github.com/SigNoz/signoz/pkg/authz"
@@ -54,6 +55,7 @@ type SigNoz struct {
Prometheus prometheus.Prometheus
Alertmanager alertmanager.Alertmanager
Querier querier.Querier
APIServer apiserver.APIServer
Zeus zeus.Zeus
Licensing licensing.Licensing
Emailing emailing.Emailing
@@ -344,11 +346,23 @@ func New(
)
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, config)
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)
// Initialize the API server
apiserver, err := factory.NewProviderFromNamedMap(
ctx,
providerSettings,
config.APIServer,
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers),
"signoz",
)
if err != nil {
return nil, err
}
// Create a list of all stats collectors
statsCollectors := []statsreporter.StatsCollector{
alertmanager,
@@ -399,6 +413,7 @@ func New(
Prometheus: prometheus,
Alertmanager: alertmanager,
Querier: querier,
APIServer: apiserver,
Zeus: zeus,
Licensing: licensing,
Emailing: emailing,

View File

@@ -221,6 +221,19 @@ type TreemapResponse struct {
Samples []TreemapEntry `json:"samples"`
}
// MetricDashboard represents a dashboard/widget referencing a metric.
type MetricDashboard struct {
DashboardName string `json:"dashboardName"`
DashboardID string `json:"dashboardId"`
WidgetID string `json:"widgetId"`
WidgetName string `json:"widgetName"`
}
// MetricDashboardsResponse represents the response for metric dashboards endpoint.
type MetricDashboardsResponse struct {
Dashboards []MetricDashboard `json:"dashboards"`
}
// MetricHighlightsResponse is the output structure for the metric highlights endpoint.
type MetricHighlightsResponse struct {
DataPoints uint64 `json:"dataPoints"`

View File

@@ -10,6 +10,7 @@ import (
type Web interface {
// AddToRouter adds the web routes to an existing router.
AddToRouter(router *mux.Router) error
// ServeHTTP serves the web routes.
http.Handler
}