mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-27 18:04:52 +00:00
Compare commits
8 Commits
feat/cmd-c
...
feat/intro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6f5b3e840 | ||
|
|
bef71a8aa9 | ||
|
|
67243a648e | ||
|
|
9c5a2aba3d | ||
|
|
ca47e471b2 | ||
|
|
529a9e7009 | ||
|
|
b00687b43f | ||
|
|
8771919de6 |
16
.github/workflows/goci.yaml
vendored
16
.github/workflows/goci.yaml
vendored
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
21
cmd/generate.go
Normal 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
41
cmd/openapi.go
Normal 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
|
||||
}
|
||||
@@ -271,3 +271,9 @@ tokenizer:
|
||||
token:
|
||||
# The maximum number of tokens a user can have. This limits the number of concurrent sessions a user can have.
|
||||
max_per_user: 5
|
||||
|
||||
##################### Flagger #####################
|
||||
flagger:
|
||||
# Config are the overrides for the feature flags which come directly from the config file.
|
||||
config:
|
||||
enable_interpolation: true
|
||||
|
||||
2293
docs/api/openapi.yml
Normal file
2293
docs/api/openapi.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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}`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { KeyboardEvent, MouseEvent } from 'react';
|
||||
|
||||
export const isCtrlOrMMetaKey = (
|
||||
event:
|
||||
| MouseEvent
|
||||
| KeyboardEvent
|
||||
| globalThis.MouseEvent
|
||||
| globalThis.KeyboardEvent,
|
||||
): boolean => event.metaKey || event.ctrlKey;
|
||||
21
go.mod
21
go.mod
@@ -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
|
||||
@@ -72,12 +74,12 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/text v0.28.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/text v0.32.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -94,11 +96,14 @@ 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
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
@@ -219,6 +224,7 @@ require (
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/open-feature/go-sdk v1.17.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.128.0 // indirect
|
||||
@@ -256,6 +262,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
|
||||
@@ -331,10 +338,10 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
google.golang.org/api v0.236.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
|
||||
64
go.sum
64
go.sum
@@ -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=
|
||||
@@ -756,6 +762,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk=
|
||||
github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw=
|
||||
github.com/open-telemetry/opamp-go v0.19.0 h1:8LvQKDwqi+BU3Yy159SU31e2XB0vgnk+PN45pnKilPs=
|
||||
github.com/open-telemetry/opamp-go v0.19.0/go.mod h1:9/1G6T5dnJz4cJtoYSr6AX18kHdOxnxxETJPZSHyEUg=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.128.0 h1:T5IE0l1qcIg6dkHui4hHe+qj3VzuMwpnhrUyubyCwO0=
|
||||
@@ -898,6 +906,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 +920,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 +993,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 +1051,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=
|
||||
@@ -1258,8 +1284,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1297,8 +1323,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1347,8 +1373,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1383,8 +1409,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1471,12 +1497,12 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1487,8 +1513,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1551,8 +1577,10 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
13
pkg/apiserver/apiserver.go
Normal file
13
pkg/apiserver/apiserver.go
Normal 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
|
||||
}
|
||||
82
pkg/apiserver/signozapiserver/authdomain.go
Normal file
82
pkg/apiserver/signozapiserver/authdomain.go
Normal 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
|
||||
}
|
||||
47
pkg/apiserver/signozapiserver/org.go
Normal file
47
pkg/apiserver/signozapiserver/org.go
Normal 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
|
||||
}
|
||||
116
pkg/apiserver/signozapiserver/preference.go
Normal file
116
pkg/apiserver/signozapiserver/preference.go
Normal 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
|
||||
}
|
||||
115
pkg/apiserver/signozapiserver/provider.go
Normal file
115
pkg/apiserver/signozapiserver/provider.go
Normal 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.addUserRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addPreferenceRoutes(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()}},
|
||||
}
|
||||
}
|
||||
153
pkg/apiserver/signozapiserver/session.go
Normal file
153
pkg/apiserver/signozapiserver/session.go
Normal 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
|
||||
}
|
||||
319
pkg/apiserver/signozapiserver/user.go
Normal file
319
pkg/apiserver/signozapiserver/user.go
Normal 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
|
||||
}
|
||||
25
pkg/flagger/config.go
Normal file
25
pkg/flagger/config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package flagger
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/factory"
|
||||
|
||||
type Config struct {
|
||||
// Config are the overrides for the feature flags which come directly from the config file.
|
||||
Config map[string]any `mapstructure:"config"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(
|
||||
factory.MustNewName("flagger"), newConfig,
|
||||
)
|
||||
}
|
||||
|
||||
// newConfig creates a new config with the default values.
|
||||
func newConfig() factory.Config {
|
||||
return &Config{
|
||||
Config: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
313
pkg/flagger/configflagger/configflagger.go
Normal file
313
pkg/flagger/configflagger/configflagger.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package configflagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
config flagger.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
// This is the default registry that will be containing all the supported features along with there all possible variants
|
||||
defaultRegistry featuretypes.Registry
|
||||
// These are the feature variants that are configured in the config file and will be used as overrides
|
||||
featureVariants map[featuretypes.Name]featuretypes.FeatureVariant
|
||||
}
|
||||
|
||||
func NewFactory(defaultRegistry featuretypes.Registry) factory.ProviderFactory[flagger.Provider, flagger.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("config"), func(ctx context.Context, ps factory.ProviderSettings, c flagger.Config) (flagger.Provider, error) {
|
||||
return New(ctx, ps, c, defaultRegistry)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, defaultRegistry featuretypes.Registry) (flagger.Provider, error) {
|
||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger/configflagger")
|
||||
|
||||
featureVariants := make(map[featuretypes.Name]featuretypes.FeatureVariant)
|
||||
|
||||
// read all the values from the config and build the featureVariants map
|
||||
for key, value := range c.Config {
|
||||
// Check if the feature is valid
|
||||
feature, _, err := defaultRegistry.GetByString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if feature.Kind == featuretypes.KindObject {
|
||||
// simply add the value to the featureVariants map
|
||||
featureVariants[feature.Name] = featuretypes.FeatureVariant{
|
||||
Variant: featuretypes.MustNewName("from_config"),
|
||||
Value: value,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
convertedValue, err := convertValueToKind(value, featuretypes.Kind(feature.Kind))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if the value is valid
|
||||
if ok, err := featuretypes.IsValidValue(feature, convertedValue); err != nil || !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the variant by value
|
||||
variant, err := featuretypes.VariantByValue(feature, convertedValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add the variant to the featureVariants map
|
||||
featureVariants[feature.Name] = *variant
|
||||
}
|
||||
|
||||
return &provider{
|
||||
config: c,
|
||||
settings: settings,
|
||||
defaultRegistry: defaultRegistry,
|
||||
featureVariants: featureVariants,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Metadata() openfeature.Metadata {
|
||||
return openfeature.Metadata{
|
||||
Name: "config",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[bool](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: variant.Value.(bool),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.BoolResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[float64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: variant.Value.(float64),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.FloatResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[string](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: variant.Value.(string),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.StringResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[int64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: variant.Value.(int64),
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.IntResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
|
||||
// check if the feature is present in the default registry
|
||||
feature, detail, err := p.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
value, detail, err := featuretypes.VariantValue[any](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: defaultValue,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// check if the feature is present in the featureVariants map
|
||||
variant, ok := p.featureVariants[feature.Name]
|
||||
if ok {
|
||||
// return early as we have found the value in the featureVariants map
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: variant.Value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// return the value from the default registry we found earlier
|
||||
return openfeature.InterfaceResolutionDetail{
|
||||
Value: value,
|
||||
ProviderResolutionDetail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) Hooks() []openfeature.Hook {
|
||||
return []openfeature.Hook{}
|
||||
}
|
||||
|
||||
func (p *provider) List(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func convertValueToKind(value any, kind featuretypes.Kind) (any, error) {
|
||||
switch kind {
|
||||
case featuretypes.KindBoolean:
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
return v, nil
|
||||
case string:
|
||||
return strconv.ParseBool(v)
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot convert %T to bool", value)
|
||||
}
|
||||
case featuretypes.KindString:
|
||||
return fmt.Sprintf("%v", value), nil
|
||||
case featuretypes.KindInt:
|
||||
switch v := value.(type) {
|
||||
case int64:
|
||||
return v, nil
|
||||
case int:
|
||||
return int64(v), nil
|
||||
case float64:
|
||||
return int64(v), nil
|
||||
case string:
|
||||
return strconv.ParseInt(v, 10, 64)
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot convert %T to int64", value)
|
||||
}
|
||||
case featuretypes.KindFloat:
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
return v, nil
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case string:
|
||||
return strconv.ParseFloat(v, 64)
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot convert %T to float64", value)
|
||||
}
|
||||
default:
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
284
pkg/flagger/flagger.go
Normal file
284
pkg/flagger/flagger.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package flagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// This is the consumer facing interface for the Flagger service.
|
||||
type Flagger interface {
|
||||
Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, string, error)
|
||||
String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, string, error)
|
||||
Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, string, error)
|
||||
Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, string, error)
|
||||
Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, string, error)
|
||||
List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeatureWithResolution, error)
|
||||
}
|
||||
|
||||
// This is the concrete implementation of the Flagger interface.
|
||||
type flagger struct {
|
||||
defaultRegistry featuretypes.Registry
|
||||
settings factory.ScopedProviderSettings
|
||||
providers map[string]Provider
|
||||
clients map[string]*openfeature.Client
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, config Config, defaultRegistry featuretypes.Registry, factories ...factory.ProviderFactory[Provider, Config]) (Flagger, error) {
|
||||
|
||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger")
|
||||
|
||||
providers := make(map[string]Provider)
|
||||
clients := make(map[string]*openfeature.Client)
|
||||
|
||||
for _, factory := range factories {
|
||||
provider, err := factory.New(ctx, ps, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers[provider.Metadata().Name] = provider
|
||||
|
||||
openfeatureClient := openfeature.NewClient(provider.Metadata().Name)
|
||||
|
||||
if err := openfeature.SetNamedProviderAndWait(provider.Metadata().Name, provider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clients[provider.Metadata().Name] = openfeatureClient
|
||||
}
|
||||
|
||||
return &flagger{
|
||||
defaultRegistry: defaultRegistry,
|
||||
settings: settings,
|
||||
providers: providers,
|
||||
clients: clients,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, string, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[bool](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.BooleanValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, client.Metadata().Domain(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, "defaultRegistry", nil
|
||||
}
|
||||
|
||||
func (f *flagger) String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, string, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[string](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.StringValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, client.Metadata().Domain(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, "defaultRegistry", nil
|
||||
}
|
||||
|
||||
func (f *flagger) Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, string, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[float64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.FloatValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, client.Metadata().Domain(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, "defaultRegistry", nil
|
||||
}
|
||||
|
||||
func (f *flagger) Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, string, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[int64](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.IntValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, client.Metadata().Domain(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, "defaultRegistry", nil
|
||||
}
|
||||
|
||||
func (f *flagger) Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, string, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.defaultRegistry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// get the default value from the feature from default registry
|
||||
defaultValue, _, err := featuretypes.VariantValue[any](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
// something which should never happen
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.ObjectValue(ctx, flag, defaultValue, evalCtx.Ctx())
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if value != defaultValue {
|
||||
return value, client.Metadata().Domain(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, "defaultRegistry", nil
|
||||
}
|
||||
|
||||
func (f *flagger) List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeatureWithResolution, error) {
|
||||
// get all the feature from the default registry
|
||||
features := f.defaultRegistry.List()
|
||||
|
||||
result := make([]*featuretypes.GettableFeatureWithResolution, 0, len(features))
|
||||
|
||||
for _, feature := range features {
|
||||
|
||||
variants := make(map[string]any, len(feature.Variants))
|
||||
for name, variant := range feature.Variants {
|
||||
variants[name.String()] = variant.Value
|
||||
}
|
||||
|
||||
var resolvedValue any
|
||||
var source string
|
||||
var err error
|
||||
|
||||
switch feature.Kind {
|
||||
case featuretypes.KindBoolean:
|
||||
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case featuretypes.KindString:
|
||||
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case featuretypes.KindFloat:
|
||||
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case featuretypes.KindInt:
|
||||
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case featuretypes.KindObject:
|
||||
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, &featuretypes.GettableFeatureWithResolution{
|
||||
Name: feature.Name.String(),
|
||||
Kind: feature.Kind.StringValue(),
|
||||
Stage: feature.Stage.StringValue(),
|
||||
Description: feature.Description,
|
||||
DefaultVariant: feature.DefaultVariant.String(),
|
||||
Variants: variants,
|
||||
ResolvedValue: resolvedValue,
|
||||
ValueSource: source,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
16
pkg/flagger/provider.go
Normal file
16
pkg/flagger/provider.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package flagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// Any feature flag provider has to implement this interface.
|
||||
type Provider interface {
|
||||
openfeature.FeatureProvider
|
||||
|
||||
// List returns all the feature flags
|
||||
List(ctx context.Context) ([]*featuretypes.GettableFeature, error)
|
||||
}
|
||||
34
pkg/flagger/registry.go
Normal file
34
pkg/flagger/registry.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package flagger
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
|
||||
var (
|
||||
FeatureEnableInterpolation = featuretypes.MustNewName("enable_interpolation")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
registry, err := featuretypes.NewRegistry(
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureEnableInterpolation,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageStable,
|
||||
Description: "Enable interpolation in statement builder",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: map[featuretypes.Name]featuretypes.FeatureVariant{
|
||||
featuretypes.MustNewName("disabled"): {
|
||||
Variant: featuretypes.MustNewName("disabled"),
|
||||
Value: false,
|
||||
},
|
||||
featuretypes.MustNewName("enabled"): {
|
||||
Variant: featuretypes.MustNewName("enabled"),
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
88
pkg/http/handler/handler.go
Normal file
88
pkg/http/handler/handler.go
Normal 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
109
pkg/http/handler/openapi.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -7,3 +7,7 @@ var (
|
||||
|
||||
// Defines custom error types
|
||||
type status struct{ s string }
|
||||
|
||||
func (s status) String() string {
|
||||
return s.s
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
@@ -565,6 +566,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/features", am.ViewAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/features", am.ViewAccess(aH.getFlaggerFeatures)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/health", am.OpenAccess(aH.getHealth)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/listErrors", am.ViewAccess(aH.listErrors)).Methods(http.MethodPost)
|
||||
@@ -575,56 +577,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 +632,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) {
|
||||
@@ -2065,6 +2024,21 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
aH.Respond(w, featureSet)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getFlaggerFeatures(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Create evaluation context (could get orgID from claims if needed)
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(valuer.GenerateUUID())
|
||||
|
||||
features, err := aH.Signoz.Flagger.List(ctx, evalCtx)
|
||||
if err != nil {
|
||||
aH.HandleError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, features)
|
||||
}
|
||||
|
||||
// getHealth is used to check the health of the service.
|
||||
// 'live' query param can be used to check liveliness of
|
||||
// the service by checking the database connection.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
@@ -101,6 +102,9 @@ type Config struct {
|
||||
|
||||
// MetricsExplorer config
|
||||
MetricsExplorer metricsexplorer.Config `mapstructure:"metricsexplorer"`
|
||||
|
||||
// Flagger config
|
||||
Flagger flagger.Config `mapstructure:"flagger"`
|
||||
}
|
||||
|
||||
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
|
||||
@@ -161,6 +165,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
|
||||
gateway.NewConfigFactory(),
|
||||
tokenizer.NewConfigFactory(),
|
||||
metricsexplorer.NewConfigFactory(),
|
||||
flagger.NewConfigFactory(),
|
||||
}
|
||||
|
||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
83
pkg/signoz/openapi.go
Normal 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)
|
||||
}
|
||||
@@ -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,15 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||
"github.com/SigNoz/signoz/pkg/emailing/smtpemailing"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/configflagger"
|
||||
"github.com/SigNoz/signoz/pkg/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"
|
||||
@@ -43,6 +53,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/tokenizer/opaquetokenizer"
|
||||
"github.com/SigNoz/signoz/pkg/tokenizer/tokenizerstore/sqltokenizerstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/SigNoz/signoz/pkg/web/noopweb"
|
||||
@@ -213,6 +224,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(
|
||||
@@ -220,3 +245,9 @@ func NewTokenizerProviderFactories(cache cache.Cache, sqlstore sqlstore.SQLStore
|
||||
jwttokenizer.NewFactory(cache, tokenStore),
|
||||
)
|
||||
}
|
||||
|
||||
func NewFlaggerProviderFactories(defaultRegistry featuretypes.Registry) factory.NamedMap[factory.ProviderFactory[flagger.Provider, flagger.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
configflagger.NewFactory(defaultRegistry),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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{},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ 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"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
@@ -54,6 +56,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
|
||||
@@ -64,6 +67,7 @@ type SigNoz struct {
|
||||
Modules Modules
|
||||
Handlers Handlers
|
||||
QueryParser queryparser.QueryParser
|
||||
Flagger flagger.Flagger
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -344,11 +348,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,
|
||||
@@ -373,6 +389,20 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize flagger from the available flagger provider factories
|
||||
defaultRegistry := flagger.MustNewRegistry()
|
||||
flaggerProviderFactories := NewFlaggerProviderFactories(defaultRegistry)
|
||||
flagger, err := flagger.New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.Flagger,
|
||||
defaultRegistry,
|
||||
flaggerProviderFactories.GetInOrder()...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
registry, err := factory.NewRegistry(
|
||||
instrumentation.Logger(),
|
||||
factory.NewNamedService(factory.MustNewName("instrumentation"), instrumentation),
|
||||
@@ -399,6 +429,7 @@ func New(
|
||||
Prometheus: prometheus,
|
||||
Alertmanager: alertmanager,
|
||||
Querier: querier,
|
||||
APIServer: apiserver,
|
||||
Zeus: zeus,
|
||||
Licensing: licensing,
|
||||
Emailing: emailing,
|
||||
@@ -408,5 +439,6 @@ func New(
|
||||
Modules: modules,
|
||||
Handlers: handlers,
|
||||
QueryParser: queryParser,
|
||||
Flagger: flagger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
23
pkg/types/featuretypes/context.go
Normal file
23
pkg/types/featuretypes/context.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// A concrete wrapper around the openfeature.EvaluationContext
|
||||
type FlaggerEvaluationContext struct {
|
||||
ctx openfeature.EvaluationContext
|
||||
}
|
||||
|
||||
// Creates a new FlaggerEvaluationContext with given details
|
||||
func NewFlaggerEvaluationContext(orgID valuer.UUID) FlaggerEvaluationContext {
|
||||
ctx := openfeature.NewTargetlessEvaluationContext(map[string]any{
|
||||
"orgId": orgID.String(),
|
||||
})
|
||||
return FlaggerEvaluationContext{ctx: ctx}
|
||||
}
|
||||
|
||||
func (ctx FlaggerEvaluationContext) Ctx() openfeature.EvaluationContext {
|
||||
return ctx.ctx
|
||||
}
|
||||
137
pkg/types/featuretypes/feature.go
Normal file
137
pkg/types/featuretypes/feature.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeFeatureVariantNotFound = errors.MustNewCode("feature_variant_not_found")
|
||||
ErrCodeFeatureValueNotFound = errors.MustNewCode("feature_value_not_found")
|
||||
ErrCodeFeatureVariantKindMismatch = errors.MustNewCode("feature_variant_kind_mismatch")
|
||||
ErrCodeFeatureDefaultVariantNotFound = errors.MustNewCode("feature_default_variant_not_found")
|
||||
ErrCodeFeatureNotFound = errors.MustNewCode("feature_not_found")
|
||||
)
|
||||
|
||||
// A concrete type for a feature flag
|
||||
type Feature struct {
|
||||
// Name of the feature
|
||||
Name Name `json:"name"`
|
||||
// Kind of the feature
|
||||
Kind Kind `json:"kind"`
|
||||
// Stage of the feature
|
||||
Stage Stage `json:"stage"`
|
||||
// Description of the feature
|
||||
Description string `json:"description"`
|
||||
// DefaultVariant of the feature
|
||||
DefaultVariant Name `json:"defaultVariant"`
|
||||
// Variants of the feature
|
||||
Variants map[Name]FeatureVariant `json:"variants"`
|
||||
}
|
||||
|
||||
// A concrete type for a feature flag variant
|
||||
type FeatureVariant struct {
|
||||
// Name of the variant
|
||||
Variant Name `json:"variant"`
|
||||
// Value of the variant
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
// Consumer facing feature struct
|
||||
type GettableFeature struct {
|
||||
*Feature
|
||||
*FeatureVariant
|
||||
}
|
||||
|
||||
type GettableFeatureWithResolution struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Stage string `json:"stage"`
|
||||
Description string `json:"description"`
|
||||
DefaultVariant string `json:"defaultVariant"`
|
||||
Variants map[string]any `json:"variants"`
|
||||
ResolvedValue any `json:"resolvedValue"`
|
||||
ValueSource string `json:"valueSource"`
|
||||
}
|
||||
|
||||
// This is the helper function to get the value of a variant of a feature
|
||||
func VariantValue[T any](feature *Feature, variant Name) (t T, detail openfeature.ProviderResolutionDetail, err error) {
|
||||
value, ok := feature.Variants[variant]
|
||||
if !ok {
|
||||
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureVariantNotFound, "variant %s not found for feature %s in variants %v", variant.String(), feature.Name.String(), feature.Variants)
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
Variant: feature.DefaultVariant.String(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
t, ok = value.Value.(T)
|
||||
if !ok {
|
||||
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureVariantKindMismatch, "variant %s for feature %s has type %T, expected %T", variant.String(), feature.Name.String(), value.Value, t)
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewTypeMismatchResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
Variant: variant.String(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
Reason: openfeature.StaticReason,
|
||||
Variant: variant.String(),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// This is the helper function to get the variant by value for the given feature
|
||||
func VariantByValue[T comparable](feature *Feature, value T) (featureVariant *FeatureVariant, err error) {
|
||||
|
||||
// technically this method should not be called for object kind
|
||||
// but just for fallback
|
||||
if feature.Kind == KindObject {
|
||||
// return the default variant - just for fallback
|
||||
// ? think more on this
|
||||
return &FeatureVariant{Variant: feature.DefaultVariant, Value: value}, nil
|
||||
}
|
||||
|
||||
for _, variant := range feature.Variants {
|
||||
if variant.Value == value {
|
||||
return &variant, nil
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func IsValidValue[T comparable](feature *Feature, value T) (bool, error) {
|
||||
if feature.Kind == KindObject {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
values, err := allFeatureValues[T](feature)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !slices.Contains(values, value) {
|
||||
return false, errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureValueNotFound, "value %v not found for feature %s in variants %v", value, feature.Name.String(), feature.Variants)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func allFeatureValues[T any](feature *Feature) (values []T, err error) {
|
||||
values = make([]T, 0, len(feature.Variants))
|
||||
for _, variant := range feature.Variants {
|
||||
v, _, err := VariantValue[T](feature, variant.Variant)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, v)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
14
pkg/types/featuretypes/kind.go
Normal file
14
pkg/types/featuretypes/kind.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package featuretypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
// A concrete type for a feature flag kind
|
||||
type Kind struct{ valuer.String }
|
||||
|
||||
var (
|
||||
KindBoolean = Kind{valuer.NewString("boolean")}
|
||||
KindString = Kind{valuer.NewString("string")}
|
||||
KindFloat = Kind{valuer.NewString("float")}
|
||||
KindInt = Kind{valuer.NewString("int")}
|
||||
KindObject = Kind{valuer.NewString("object")}
|
||||
)
|
||||
37
pkg/types/featuretypes/name.go
Normal file
37
pkg/types/featuretypes/name.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
var nameRegex = regexp.MustCompile(`^[a-z][a-z0-9_]+$`)
|
||||
|
||||
// Name is a concrete type for a feature name.
|
||||
// We make this abstract to avoid direct use of strings and enforce
|
||||
// a consistent way to create and validate feature names.
|
||||
type Name struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func NewName(s string) (Name, error) {
|
||||
if !nameRegex.MatchString(s) {
|
||||
return Name{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid feature name: %s", s)
|
||||
}
|
||||
|
||||
return Name{s: s}, nil
|
||||
}
|
||||
|
||||
func MustNewName(s string) Name {
|
||||
name, err := NewName(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (n Name) String() string {
|
||||
return n.s
|
||||
}
|
||||
129
pkg/types/featuretypes/registry.go
Normal file
129
pkg/types/featuretypes/registry.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package featuretypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// Consumer facing interface for the feature registry
|
||||
type Registry interface {
|
||||
// Returns the feature and the resolution detail for the given name
|
||||
Get(name Name) (*Feature, openfeature.ProviderResolutionDetail, error)
|
||||
|
||||
// Returns the feature and the resolution detail for the given string name
|
||||
GetByString(name string) (*Feature, openfeature.ProviderResolutionDetail, error)
|
||||
|
||||
// Returns all the features in the registry
|
||||
List() []*Feature
|
||||
}
|
||||
|
||||
// Concrete implementation of the Registry interface
|
||||
type registry struct {
|
||||
features map[Name]*Feature
|
||||
}
|
||||
|
||||
// Validates and builds a new registry from a list of features
|
||||
func NewRegistry(features ...*Feature) (Registry, error) {
|
||||
registry := ®istry{features: make(map[Name]*Feature)}
|
||||
|
||||
for _, feature := range features {
|
||||
// Check if the name is unique
|
||||
if _, ok := registry.features[feature.Name]; ok {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "feature name %s already exists", feature.Name.String())
|
||||
}
|
||||
|
||||
// Default variant should always be present
|
||||
if _, ok := feature.Variants[feature.DefaultVariant]; !ok {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "default variant %s not found for feature %s in variants %v", feature.DefaultVariant.String(), feature.Name.String(), feature.Variants)
|
||||
}
|
||||
|
||||
switch feature.Kind {
|
||||
|
||||
case KindBoolean:
|
||||
err := validateFeature[bool](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindString:
|
||||
err := validateFeature[string](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindFloat:
|
||||
err := validateFeature[float64](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindInt:
|
||||
err := validateFeature[int64](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case KindObject:
|
||||
err := validateFeature[any](feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registry.features[feature.Name] = feature
|
||||
}
|
||||
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
func validateFeature[T any](feature *Feature) error {
|
||||
_, _, err := VariantValue[T](feature, feature.DefaultVariant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for variant := range feature.Variants {
|
||||
_, _, err := VariantValue[T](feature, variant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) Get(name Name) (f *Feature, detail openfeature.ProviderResolutionDetail, err error) {
|
||||
feature, ok := r.features[name]
|
||||
if !ok {
|
||||
err = errors.Newf(errors.TypeNotFound, ErrCodeFeatureNotFound, "feature %s not found", name.String())
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return feature, openfeature.ProviderResolutionDetail{}, nil
|
||||
}
|
||||
|
||||
func (r *registry) GetByString(name string) (f *Feature, detail openfeature.ProviderResolutionDetail, err error) {
|
||||
featureName, err := NewName(name)
|
||||
if err != nil {
|
||||
detail = openfeature.ProviderResolutionDetail{
|
||||
ResolutionError: openfeature.NewFlagNotFoundResolutionError(err.Error()),
|
||||
Reason: openfeature.ErrorReason,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return r.Get(featureName)
|
||||
}
|
||||
|
||||
func (r *registry) List() []*Feature {
|
||||
features := make([]*Feature, 0, len(r.features))
|
||||
for _, f := range r.features {
|
||||
features = append(features, f)
|
||||
}
|
||||
return features
|
||||
}
|
||||
20
pkg/types/featuretypes/stage.go
Normal file
20
pkg/types/featuretypes/stage.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package featuretypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
// A concrete type for a feature flag stage
|
||||
type Stage struct{ valuer.String }
|
||||
|
||||
var (
|
||||
// Used when the feature is experimental
|
||||
StageExperimental = Stage{valuer.NewString("experimental")}
|
||||
|
||||
// Used when the feature works and in preview stage but is not ready for production
|
||||
StagePreview = Stage{valuer.NewString("preview")}
|
||||
|
||||
// Used when the feature is stable and ready for production
|
||||
StageStable = Stage{valuer.NewString("stable")}
|
||||
|
||||
// Used when the feature is deprecated and will be removed in the future
|
||||
StageDeprecated = Stage{valuer.NewString("deprecated")}
|
||||
)
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user