Compare commits

..

11 Commits

Author SHA1 Message Date
grandwizard28
d05e279bcd Merge branch 'main' into alertmanager 2025-02-12 22:58:29 +05:30
grandwizard28
f1c5d873f7 feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:28 +05:30
grandwizard28
aec239cc7c feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:28 +05:30
grandwizard28
59e26652dc feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:27 +05:30
grandwizard28
e02afc5e97 feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:27 +05:30
grandwizard28
3eac8ac30b feat(alertmanager): first attempt at bootstrap 2025-02-12 18:46:27 +05:30
grandwizard28
382c4f58e1 refactor(alertmanager): add alertmanager 2025-02-12 18:46:27 +05:30
grandwizard28
73ea632a3f refactor(alertmanager): move to types package 2025-02-12 18:46:27 +05:30
grandwizard28
00fa8810c0 feat(alertmanager): add support for multi org 2025-02-12 18:46:26 +05:30
grandwizard28
6cee330d44 feat(alertmanager): add support for testing receiver 2025-02-12 18:46:26 +05:30
grandwizard28
871c6e642c feat(alertmanager): first iteration of alertmanager 2025-02-12 18:46:25 +05:30
113 changed files with 1568 additions and 5942 deletions

2
.gitignore vendored
View File

@@ -76,3 +76,5 @@ dist/
# ignore user_scripts that is fetched by init-clickhouse
deploy/common/clickhouse/user_scripts/
# queries.active
queries.active

View File

@@ -66,6 +66,8 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
@@ -88,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

View File

@@ -66,6 +66,8 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
@@ -88,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

View File

@@ -20,6 +20,7 @@ import (
basemodel "go.signoz.io/signoz/pkg/query-service/model"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
)
type APIHandlerOptions struct {
@@ -41,6 +42,7 @@ type APIHandlerOptions struct {
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
SigNoz *signoz.SigNoz
}
type APIHandler struct {
@@ -65,6 +67,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
SigNoz: opts.SigNoz,
})
if err != nil {

View File

@@ -269,6 +269,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
GatewayUrl: serverOptions.GatewayUrl,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
SigNoz: serverOptions.SigNoz,
}
apiHandler, err := api.NewAPIHandler(apiOpts)

View File

@@ -7,7 +7,6 @@ import (
"os"
"os/signal"
"strconv"
"syscall"
"time"
"go.opentelemetry.io/otel/sdk/resource"
@@ -87,6 +86,8 @@ func init() {
}
func main() {
ctx := context.Background()
var promConfigPath, skipTopLvlOpsPath string
// disables rule execution but allows change to the rule definition
@@ -191,20 +192,20 @@ func main() {
zap.L().Fatal("Could not start server", zap.Error(err))
}
if err := auth.InitAuthCache(context.Background()); err != nil {
if err := auth.InitAuthCache(ctx); err != nil {
zap.L().Fatal("Failed to initialize auth cache", zap.Error(err))
}
signalsChannel := make(chan os.Signal, 1)
signal.Notify(signalsChannel, os.Interrupt, syscall.SIGTERM)
if err := signoz.Start(ctx); err != nil {
zap.L().Fatal("Failed to start signoz", zap.Error(err))
}
for {
select {
case status := <-server.HealthCheckStatus():
zap.L().Info("Received HealthCheck status: ", zap.Int("status", int(status)))
case <-signalsChannel:
zap.L().Fatal("Received OS Interrupt Signal ... ")
server.Stop()
}
if err := signoz.Wait(ctx); err != nil {
zap.L().Fatal("Failed to wait for signoz", zap.Error(err))
}
server.Stop()
if err := signoz.Stop(ctx); err != nil {
zap.L().Fatal("Failed to stop signoz", zap.Error(err))
}
}

View File

@@ -157,6 +157,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -279,6 +286,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -415,4 +429,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -18,9 +18,7 @@ import {
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
@@ -31,7 +29,6 @@ import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
@@ -86,20 +83,7 @@ function ExplorerCard({
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
const { options } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
const { mutateAsync: updateViewAsync } = useUpdateView({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),

View File

@@ -6,24 +6,11 @@ import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = jest.fn();
// eslint-disable-next-line sonarjs/no-duplicate-string
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({

View File

@@ -2,7 +2,6 @@ import { FormInstance } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { UseMutateAsyncFunction } from 'react-query';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -37,7 +36,6 @@ export interface IsQueryUpdatedInViewProps {
data: ViewProps[] | undefined;
stagedQuery: Query | null;
currentPanelType: PANEL_TYPES | null;
options: OptionsQuery;
}
export interface SaveViewWithNameProps {

View File

@@ -80,13 +80,12 @@ export const isQueryUpdatedInView = ({
data,
stagedQuery,
currentPanelType,
options,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType, extraData } = currentViewDetails;
const { query, panelType } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
@@ -98,15 +97,12 @@ export const isQueryUpdatedInView = ({
) {
return false;
}
return (
panelType !== currentPanelType ||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
!isEqual(query.promql, updatedCurrentQuery?.promql) ||
!isEqual(
options?.selectColumns,
extraData && JSON.parse(extraData)?.selectColumns,
)
!isEqual(query.promql, updatedCurrentQuery?.promql)
);
};

View File

@@ -6,8 +6,6 @@ import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -27,12 +25,8 @@ function DynamicColumnTable({
onDragColumn,
facingIssueBtn,
shouldSendAlertsLogEvent,
pagination,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
columns,
);
@@ -99,28 +93,6 @@ function DynamicColumnTable({
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
const currentPage = Number(urlQuery.get('page')) || 1;
const handlePaginationChange = (page: number, pageSize?: number): void => {
// Update URL with new page number while preserving other params
urlQuery.set('page', page.toString());
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
safeNavigate(newUrl);
// Call original pagination handler if provided
if (pagination?.onChange && !!pageSize) {
pagination.onChange(page, pageSize);
}
};
const enhancedPagination = {
...pagination,
current: currentPage, // Ensure the pagination component shows the correct page
onChange: handlePaginationChange,
};
return (
<div className="DynamicColumnTable">
<Flex justify="flex-end" align="center" gap={8}>
@@ -144,7 +116,6 @@ function DynamicColumnTable({
<ResizeTable
columns={columnsData}
onDragColumn={onDragColumn}
pagination={enhancedPagination}
{...restProps}
/>
</div>

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { PaginationProps } from 'antd/lib';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
@@ -16,7 +15,6 @@ export interface DynamicColumnTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: LaunchChatSupportProps;
shouldSendAlertsLogEvent?: boolean;
pagination?: PaginationProps;
}
export type GetVisibleColumnsFunction = (

View File

@@ -23,4 +23,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
AWS_INTEGRATION = 'AWS_INTEGRATION',
}

View File

@@ -159,9 +159,8 @@ function AccountActions(): JSX.Element {
useEffect(() => {
if (initialAccount !== null) {
setActiveAccount(initialAccount);
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: latestUrlQuery.toString() });
urlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialAccount]);

View File

@@ -2,11 +2,9 @@ import './CloudAccountSetupModal.style.scss';
import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import {
ActiveViewEnum,
@@ -20,7 +18,6 @@ import { SuccessView } from './SuccessView';
function CloudAccountSetupModal({
onClose,
}: IntegrationModalProps): JSX.Element {
const queryClient = useQueryClient();
const {
form,
modalState,
@@ -113,10 +110,7 @@ function CloudAccountSetupModal({
</div>
),
block: true,
onOk: (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
handleClose();
},
onOk: handleClose,
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
@@ -157,7 +151,6 @@ function CloudAccountSetupModal({
activeView,
handleClose,
setActiveView,
queryClient,
]);
const modalConfig = getModalConfig();

View File

@@ -34,19 +34,10 @@ function ConfigureServiceModal({
const [isLoading, setIsLoading] = useState(false);
// Track current form values
const initialValues = {
const [currentValues, setCurrentValues] = useState({
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
};
const [currentValues, setCurrentValues] = useState(initialValues);
const isSaveDisabled = useMemo(
() =>
// disable only if current values are same as the initial config
currentValues.metrics === initialValues.metrics &&
currentValues.logs === initialValues.logs,
[currentValues, initialValues.metrics, initialValues.logs],
);
});
const {
mutate: updateServiceConfig,
@@ -102,6 +93,11 @@ function ConfigureServiceModal({
onClose,
]);
const isSaveDisabled = useMemo(
() => currentValues.metrics === false && currentValues.logs === false,
[currentValues],
);
const handleClose = useCallback(() => {
form.resetFields();
onClose();

View File

@@ -111,27 +111,24 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__title-bar">
<div className="service-details__details-title">Details</div>
<div className="service-details__right-actions">
{isAnySignalConfigured && (
<ServiceStatus serviceStatus={serviceDetailsData.status} />
)}
<ServiceStatus serviceStatus={serviceDetailsData.status} />
{!!cloudAccountId &&
(isAnySignalConfigured ? (
<Button
className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button>
))}
{!!cloudAccountId && isAnySignalConfigured ? (
<Button
className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button>
)}
</div>
</div>
<div className="service-details__overview">

View File

@@ -24,11 +24,10 @@ function ServicesList({
const handleActiveService = useCallback(
(serviceId: string): void => {
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('service', serviceId);
navigate({ search: latestUrlQuery.toString() });
urlQuery.set('service', serviceId);
navigate({ search: urlQuery.toString() });
},
[navigate],
[navigate, urlQuery],
);
const filteredServices = useMemo(() => {

View File

@@ -27,12 +27,6 @@ jest.mock('uplot', () => {
};
});
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
let mockWindowOpen: jest.Mock;
window.ResizeObserver =

View File

@@ -36,11 +36,6 @@ window.ResizeObserver =
unobserve: jest.fn(),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Anomaly Alert Documentation Redirection', () => {
let mockWindowOpen: jest.Mock;

View File

@@ -212,46 +212,13 @@ function ExplorerOptions({
0.08,
);
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: BaseAutocompleteData[],
): string => {
let updatedExtraData;
if (extraData) {
const parsedExtraData = JSON.parse(extraData);
parsedExtraData.selectColumns = newSelectedColumns;
updatedExtraData = JSON.stringify(parsedExtraData);
} else {
updatedExtraData = JSON.stringify({
color: Color.BG_SIENNA_500,
selectColumns: newSelectedColumns,
});
}
return updatedExtraData;
};
const updatedExtraData = getUpdatedExtraData(
extraData,
options?.selectColumns,
);
const {
mutateAsync: updateViewAsync,
isLoading: isViewUpdating,
} = useUpdateView({
compositeQuery,
viewKey,
extraData: updatedExtraData,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
sourcePage: sourcepage,
viewName,
});
@@ -263,11 +230,13 @@ function ExplorerOptions({
};
const onUpdateQueryHandler = (): void => {
const extraData = viewsData?.data?.data?.find((view) => view.uuid === viewKey)
?.extraData;
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: updatedExtraData,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
sourcePage: sourcepage,
viewName,
},
@@ -289,6 +258,15 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
type ExtraData = {
selectColumns?: BaseAutocompleteData[];
version?: number;
@@ -444,11 +422,7 @@ function ExplorerOptions({
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
const {
isLoading: isSaveViewLoading,

View File

@@ -17,8 +17,8 @@ import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es';
@@ -87,7 +87,7 @@ function FormAlertRules({
// init namespace for translations
const { t } = useTranslation('alerts');
const { featureFlags } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
@@ -224,7 +224,7 @@ function FormAlertRules({
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [detectionMethod]);
@@ -295,8 +295,8 @@ function FormAlertRules({
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [safeNavigate, urlQuery]);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [urlQuery]);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
@@ -515,7 +515,7 @@ function FormAlertRules({
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} else {
logData = {

View File

@@ -20,11 +20,11 @@ import {
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useChartMutable } from 'hooks/useChartMutable';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -49,7 +49,6 @@ function FullView({
isDependedDataLoaded = false,
onToggleModelHandler,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
@@ -138,9 +137,9 @@ function FullView({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.push(generatedUrl);
},
[dispatch, location.pathname, safeNavigate, urlQuery],
[dispatch, location.pathname, urlQuery],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<

View File

@@ -23,12 +23,6 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),

View File

@@ -10,9 +10,9 @@ import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -51,7 +51,6 @@ function WidgetGraphComponent({
customSeries,
customErrorMessage,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
const { notifications } = useNotifications();
@@ -174,7 +173,7 @@ function WidgetGraphComponent({
graphType: widget?.panelTypes,
widgetId: uuid,
};
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
history.push(`${pathname}/new?${createQueryParams(queryParams)}`);
},
},
);
@@ -195,7 +194,7 @@ function WidgetGraphComponent({
const separator = existingSearch.toString() ? '&' : '';
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
safeNavigate({
history.push({
pathname,
search: newSearch,
});
@@ -222,7 +221,7 @@ function WidgetGraphComponent({
});
setGraphVisibility(localStoredVisibilityState);
}
safeNavigate({
history.push({
pathname,
search: createQueryParams(updatedQueryParams),
});

View File

@@ -15,8 +15,8 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import {
@@ -55,7 +55,6 @@ interface GraphLayoutProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function GraphLayout(props: GraphLayoutProps): JSX.Element {
const { handle } = props;
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
layouts,
@@ -67,7 +66,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
isDashboardFetching,
} = useDashboard();
const { data } = selectedDashboard || {};
const { pathname } = useLocation();
@@ -216,13 +214,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, safeNavigate, urlQuery],
[dispatch, pathname, urlQuery],
);
useEffect(() => {
@@ -233,8 +231,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
!isEqual(layouts, dashboardLayout) &&
!isDashboardLocked &&
saveLayoutPermission &&
!updateDashboardMutation.isLoading &&
!isDashboardFetching
!updateDashboardMutation.isLoading
) {
onSaveHandler();
}

View File

@@ -18,8 +18,8 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import useComponentPermission from 'hooks/useComponentPermission';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { CircleX, X } from 'lucide-react';
@@ -72,7 +72,6 @@ function WidgetHeader({
setSearchTerm,
}: IWidgetHeaderProps): JSX.Element | null {
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const onEditHandler = useCallback((): void => {
const widgetId = widget.id;
urlQuery.set(QueryParams.widgetId, widgetId);
@@ -82,8 +81,8 @@ function WidgetHeader({
encodeURIComponent(JSON.stringify(widget.query)),
);
const generatedUrl = `${window.location.pathname}/new?${urlQuery}`;
safeNavigate(generatedUrl);
}, [safeNavigate, urlQuery, widget.id, widget.panelTypes, widget.query]);
history.push(generatedUrl);
}, [urlQuery, widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');

View File

@@ -35,7 +35,7 @@ import dayjs from 'dayjs';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { get, isEmpty, isUndefined } from 'lodash-es';
import {
ArrowDownWideNarrow,
@@ -74,7 +74,7 @@ import {
} from 'react';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { generatePath, Link } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import {
Dashboard,
@@ -105,7 +105,7 @@ function DashboardsList(): JSX.Element {
} = useGetAllDashboard();
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const {
listSortOrder: sortOrder,
setListSortOrder: setSortOrder,
@@ -293,7 +293,7 @@ function DashboardsList(): JSX.Element {
});
if (response.statusCode === 200) {
safeNavigate(
history.push(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),
@@ -313,7 +313,7 @@ function DashboardsList(): JSX.Element {
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, safeNavigate, t]);
}, [newDashboardState, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
@@ -418,7 +418,7 @@ function DashboardsList(): JSX.Element {
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
history.push(getLink());
}
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
@@ -444,12 +444,10 @@ function DashboardsList(): JSX.Element {
placement="left"
overlayClassName="title-toolip"
>
<div
<Link
to={getLink()}
className="title-link"
onClick={(e): void => {
e.stopPropagation();
safeNavigate(getLink());
}}
onClick={(e): void => e.stopPropagation()}
>
<img
src={dashboard?.image || Base64Icons[0]}
@@ -462,7 +460,7 @@ function DashboardsList(): JSX.Element {
>
{dashboard.name}
</Typography.Text>
</div>
</Link>
</Tooltip>
</div>

View File

@@ -18,8 +18,8 @@ import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history';
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
// See more: https://github.com/lucide-icons/lucide/issues/94
@@ -33,7 +33,6 @@ function ImportJSON({
uploadedGrafana,
onModalHandler,
}: ImportJSONProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [jsonData, setJsonData] = useState<Record<string, unknown>>();
const { t } = useTranslation(['dashboard', 'common']);
const [isUploadJSONError, setIsUploadJSONError] = useState<boolean>(false);
@@ -98,7 +97,7 @@ function ImportJSON({
});
if (response.statusCode === 200) {
safeNavigate(
history.push(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),

View File

@@ -2,12 +2,14 @@ import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import { themeColors } from 'constants/theme';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import useUrlQuery from 'hooks/useUrlQuery';
import getChartData, { GetChartDataProps } from 'lib/getChartData';
import GetMinMax from 'lib/getMinMax';
import { colors } from 'lib/getRandomColor';
import { memo, useCallback, useMemo } from 'react';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
@@ -26,7 +28,6 @@ function LogsExplorerChart({
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = useCallback(
(element, index, allLabels) => ({
data: element,
@@ -61,13 +62,41 @@ function LogsExplorerChart({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.push(generatedUrl);
},
[dispatch, location.pathname, safeNavigate, urlQuery],
[dispatch, location.pathname, urlQuery],
);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),
parseInt(getTimeString(endTime), 10),
]),
);
}
};
useEffect(() => {
window.addEventListener('popstate', handleBackNavigation);
return (): void => {
window.removeEventListener('popstate', handleBackNavigation);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const graphData = useMemo(
() =>
getChartData({

View File

@@ -38,7 +38,6 @@ import useAxiosError from 'hooks/useAxiosError';
import useClickOutside from 'hooks/useClickOutside';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { FlatLogData } from 'lib/logs/flatLogData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
@@ -63,6 +62,7 @@ import {
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log';
@@ -98,7 +98,7 @@ function LogsExplorerViews({
chartQueryKeyRef: MutableRefObject<any>;
}): JSX.Element {
const { notifications } = useNotifications();
const { safeNavigate } = useSafeNavigate();
const history = useHistory();
// this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@@ -486,7 +486,7 @@ function LogsExplorerViews({
widgetId,
});
safeNavigate(dashboardEditView);
history.push(dashboardEditView);
},
onError: handleAxisError,
});
@@ -495,7 +495,7 @@ function LogsExplorerViews({
getUpdatedQueryForExport,
exportDefaultQuery,
options.selectColumns,
safeNavigate,
history,
notifications,
panelType,
updateDashboard,

View File

@@ -75,12 +75,6 @@ jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
useGetExplorerQueryRange: jest.fn(),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
beforeEach(() => {
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({

View File

@@ -13,7 +13,6 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
@@ -158,7 +157,6 @@ function DBCall(): JSX.Element {
servicename,
isDBCall: true,
});
const { safeNavigate } = useSafeNavigate();
return (
<Row gutter={24}>
@@ -173,7 +171,6 @@ function DBCall(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -209,7 +206,6 @@ function DBCall(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces

View File

@@ -15,7 +15,6 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
@@ -221,8 +220,6 @@ function External(): JSX.Element {
isExternalCall: true,
});
const { safeNavigate } = useSafeNavigate();
return (
<>
<Row gutter={24}>
@@ -237,7 +234,6 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -274,7 +270,6 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -314,7 +309,6 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -351,7 +345,6 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces

View File

@@ -13,7 +13,6 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
@@ -291,7 +290,6 @@ function Application(): JSX.Element {
},
],
});
const { safeNavigate } = useSafeNavigate();
return (
<>
@@ -319,7 +317,6 @@ function Application(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -349,7 +346,6 @@ function Application(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces

View File

@@ -12,7 +12,6 @@ import { latency } from 'container/MetricsApplication/MetricsPageQueries/Overvie
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { useAppContext } from 'providers/App/App';
import { useMemo } from 'react';
@@ -86,8 +85,6 @@ function ServiceOverview({
const apmToLogQuery = useGetAPMToLogsQueries({ servicename });
const { safeNavigate } = useSafeNavigate();
return (
<>
<GraphControlsPanel
@@ -99,7 +96,6 @@ function ServiceOverview({
apmToTraceQuery: apmToLogQuery,
isViewLogsClicked: true,
stepInterval,
safeNavigate,
})}
onViewTracesClick={onViewTracePopupClick({
servicename,
@@ -107,7 +103,6 @@ function ServiceOverview({
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
/>
<Card data-testid="service_latency">

View File

@@ -1,6 +1,5 @@
import { Tooltip, Typography } from 'antd';
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 { v4 as uuid } from 'uuid';
@@ -15,7 +14,6 @@ function ColumnWithLink({
record,
}: LinkColumnProps): JSX.Element {
const text = record.toString();
const { safeNavigate } = useSafeNavigate();
const apmToTraceQuery = useGetAPMToTracesQueries({
servicename,
@@ -44,7 +42,6 @@ function ColumnWithLink({
maxTime,
selectedTraceTags,
apmToTraceQuery,
safeNavigate,
});
};

View File

@@ -6,6 +6,7 @@ import { getQueryString } from 'container/SideNav/helper';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
import history from 'lib/history';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
import { Dispatch, SetStateAction, useMemo } from 'react';
@@ -35,7 +36,6 @@ interface OnViewTracePopupClickProps {
apmToTraceQuery: Query;
isViewLogsClicked?: boolean;
stepInterval?: number;
safeNavigate: (url: string) => void;
}
export function generateExplorerPath(
@@ -63,7 +63,6 @@ export function onViewTracePopupClick({
apmToTraceQuery,
isViewLogsClicked,
stepInterval,
safeNavigate,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
const endTime = timestamp;
@@ -89,7 +88,7 @@ export function onViewTracePopupClick({
queryString,
);
safeNavigate(newPath);
history.push(newPath);
};
}
@@ -112,7 +111,7 @@ export function onGraphClickHandler(
buttonElement.style.display = 'block';
buttonElement.style.left = `${mouseX}px`;
buttonElement.style.top = `${mouseY}px`;
setSelectedTimeStamp(Math.floor(xValue * 1_000));
setSelectedTimeStamp(xValue);
}
} else if (buttonElement && buttonElement.style.display === 'block') {
buttonElement.style.display = 'none';

View File

@@ -8,7 +8,6 @@ import Download from 'container/Download/Download';
import { filterDropdown } from 'container/ServiceApplication/Filter/FilterDropdown';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
@@ -32,7 +31,7 @@ function TopOperationsTable({
}: TopOperationsTableProps): JSX.Element {
const searchInput = useRef<InputRef>(null);
const { servicename: encodedServiceName } = useParams<IServiceName>();
const { safeNavigate } = useSafeNavigate();
const servicename = decodeURIComponent(encodedServiceName);
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@@ -88,7 +87,6 @@ function TopOperationsTable({
maxTime,
selectedTraceTags,
apmToTraceQuery: preparedQuery,
safeNavigate,
});
};
@@ -128,7 +126,7 @@ function TopOperationsTable({
key: 'p50',
width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number => a.p50 - b.p50,
render: (value: number): string => (value / 1_000_000).toFixed(2),
render: (value: number): string => (value / 1000000).toFixed(2),
},
{
title: 'P95 (in ms)',
@@ -136,7 +134,7 @@ function TopOperationsTable({
key: 'p95',
width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number => a.p95 - b.p95,
render: (value: number): string => (value / 1_000_000).toFixed(2),
render: (value: number): string => (value / 1000000).toFixed(2),
},
{
title: 'P99 (in ms)',
@@ -144,7 +142,7 @@ function TopOperationsTable({
key: 'p99',
width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number => a.p99 - b.p99,
render: (value: number): string => (value / 1_000_000).toFixed(2),
render: (value: number): string => (value / 1000000).toFixed(2),
},
{
title: 'Number of Calls',

View File

@@ -21,7 +21,6 @@ export interface NavigateToTraceProps {
maxTime: number;
selectedTraceTags: string;
apmToTraceQuery: Query;
safeNavigate: (path: string) => void;
}
export interface DatabaseCallsRPSProps extends DatabaseCallProps {

View File

@@ -1,5 +1,6 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
@@ -18,14 +19,10 @@ export const navigateToTrace = ({
maxTime,
selectedTraceTags,
apmToTraceQuery,
safeNavigate,
}: NavigateToTraceProps): void => {
const urlParams = new URLSearchParams();
urlParams.set(
QueryParams.startTime,
Math.floor(minTime / 1_000_000).toString(),
);
urlParams.set(QueryParams.endTime, Math.floor(maxTime / 1_000_000).toString());
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(apmToTraceQuery));
@@ -35,7 +32,7 @@ export const navigateToTrace = ({
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
safeNavigate(newTraceExplorerPath);
history.push(newTraceExplorerPath);
};
export const getNearestHighestBucketValue = (

View File

@@ -25,12 +25,6 @@ jest.mock(
},
);
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Dashboard landing page actions header tests', () => {
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
const mockLocation = {

View File

@@ -21,8 +21,8 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { isEmpty } from 'lodash-es';
import {
Check,
@@ -89,7 +89,6 @@ export function sanitizeDashboardData(
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { handle } = props;
const {
selectedDashboard,
@@ -312,7 +311,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
}
return (

View File

@@ -3,11 +3,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import {
Dispatch,
SetStateAction,
@@ -33,7 +33,6 @@ function WidgetGraph({
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
@@ -72,9 +71,9 @@ function WidgetGraph({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.push(generatedUrl);
},
[dispatch, location.pathname, safeNavigate, urlQuery],
[dispatch, location.pathname, urlQuery],
);
useEffect(() => {

View File

@@ -87,12 +87,6 @@ jest.mock('hooks/queryBuilder/useGetCompositeQueryParam', () => ({
useGetCompositeQueryParam: (): Query => compositeQueryParam as Query,
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Column unit selector panel unit test', () => {
it('unit selectors should be rendered for queries and formula', () => {
const mockLocation = {

View File

@@ -20,10 +20,10 @@ import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import history from 'lib/history';
import { defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
@@ -67,7 +67,6 @@ import {
} from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
setSelectedDashboard,
@@ -329,11 +328,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
}
const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) };
updatedQuery.builder.queryData[0].pageSize = 10;
// If stagedQuery exists, don't re-run the query (e.g. when clicking on Add to Dashboard from logs and traces explorer)
if (!stagedQuery) {
redirectWithQueryBuilderData(updatedQuery);
}
redirectWithQueryBuilderData(updatedQuery);
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
@@ -474,7 +469,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setSelectedRowWidgetId(null);
setSelectedDashboard(dashboard);
setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({
history.push({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
});
},
@@ -497,7 +492,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setSelectedDashboard,
setToScrollWidgetId,
setSelectedRowWidgetId,
safeNavigate,
dashboardId,
]);
@@ -506,12 +500,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setDiscardModal(true);
return;
}
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified, safeNavigate]);
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified]);
const discardChanges = useCallback(() => {
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, safeNavigate]);
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId]);
const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true);

View File

@@ -23,12 +23,6 @@ jest.mock('providers/Dashboard/Dashboard', () => ({
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('QueryTable -', () => {
it('should render correctly with all the data rows', () => {
const { container } = render(<QueryTable {...QueryTableProps} />);

View File

@@ -23,18 +23,17 @@ import { QueryHistoryState } from 'container/LiveLogs/types';
import NewExplorerCTA from 'container/NewExplorerCTA';
import dayjs, { Dayjs } from 'dayjs';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { isObject } from 'lodash-es';
import { Check, Copy, Info, Send, Undo } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { connect, useDispatch, useSelector } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { useNavigationType } from 'react-router-dom-v5-compat';
import { useCopyToClipboard } from 'react-use';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@@ -44,7 +43,6 @@ import AppActions from 'types/actions';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { normalizeTimeToMs } from 'utils/timeUtils';
import AutoRefresh from '../AutoRefreshV2';
import { DateTimeRangeType } from '../CustomDateTimeModal';
@@ -77,9 +75,6 @@ function DateTimeSelection({
modalSelectedInterval,
}: Props): JSX.Element {
const [formSelector] = Form.useForm();
const { safeNavigate } = useSafeNavigate();
const navigationType = useNavigationType(); // Returns 'POP' for back/forward navigation
const dispatch = useDispatch();
const [hasSelectedTimeError, setHasSelectedTimeError] = useState(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
@@ -194,8 +189,8 @@ function DateTimeSelection({
const path = `${ROUTES.LIVE_LOGS}?${QueryParams.compositeQuery}=${JSONCompositeQuery}`;
safeNavigate(path, { state: queryHistoryState });
}, [panelType, queryClient, safeNavigate, stagedQuery]);
history.push(path, queryHistoryState);
}, [panelType, queryClient, stagedQuery]);
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
@@ -354,7 +349,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.relativeTime, value);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
}
// For logs explorer - time range handling is managed in useCopyLogLink.ts:52
@@ -373,7 +368,6 @@ function DateTimeSelection({
location.pathname,
onTimeChange,
refreshButtonHidden,
safeNavigate,
stagedQuery,
updateLocalStorageForRoutes,
updateTimeInterval,
@@ -446,7 +440,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
}
}
}
@@ -473,7 +467,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
}
if (!stagedQuery) {
@@ -515,77 +509,6 @@ function DateTimeSelection({
return time;
};
const handleAbsoluteTimeSync = useCallback(
(
startTime: string,
endTime: string,
currentMinTime: number,
currentMaxTime: number,
): void => {
const startTs = normalizeTimeToMs(startTime);
const endTs = normalizeTimeToMs(endTime);
const timeComparison = {
url: {
start: dayjs(startTs).startOf('minute'),
end: dayjs(endTs).startOf('minute'),
},
current: {
start: dayjs(normalizeTimeToMs(currentMinTime)).startOf('minute'),
end: dayjs(normalizeTimeToMs(currentMaxTime)).startOf('minute'),
},
};
const hasTimeChanged =
!timeComparison.current.start.isSame(timeComparison.url.start) ||
!timeComparison.current.end.isSame(timeComparison.url.end);
if (hasTimeChanged) {
dispatch(UpdateTimeInterval('custom', [startTs, endTs]));
}
},
[dispatch],
);
const handleRelativeTimeSync = useCallback(
(relativeTime: string): void => {
updateTimeInterval(relativeTime as Time);
setIsValidteRelativeTime(true);
setRefreshButtonHidden(false);
},
[updateTimeInterval],
);
// Sync time picker state with URL on browser navigation
useEffect(() => {
if (navigationType !== 'POP') return;
if (searchStartTime && searchEndTime) {
handleAbsoluteTimeSync(searchStartTime, searchEndTime, minTime, maxTime);
return;
}
if (
relativeTimeFromUrl &&
isValidTimeFormat(relativeTimeFromUrl) &&
relativeTimeFromUrl !== selectedTime
) {
handleRelativeTimeSync(relativeTimeFromUrl);
}
}, [
navigationType,
searchStartTime,
searchEndTime,
relativeTimeFromUrl,
selectedTime,
minTime,
maxTime,
dispatch,
updateTimeInterval,
handleAbsoluteTimeSync,
handleRelativeTimeSync,
]);
// this is triggred when we change the routes and based on that we are changing the default options
useEffect(() => {
const metricsTimeDuration = getLocalStorageKey(
@@ -601,16 +524,6 @@ function DateTimeSelection({
const currentRoute = location.pathname;
// Give priority to relativeTime from URL if it exists and start /end time are not present in the url, to sync the relative time in URL param with the time picker
if (
!searchStartTime &&
!searchEndTime &&
relativeTimeFromUrl &&
isValidTimeFormat(relativeTimeFromUrl)
) {
handleRelativeTimeSync(relativeTimeFromUrl);
}
// set the default relative time for alert history and overview pages if relative time is not specified
if (
(!urlQuery.has(QueryParams.startTime) ||
@@ -622,7 +535,7 @@ function DateTimeSelection({
updateTimeInterval(defaultRelativeTime);
urlQuery.set(QueryParams.relativeTime, defaultRelativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
return;
}
@@ -660,7 +573,7 @@ function DateTimeSelection({
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, updateTimeInterval, globalTimeLoading]);

View File

@@ -12,7 +12,6 @@ import {
transformDataWithDate,
} from 'container/TracesExplorer/ListView/utils';
import { Pagination } from 'hooks/queryPagination';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
@@ -40,7 +39,6 @@ function TracesTableComponent({
offset: 0,
limit: 10,
});
const { safeNavigate } = useSafeNavigate();
useEffect(() => {
setRequestData((prev) => ({
@@ -89,25 +87,6 @@ function TracesTableComponent({
[],
);
const handlePaginationChange = useCallback(
(newPagination: Pagination) => {
const urlQuery = new URLSearchParams(window.location.search);
// Update URL with new pagination values
urlQuery.set('offset', newPagination.offset.toString());
urlQuery.set('limit', newPagination.limit.toString());
// Update URL without page reload
safeNavigate({
search: urlQuery.toString(),
});
// Update component state
setPagination(newPagination);
},
[safeNavigate],
);
if (queryResponse.isError) {
return <div>{SOMETHING_WENT_WRONG}</div>;
}
@@ -137,19 +116,19 @@ function TracesTableComponent({
offset={pagination.offset}
countPerPage={pagination.limit}
handleNavigatePrevious={(): void => {
handlePaginationChange({
setPagination({
...pagination,
offset: pagination.offset - pagination.limit,
});
}}
handleNavigateNext={(): void => {
handlePaginationChange({
setPagination({
...pagination,
offset: pagination.offset + pagination.limit,
});
}}
handleCountItemsPerPageChange={(value): void => {
handlePaginationChange({
setPagination({
...pagination,
limit: value,
offset: 0,

View File

@@ -1,10 +1,10 @@
import { useMachine } from '@xstate/react';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { encode } from 'js-base64';
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import history from 'lib/history';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { whilelistedKeys } from './config';
@@ -32,7 +32,6 @@ function ResourceProvider({ children }: Props): JSX.Element {
const [queries, setQueries] = useState<IResourceAttribute[]>(
getResourceAttributeQueriesFromURL(),
);
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const [optionsData, setOptionsData] = useState<OptionsData>({
@@ -40,12 +39,6 @@ function ResourceProvider({ children }: Props): JSX.Element {
options: [],
});
// Watch for URL query changes
useEffect(() => {
const queriesFromUrl = getResourceAttributeQueriesFromURL();
setQueries(queriesFromUrl);
}, [urlQuery]);
const handleLoading = (isLoading: boolean): void => {
setLoading(isLoading);
if (isLoading) {
@@ -60,10 +53,10 @@ function ResourceProvider({ children }: Props): JSX.Element {
encode(JSON.stringify(queries)),
);
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
setQueries(queries);
},
[pathname, safeNavigate, urlQuery],
[pathname, urlQuery],
);
const [state, send] = useMachine(ResourceAttributesFilterMachine, {

View File

@@ -5,12 +5,6 @@ import { Router } from 'react-router-dom';
import ResourceProvider from '../ResourceProvider';
import useResourceAttribute from '../useResourceAttribute';
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('useResourceAttribute component hook', () => {
it('should not change other query params except for resourceAttribute', async () => {
const history = createMemoryHistory({

View File

@@ -1,136 +0,0 @@
import { cloneDeep, isEqual } from 'lodash-es';
import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
interface NavigateOptions {
replace?: boolean;
state?: any;
}
interface SafeNavigateParams {
pathname?: string;
search?: string;
}
const areUrlsEffectivelySame = (url1: URL, url2: URL): boolean => {
if (url1.pathname !== url2.pathname) return false;
const params1 = new URLSearchParams(url1.search);
const params2 = new URLSearchParams(url2.search);
const allParams = new Set([
...Array.from(params1.keys()),
...Array.from(params2.keys()),
]);
return Array.from(allParams).every((param) => {
if (param === 'compositeQuery') {
try {
const query1 = params1.get('compositeQuery');
const query2 = params2.get('compositeQuery');
if (!query1 || !query2) return false;
const decoded1 = JSON.parse(decodeURIComponent(query1));
const decoded2 = JSON.parse(decodeURIComponent(query2));
const filtered1 = cloneDeep(decoded1);
const filtered2 = cloneDeep(decoded2);
delete filtered1.id;
delete filtered2.id;
return isEqual(filtered1, filtered2);
} catch (error) {
console.warn('Error comparing compositeQuery:', error);
return false;
}
}
return params1.get(param) === params2.get(param);
});
};
/**
* Determines if this navigation is adding default/initial parameters
* Returns true if:
* 1. We're staying on the same page (same pathname)
* 2. Either:
* - Current URL has no params and target URL has params, or
* - Target URL has new params that didn't exist in current URL
*/
const isDefaultNavigation = (currentUrl: URL, targetUrl: URL): boolean => {
// Different pathnames means it's not a default navigation
if (currentUrl.pathname !== targetUrl.pathname) return false;
const currentParams = new URLSearchParams(currentUrl.search);
const targetParams = new URLSearchParams(targetUrl.search);
// Case 1: Clean URL getting params for the first time
if (!currentParams.toString() && targetParams.toString()) return true;
// Case 2: Check for new params that didn't exist before
const currentKeys = new Set(Array.from(currentParams.keys()));
const targetKeys = new Set(Array.from(targetParams.keys()));
// Find keys that exist in target but not in current
const newKeys = Array.from(targetKeys).filter((key) => !currentKeys.has(key));
return newKeys.length > 0;
};
export const useSafeNavigate = (): {
safeNavigate: (
to: string | SafeNavigateParams,
options?: NavigateOptions,
) => void;
} => {
const navigate = useNavigate();
const location = useLocation();
const safeNavigate = useCallback(
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
const currentUrl = new URL(
`${location.pathname}${location.search}`,
window.location.origin,
);
let targetUrl: URL;
if (typeof to === 'string') {
targetUrl = new URL(to, window.location.origin);
} else {
targetUrl = new URL(
`${to.pathname || location.pathname}${to.search || ''}`,
window.location.origin,
);
}
const urlsAreSame = areUrlsEffectivelySame(currentUrl, targetUrl);
const isDefaultParamsNavigation = isDefaultNavigation(currentUrl, targetUrl);
if (urlsAreSame) {
return;
}
const navigationOptions = {
...options,
replace: isDefaultParamsNavigation || options?.replace,
};
if (typeof to === 'string') {
navigate(to, navigationOptions);
} else {
navigate(
{
pathname: to.pathname || location.pathname,
search: to.search,
},
navigationOptions,
);
}
},
[navigate, location.pathname, location.search],
);
return { safeNavigate };
};

View File

@@ -1,16 +1,15 @@
import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { useSafeNavigate } from './useSafeNavigate';
import useUrlQuery from './useUrlQuery';
const useUrlQueryData = <T>(
queryKey: string,
defaultData?: T,
): UseUrlQueryData<T> => {
const history = useHistory();
const location = useLocation();
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const query = useMemo(() => urlQuery.get(queryKey), [urlQuery, queryKey]);
@@ -33,9 +32,9 @@ const useUrlQueryData = <T>(
// Construct the new URL by combining the current pathname with the updated query string
const generatedUrl = `${location.pathname}?${currentUrlQuery.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
},
[location.pathname, queryKey, safeNavigate],
[history, location.pathname, queryKey],
);
return {

View File

@@ -20,7 +20,6 @@ import { urlKey } from 'container/AllError/utils';
import { RelativeTimeMap } from 'container/TopNav/DateTimeSelection/config';
import useAxiosError from 'hooks/useAxiosError';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import GetMinMax from 'lib/getMinMax';
@@ -322,8 +321,6 @@ export const useTimelineTable = ({
extra: any,
) => void;
} => {
const { safeNavigate } = useSafeNavigate();
const { pathname } = useLocation();
const { search } = useLocation();
@@ -346,7 +343,7 @@ export const useTimelineTable = ({
const updatedOrder = order === 'ascend' ? 'asc' : 'desc';
const params = new URLSearchParams(window.location.search);
safeNavigate(
history.replace(
`${pathname}?${createQueryParams({
...Object.fromEntries(params),
order: updatedOrder,
@@ -356,7 +353,7 @@ export const useTimelineTable = ({
);
}
},
[pathname, safeNavigate],
[pathname],
);
const offsetInt = parseInt(offset, 10);

View File

@@ -5,8 +5,8 @@ import ROUTES from 'constants/routes';
import AllAlertRules from 'container/ListAlertRules';
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
import TriggeredAlerts from 'container/TriggeredAlerts';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { GalleryVerticalEnd, Pyramid } from 'lucide-react';
import AlertDetails from 'pages/AlertDetails';
import { useLocation } from 'react-router-dom';
@@ -14,7 +14,6 @@ import { useLocation } from 'react-router-dom';
function AllAlertList(): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const tab = urlQuery.get('tab');
const isAlertHistory = location.pathname === ROUTES.ALERT_HISTORY;
@@ -68,7 +67,7 @@ function AllAlertList(): JSX.Element {
if (search) {
params += `&search=${search}`;
}
safeNavigate(`/alerts?${params}`);
history.replace(`/alerts?${params}`);
}}
className={`${
isAlertHistory || isAlertOverview ? 'alert-details-tabs' : ''

View File

@@ -4,8 +4,8 @@ import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import NewWidget from 'container/NewWidget';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useEffect, useState } from 'react';
import { generatePath, useLocation, useParams } from 'react-router-dom';
@@ -14,7 +14,6 @@ import { Widgets } from 'types/api/dashboard/getAll';
function DashboardWidget(): JSX.Element | null {
const { search } = useLocation();
const { dashboardId } = useParams<DashboardWidgetPageParams>();
const { safeNavigate } = useSafeNavigate();
const [selectedGraph, setSelectedGraph] = useState<PANEL_TYPES>();
@@ -33,11 +32,11 @@ function DashboardWidget(): JSX.Element | null {
const graphType = params.get('graphType') as PANEL_TYPES | null;
if (graphType === null) {
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
} else {
setSelectedGraph(graphType);
}
}, [dashboardId, safeNavigate, search]);
}, [dashboardId, search]);
if (selectedGraph === undefined || dashboardResponse.isLoading) {
return <Spinner tip="Loading.." />;

View File

@@ -29,12 +29,6 @@ jest.mock('react-router-dom', () => ({
const mockWindowOpen = jest.fn();
window.open = mockWindowOpen;
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('dashboard list page', () => {
// should render on updatedAt and descend when the column key and order is messed up
it('should render the list even when the columnKey or the order is mismatched', async () => {

View File

@@ -8,7 +8,6 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import EditRulesContainer from 'container/EditRules';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { useEffect } from 'react';
@@ -22,7 +21,6 @@ import {
} from './constants';
function EditRules(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const params = useUrlQuery();
const ruleId = params.get(QueryParams.ruleId);
const { t } = useTranslation('common');
@@ -57,9 +55,9 @@ function EditRules(): JSX.Element {
notifications.error({
message: 'Rule Id is required',
});
safeNavigate(ROUTES.LIST_ALL_ALERT);
history.replace(ROUTES.LIST_ALL_ALERT);
}
}, [isValidRuleId, ruleId, notifications, safeNavigate]);
}, [isValidRuleId, ruleId, notifications]);
if (
(isError && !isValidRuleId) ||

View File

@@ -4,8 +4,10 @@ import './Integrations.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, List, Typography } from 'antd';
import { FeatureKeys } from 'constants/features';
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
import { MoveUpRight, RotateCw } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { IntegrationsProps } from 'types/api/integrations/types';
import { isCloudUser } from 'utils/app';
@@ -44,10 +46,18 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
refetch,
} = useGetAllIntegrations();
const { featureFlags } = useAppContext();
const isAwsIntegrationEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.AWS_INTEGRATION)
?.active || false;
const filteredDataList = useMemo(() => {
let integrationsList: IntegrationsProps[] = [];
if (AWS_INTEGRATION.title.toLowerCase().includes(searchTerm.toLowerCase())) {
if (
isAwsIntegrationEnabled &&
AWS_INTEGRATION.title.toLowerCase().includes(searchTerm.toLowerCase())
) {
integrationsList.push(AWS_INTEGRATION);
}
@@ -62,7 +72,7 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
}
return integrationsList;
}, [data?.data.data.integrations, searchTerm]);
}, [data?.data.data.integrations, isAwsIntegrationEnabled, searchTerm]);
const loading = isLoading || isFetching || isRefetching;

View File

@@ -67,12 +67,6 @@ jest.mock('d3-interpolate', () => ({
interpolate: jest.fn(),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
const logsQueryServerRequest = (): void =>
server.use(
rest.post(queryRangeURL, (req, res, ctx) =>

View File

@@ -11,7 +11,7 @@ import logEvent from 'api/common/logEvent';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
import { isArray, isEmpty, isEqual } from 'lodash-es';
import {
Dispatch,
SetStateAction,
@@ -177,21 +177,6 @@ export function Filter(props: FilterProps): JSX.Element {
return items as TagFilterItem[];
};
const removeFilterItemIds = (query: Query): Query => {
const clonedQuery = cloneDeep(query);
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
...data,
filters: {
...data.filters,
items: data.filters?.items?.map((item) => ({
...item,
id: '',
})),
},
}));
return clonedQuery;
};
const handleRun = useCallback(
(props?: HandleRunProps): void => {
const preparedQuery: Query = {
@@ -219,16 +204,9 @@ export function Filter(props: FilterProps): JSX.Element {
});
}
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);
if (
isEqual(currentQueryWithoutIds, preparedQueryWithoutIds) &&
!props?.resetAll
) {
if (isEqual(currentQuery, preparedQuery) && !props?.resetAll) {
return;
}
redirectWithQueryBuilderData(preparedQuery);
},
[currentQuery, redirectWithQueryBuilderData, selectedFilters],

View File

@@ -116,12 +116,6 @@ jest.mock('react-redux', () => ({
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('TracesExplorer - Filters', () => {
// Initial filter panel rendering
// Test the initial state like which filters section are opened, default state of duration slider, etc.

View File

@@ -24,7 +24,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { cloneDeep, isEmpty, set } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -61,7 +61,6 @@ function TracesExplorer(): JSX.Element {
const currentPanelType = useGetPanelTypesQueryParam();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { safeNavigate } = useSafeNavigate();
const currentTab = panelType || PANEL_TYPES.LIST;
@@ -198,7 +197,7 @@ function TracesExplorer(): JSX.Element {
widgetId,
});
safeNavigate(dashboardEditView);
history.push(dashboardEditView);
},
onError: (error) => {
if (axios.isAxiosError(error)) {

View File

@@ -9,10 +9,10 @@ import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs, { Dayjs } from 'dayjs';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useAxiosError from 'hooks/useAxiosError';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useTabVisibility from 'hooks/useTabFocus';
import useUrlQuery from 'hooks/useUrlQuery';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history';
import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined';
@@ -73,7 +73,6 @@ const DashboardContext = createContext<IDashboardContext>({
setDashboardQueryRangeCalled: () => {},
selectedRowWidgetId: '',
setSelectedRowWidgetId: () => {},
isDashboardFetching: false,
});
interface Props {
@@ -84,7 +83,6 @@ interface Props {
export function DashboardProvider({
children,
}: PropsWithChildren): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
const [toScrollWidgetId, setToScrollWidgetId] = useState<string>('');
@@ -146,7 +144,7 @@ export function DashboardProvider({
params.set('order', sortOrder.order as string);
params.set('page', sortOrder.pagination || '1');
params.set('search', sortOrder.search || '');
safeNavigate({ search: params.toString() });
history.replace({ search: params.toString() });
}
const dispatch = useDispatch<Dispatch<AppActions>>();
@@ -194,8 +192,6 @@ export function DashboardProvider({
const { t } = useTranslation(['dashboard']);
const dashboardRef = useRef<Dashboard>();
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
const mergeDBWithLocalStorage = (
data: Dashboard,
localStorageVariables: any,
@@ -260,16 +256,10 @@ export function DashboardProvider({
[REACT_QUERY_KEY.DASHBOARD_BY_ID, isDashboardPage?.params],
{
enabled: (!!isDashboardPage || !!isDashboardWidgetPage) && isLoggedIn,
queryFn: async () => {
setIsDashboardFetching(true);
try {
return await getDashboard({
uuid: dashboardId,
});
} finally {
setIsDashboardFetching(false);
}
},
queryFn: () =>
getDashboard({
uuid: dashboardId,
}),
refetchOnWindowFocus: false,
onSuccess: (data) => {
const updatedDashboardData = transformDashboardVariables(data);
@@ -434,7 +424,6 @@ export function DashboardProvider({
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
isDashboardFetching,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
@@ -456,7 +445,6 @@ export function DashboardProvider({
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
isDashboardFetching,
],
);

View File

@@ -47,5 +47,4 @@ export interface IDashboardContext {
setDashboardQueryRangeCalled: (value: boolean) => void;
selectedRowWidgetId: string | null;
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
isDashboardFetching: boolean;
}

View File

@@ -20,10 +20,8 @@ import {
panelTypeDataSourceFormValuesMap,
PartialPanelTypes,
} from 'container/NewWidget/utils';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
@@ -40,7 +38,7 @@ import {
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
// ** Types
import {
@@ -97,6 +95,7 @@ export function QueryBuilderProvider({
children,
}: PropsWithChildren): JSX.Element {
const urlQuery = useUrlQuery();
const history = useHistory();
const location = useLocation();
const currentPathnameRef = useRef<string | null>(location.pathname);
@@ -748,23 +747,16 @@ export function QueryBuilderProvider({
);
const isStagedQueryUpdated = useCallback(
(
viewData: ViewProps[] | undefined,
viewKey: string,
options: OptionsQuery,
): boolean =>
(viewData: ViewProps[] | undefined, viewKey: string): boolean =>
isQueryUpdatedInView({
currentPanelType: panelType,
data: viewData,
stagedQuery,
viewKey,
options,
}),
[panelType, stagedQuery],
);
const { safeNavigate } = useSafeNavigate();
const redirectWithQueryBuilderData = useCallback(
(
query: Partial<Query>,
@@ -835,9 +827,9 @@ export function QueryBuilderProvider({
? `${redirectingUrl}?${urlQuery}`
: `${location.pathname}?${urlQuery}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
},
[location.pathname, safeNavigate, urlQuery],
[history, location.pathname, urlQuery],
);
const handleSetConfig = useCallback(

View File

@@ -88,17 +88,6 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigationType: (): any => 'PUSH',
}));
export function getAppContextMock(
role: string,
appContextOverrides?: Partial<IAppContext>,

View File

@@ -1,7 +1,6 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { Format } from 'container/NewWidget/RightContainer/types';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { Dispatch, SetStateAction } from 'react';
import {
IBuilderFormula,
@@ -247,7 +246,6 @@ export type QueryBuilderContextType = {
isStagedQueryUpdated: (
viewData: ViewProps[] | undefined,
viewKey: string,
options: OptionsQuery,
) => boolean;
isDefaultQuery: (props: IsDefaultQueryProps) => boolean;
};

View File

@@ -134,21 +134,3 @@ export const epochToTimeString = (epochMs: number): string => {
};
return date.toLocaleTimeString('en-US', options);
};
/**
* Converts nanoseconds to milliseconds
* @param timestamp - The timestamp to convert
* @returns The timestamp in milliseconds
*/
export const normalizeTimeToMs = (timestamp: number | string): number => {
let ts = timestamp;
if (typeof timestamp === 'string') {
ts = Math.trunc(parseInt(timestamp, 10));
}
ts = Number(ts);
// Check if timestamp is in nanoseconds (19+ digits)
const isNanoSeconds = ts.toString().length >= 19;
return isNanoSeconds ? Math.floor(ts / 1_000_000) : ts;
};

View File

@@ -0,0 +1,89 @@
package alertmanagerstoretest
import (
"context"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
var _ alertmanagerstore.Store = (*Provider)(nil)
type Provider struct {
States map[string]map[alertmanagertypes.StateName][]byte
Configs map[string]string
OrgIDs []string
}
func New(ctx context.Context, settings factory.ProviderSettings, config alertmanagerstore.Config, orgIDs []string) (*Provider, error) {
states := make(map[string]map[alertmanagertypes.StateName][]byte)
for _, orgID := range orgIDs {
states[orgID] = make(map[alertmanagertypes.StateName][]byte)
states[orgID][alertmanagertypes.SilenceStateName] = []byte{}
states[orgID][alertmanagertypes.NFLogStateName] = []byte{}
}
return &Provider{
States: states,
Configs: make(map[string]string),
OrgIDs: orgIDs,
}, nil
}
func (provider *Provider) GetState(ctx context.Context, orgID string, stateName alertmanagertypes.StateName) (string, error) {
if _, ok := provider.States[orgID][stateName]; !ok {
return "", errors.Newf(errors.TypeNotFound, alertmanagerstore.ErrCodeAlertmanagerStateNotFound, "cannot find state %q for org %q", stateName, orgID)
}
return string(provider.States[orgID][stateName]), nil
}
func (provider *Provider) SetState(ctx context.Context, orgID string, stateName alertmanagertypes.StateName, state alertmanagertypes.State) (int64, error) {
var err error
provider.States[orgID][stateName], err = state.MarshalBinary()
if err != nil {
return 0, err
}
return int64(len(provider.States[orgID][stateName])), nil
}
func (provider *Provider) GetConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
if _, ok := provider.Configs[orgID]; !ok {
return nil, errors.Newf(errors.TypeNotFound, alertmanagerstore.ErrCodeAlertmanagerConfigNotFound, "cannot find config for org %s", orgID)
}
return alertmanagertypes.NewConfigFromString(provider.Configs[orgID], orgID)
}
func (provider *Provider) SetConfig(ctx context.Context, orgID string, config *alertmanagertypes.Config) error {
provider.Configs[orgID] = string(config.Raw())
return nil
}
func (provider *Provider) DelConfig(ctx context.Context, orgID string) error {
delete(provider.Configs, orgID)
return nil
}
func (provider *Provider) ListOrgIDs(ctx context.Context) ([]string, error) {
return provider.OrgIDs, nil
}
func (provider *Provider) ListChannels(ctx context.Context, orgID string) (alertmanagertypes.Channels, error) {
if _, ok := provider.Configs[orgID]; !ok {
return nil, errors.Newf(errors.TypeNotFound, alertmanagerstore.ErrCodeAlertmanagerConfigNotFound, "cannot find config for org %s", orgID)
}
config, err := alertmanagertypes.NewConfigFromString(provider.Configs[orgID], orgID)
if err != nil {
return nil, err
}
return config.Channels(), nil
}
func (provider *Provider) GetChannel(ctx context.Context, orgID string, id uint64) (*alertmanagertypes.Channel, error) {
return nil, nil
}

View File

@@ -0,0 +1,17 @@
package alertmanagerstore
import "go.signoz.io/signoz/pkg/factory"
type Config struct {
Provider string `mapstructure:"provider"`
}
func NewConfig() factory.Config {
return Config{
Provider: "sql",
}
}
func (c Config) Validate() error {
return nil
}

View File

@@ -0,0 +1,248 @@
package sqlalertmanagerstore
import (
"context"
"database/sql"
"encoding/base64"
"fmt"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/sqlstore"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
type provider struct {
sqlstore sqlstore.SQLStore
settings factory.ScopedProviderSettings
}
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanagerstore.Store, alertmanagerstore.Config] {
return factory.NewProviderFactory(factory.MustNewName("sql"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanagerstore.Config) (alertmanagerstore.Store, error) {
return New(ctx, settings, config, sqlstore)
})
}
func New(ctx context.Context, settings factory.ProviderSettings, config alertmanagerstore.Config, sqlstore sqlstore.SQLStore) (*provider, error) {
return &provider{
sqlstore: sqlstore,
settings: factory.NewScopedProviderSettings(settings, "go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"),
}, nil
}
func (provider *provider) GetState(ctx context.Context, orgID string, stateName alertmanagertypes.StateName) (string, error) {
storedConfig := new(alertmanagertypes.StoredConfig)
err := provider.
sqlstore.
BunDB().
NewSelect().
Model(storedConfig).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return "", errors.Newf(errors.TypeNotFound, alertmanagerstore.ErrCodeAlertmanagerStateNotFound, "cannot find alertmanager state for org %s", orgID)
}
return "", err
}
if stateName == alertmanagertypes.SilenceStateName {
decodedState, err := base64.RawStdEncoding.DecodeString(storedConfig.SilencesState)
if err != nil {
return "", err
}
return string(decodedState), nil
}
if stateName == alertmanagertypes.NFLogStateName {
decodedState, err := base64.RawStdEncoding.DecodeString(storedConfig.NFLogState)
if err != nil {
return "", err
}
return string(decodedState), nil
}
return "", errors.Newf(errors.TypeNotFound, alertmanagerstore.ErrCodeAlertmanagerStateNotFound, "cannot find alertmanager state for org %s", orgID)
}
func (provider *provider) SetState(ctx context.Context, orgID string, stateName alertmanagertypes.StateName, state alertmanagertypes.State) (int64, error) {
marshalledState, err := state.MarshalBinary()
if err != nil {
return 0, err
}
encodedState := base64.StdEncoding.EncodeToString(marshalledState)
q := provider.
sqlstore.
BunDB().
NewUpdate().
Model(&alertmanagertypes.StoredConfig{}).
Where("org_id = ?", orgID)
if stateName == alertmanagertypes.SilenceStateName {
q.Set("silences_state = ?", encodedState)
}
if stateName == alertmanagertypes.NFLogStateName {
q.Set("nflog_state = ?", encodedState)
}
_, err = q.Exec(ctx)
if err != nil {
return 0, err
}
return int64(len(marshalledState)), nil
}
func (provider *provider) GetConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
storedConfig := new(alertmanagertypes.StoredConfig)
err := provider.
sqlstore.
BunDB().
NewSelect().
Model(storedConfig).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Newf(errors.TypeNotFound, alertmanagerstore.ErrCodeAlertmanagerConfigNotFound, "cannot find alertmanager config for org %s", orgID)
}
return nil, err
}
config, err := alertmanagertypes.NewConfigFromString(storedConfig.Config, orgID)
if err != nil {
return nil, err
}
return config, nil
}
func (provider *provider) SetConfig(ctx context.Context, orgID string, config *alertmanagertypes.Config) error {
tx, err := provider.sqlstore.BunDB().BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() //nolint:errcheck
if _, err = tx.
NewInsert().
Model(config.StoredConfig()).
On("CONFLICT (org_id) DO UPDATE").
Set("config = ?", string(config.StoredConfig().Config)).
Set("updated_at = ?", config.StoredConfig().UpdatedAt).
Exec(ctx); err != nil {
return err
}
channels := config.Channels()
fmt.Println("channels", channels)
if len(channels) != 0 {
fmt.Println("channels", channels)
if _, err = tx.NewInsert().
Model(&channels).
On("CONFLICT (name) DO UPDATE").
Set("data = EXCLUDED.data").
Set("updated_at = EXCLUDED.updated_at").
Exec(ctx); err != nil {
return err
}
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
func (provider *provider) DelConfig(ctx context.Context, orgID string) error {
_, err := provider.
sqlstore.
BunDB().
NewDelete().
Model(&alertmanagertypes.StoredConfig{}).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return err
}
_, err = provider.
sqlstore.
BunDB().
NewDelete().
Model(&alertmanagertypes.Channel{}).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return err
}
return nil
}
func (provider *provider) ListOrgIDs(ctx context.Context) ([]string, error) {
var orgIDs []string
err := provider.
sqlstore.
BunDB().
NewSelect().
Table("organizations").
ColumnExpr("id").
Scan(ctx, &orgIDs)
if err != nil {
return nil, err
}
return orgIDs, nil
}
func (provider *provider) ListChannels(ctx context.Context, orgID string) (alertmanagertypes.Channels, error) {
channels := alertmanagertypes.Channels{}
err := provider.
sqlstore.
BunDB().
NewSelect().
Model(&channels).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, err
}
return channels, nil
}
func (provider *provider) GetChannel(ctx context.Context, orgID string, id uint64) (*alertmanagertypes.Channel, error) {
channel := new(alertmanagertypes.Channel)
err := provider.
sqlstore.
BunDB().
NewSelect().
Model(channel).
Where("org_id = ?", orgID).
Where("id = ?", id).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Newf(errors.TypeNotFound, alertmanagerstore.ErrCodeAlertmanagerChannelNotFound, "cannot find channel for org %s", orgID)
}
return nil, err
}
return channel, nil
}

View File

@@ -0,0 +1,44 @@
package alertmanagerstore
import (
"context"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
var (
ErrCodeAlertmanagerConfigNotFound = errors.MustNewCode("alertmanager_config_not_found")
ErrCodeAlertmanagerStateNotFound = errors.MustNewCode("alertmanager_state_not_found")
ErrCodeAlertmanagerChannelNotFound = errors.MustNewCode("alertmanager_channel_not_found")
)
type Store interface {
// Creates the silence or the notification log state and returns the number of bytes in the state.
// The return type matches the return of `silence.Maintenance` or `nflog.Maintenance`.
// See https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/silence/silence.go#L217
// and https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/nflog/nflog.go#L94
SetState(context.Context, string, alertmanagertypes.StateName, alertmanagertypes.State) (int64, error)
// Gets the silence state or the notification log state as a string from the store. This is used as a snapshot to load the
// initial state of silences or notification log when starting the alertmanager.
GetState(context.Context, string, alertmanagertypes.StateName) (string, error)
// Get an alertmanager config for an organization
GetConfig(context.Context, string) (*alertmanagertypes.Config, error)
// Set an alertmanager config for an organization
SetConfig(context.Context, string, *alertmanagertypes.Config) error
// Deletes the config for an organization
DelConfig(context.Context, string) error
// Get all organization ids
ListOrgIDs(context.Context) ([]string, error)
// Get all channels for an organization
ListChannels(context.Context, string) (alertmanagertypes.Channels, error)
// Get a channel for an organization
GetChannel(context.Context, string, uint64) (*alertmanagertypes.Channel, error)
}

View File

@@ -0,0 +1,36 @@
package alertmanager
import (
"context"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
type Client interface {
// GetAlerts gets the alerts from the alertmanager per organization.
GetAlerts(context.Context, string, alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error)
// PutAlerts puts the alerts into the alertmanager per organization.
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
// SetConfig sets the config into the alertmanager per organization.
SetConfig(context.Context, string, alertmanagertypes.PostableConfig) error
// TestReceiver sends a test alert to a receiver.
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
// CreateChannel creates a channel for the organization.
CreateChannel(context.Context, string, *alertmanagertypes.Channel) error
// GetChannel gets a channel for the organization.
GetChannel(context.Context, string, uint64) (*alertmanagertypes.Channel, error)
// DeleteChannel deletes a channel for the organization.
DelChannel(context.Context, string, uint64) error
// ListChannels lists all channels for the organization.
ListChannels(context.Context, string) (alertmanagertypes.Channels, error)
// UpdateChannel updates a channel for the organization.
UpdateChannel(context.Context, string, uint64, string) error
}

View File

@@ -1,24 +1,27 @@
package server
package alertmanager
import (
"net/url"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/common/model"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore"
"go.signoz.io/signoz/pkg/factory"
)
type Config struct {
// PollInterval is the interval at which the alertmanager config is polled from the store.
PollInterval time.Duration `mapstructure:"poll_interval"`
// The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself.
// See https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/cmd/alertmanager/main.go#L155C54-L155C249
ExternalUrl *url.URL `mapstructure:"external_url"`
// GlobalConfig is the global configuration for the alertmanager
Global alertmanagertypes.GlobalConfig `mapstructure:"global"`
// ResolveTimeout is the time after which an alert is declared resolved if it has not been updated.
// See https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/config/config.go#L836
ResolveTimeout time.Duration `mapstructure:"resolve_timeout"`
// Config of the root node of the routing tree.
Route alertmanagertypes.RouteConfig `mapstructure:"route"`
Route RouteConfig `mapstructure:"route"`
// Configuration for alerts.
Alerts AlertsConfig `mapstructure:"alerts"`
@@ -28,6 +31,34 @@ type Config struct {
// Configuration for the notification log.
NFLog NFLogConfig `mapstructure:"nflog"`
// Configuration for the Email receiver. We are explicitly defining this here instead of taking it as part of the receiver configuration.
// This is because we want to use the same SMTP configuration for all receivers.
SMTP SMTPConfig `mapstructure:"smtp"`
// Configuration for the alertmanagerstore.
Store alertmanagerstore.Config `mapstructure:"store"`
}
type RouteConfig struct {
GroupBy []string `mapstructure:"group_by"`
GroupInterval time.Duration `mapstructure:"group_interval"`
GroupWait time.Duration `mapstructure:"group_wait"`
RepeatInterval time.Duration `mapstructure:"repeat_interval"`
}
// This is a best effort to make it similar to the upstream alertmanager config. See
// https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/config/config.go#L843
type SMTPConfig struct {
Hello string `mapstructure:"hello"`
From string `mapstructure:"from"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
AuthUsername string `mapstructure:"auth_username"`
AuthPassword string `mapstructure:"auth_password"`
AuthSecret string `mapstructure:"auth_secret"`
AuthIdentity string `mapstructure:"auth_identity"`
RequireTLS bool `mapstructure:"require_tls"`
}
type AlertsConfig struct {
@@ -64,21 +95,20 @@ type NFLogConfig struct {
Retention time.Duration `mapstructure:"retention"`
}
func NewConfig() Config {
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("alertmanager"), newConfig)
}
func newConfig() factory.Config {
return Config{
PollInterval: 15 * time.Second,
ExternalUrl: &url.URL{
Host: "localhost:8080",
},
Global: alertmanagertypes.GlobalConfig{
// Corresponds to the default in upstream (https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/config/config.go#L727)
ResolveTimeout: model.Duration(5 * time.Minute),
SMTPHello: "localhost",
SMTPFrom: "alertmanager@signoz.io",
SMTPSmarthost: config.HostPort{Host: "localhost", Port: "25"},
SMTPRequireTLS: true,
},
Route: alertmanagertypes.RouteConfig{
GroupByStr: []string{"alertname"},
// Corresponds to the default in upstream (https://github.com/prometheus/alertmanager/blob/3b06b97af4d146e141af92885a185891eb79a5b0/config/config.go#L727)
ResolveTimeout: 5 * time.Minute,
Route: RouteConfig{
GroupBy: []string{"alertname"},
GroupInterval: 5 * time.Minute,
GroupWait: 30 * time.Second,
RepeatInterval: 4 * time.Hour,
@@ -99,5 +129,17 @@ func NewConfig() Config {
MaintenanceInterval: 15 * time.Minute,
Retention: 120 * time.Hour,
},
SMTP: SMTPConfig{
Hello: "localhost",
From: "alertmanager@signoz.io",
Host: "localhost",
Port: 25,
RequireTLS: true,
},
Store: alertmanagerstore.NewConfig().(alertmanagerstore.Config),
}
}
func (c Config) Validate() error {
return nil
}

View File

@@ -1,8 +1,7 @@
package server
package alertmanager
import (
"context"
"log/slog"
"strings"
"sync"
"time"
@@ -16,12 +15,15 @@ import (
"github.com/prometheus/alertmanager/silence"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
var _ factory.Service = (*Server)(nil)
var (
// This is not a real file and will never be used. We need this placeholder to ensure maintenance runs on shutdown. See
// https://github.com/prometheus/server/blob/3ee2cd0f1271e277295c02b6160507b4d193dde2/silence/silence.go#L435-L438
@@ -30,24 +32,20 @@ var (
)
type Server struct {
// logger is the logger for the alertmanager
logger *slog.Logger
// registry is the prometheus registry for the alertmanager
registry *prometheus.Registry
// srvConfig is the server config for the alertmanager
srvConfig Config
// alertmanagerConfigHash is the hash of the alertmanager config
alertmanagerConfigHash [16]byte
// alertmanagerConfigRaw is the raw config of the alertmanager
alertmanagerConfigRaw []byte
// alertmanagerConfig is the config of the alertmanager
alertmanagerConfig *alertmanagertypes.Config
// Settings is the factorysettings for the alertmanager
settings factory.NamespacedSettings
// orgID is the orgID for the alertmanager
orgID string
// store is the backing store for the alertmanager
stateStore alertmanagertypes.StateStore
store alertmanagerstore.Store
// alertmanager primitives from upstream alertmanager
alerts *mem.Alerts
nflog *nflog.Log
@@ -64,26 +62,25 @@ type Server struct {
stopc chan struct{}
}
func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore) (*Server, error) {
func NewForOrg(ctx context.Context, settings factory.Settings, srvConfig Config, orgID string, store alertmanagerstore.Store) (*Server, error) {
server := &Server{
logger: logger.With("pkg", "go.signoz.io/pkg/alertmanager/server"),
registry: registry,
srvConfig: srvConfig,
orgID: orgID,
stateStore: stateStore,
stopc: make(chan struct{}),
srvConfig: srvConfig,
settings: factory.NewNamespacedSettings(settings, "go.signoz.io/signoz/pkg/alertmanager"),
orgID: orgID,
store: store,
stopc: make(chan struct{}),
}
// initialize marker
server.marker = alertmanagertypes.NewMarker(server.registry)
server.marker = alertmanagertypes.NewMarker(server.settings.PrometheusRegisterer())
// get silences for initial state
silencesstate, err := server.stateStore.Get(ctx, server.orgID, alertmanagertypes.SilenceStateName)
silencesstate, err := store.GetState(ctx, server.orgID, alertmanagertypes.SilenceStateName)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
// get nflog for initial state
nflogstate, err := server.stateStore.Get(ctx, server.orgID, alertmanagertypes.NFLogStateName)
nflogstate, err := store.GetState(ctx, server.orgID, alertmanagertypes.NFLogStateName)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
@@ -96,8 +93,8 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
MaxSilences: func() int { return srvConfig.Silences.Max },
MaxSilenceSizeBytes: func() int { return srvConfig.Silences.MaxSizeBytes },
},
Metrics: server.registry,
Logger: server.logger,
Metrics: server.settings.PrometheusRegisterer(),
Logger: server.settings.Logger(),
})
if err != nil {
return nil, err
@@ -107,8 +104,8 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
server.nflog, err = nflog.New(nflog.Options{
SnapshotReader: strings.NewReader(nflogstate),
Retention: server.srvConfig.NFLog.Retention,
Metrics: server.registry,
Logger: server.logger,
Metrics: server.settings.PrometheusRegisterer(),
Logger: server.settings.Logger(),
})
if err != nil {
return nil, err
@@ -121,11 +118,11 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
server.silences.Maintenance(server.srvConfig.Silences.MaintenanceInterval, snapfnoop, server.stopc, func() (int64, error) {
// Delete silences older than the retention period.
if _, err := server.silences.GC(); err != nil {
server.logger.ErrorContext(ctx, "silence garbage collection", "error", err)
server.settings.Logger().ErrorContext(ctx, "silence garbage collection", "error", err)
// Don't return here - we need to snapshot our state first.
}
return server.stateStore.Set(ctx, server.orgID, alertmanagertypes.SilenceStateName, server.silences)
return server.store.SetState(ctx, server.orgID, alertmanagertypes.SilenceStateName, server.silences)
})
}()
@@ -136,25 +133,54 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
defer server.wg.Done()
server.nflog.Maintenance(server.srvConfig.NFLog.MaintenanceInterval, snapfnoop, server.stopc, func() (int64, error) {
if _, err := server.nflog.GC(); err != nil {
server.logger.ErrorContext(ctx, "notification log garbage collection", "error", err)
server.settings.Logger().ErrorContext(ctx, "notification log garbage collection", "error", err)
// Don't return without saving the current state.
}
return server.stateStore.Set(ctx, server.orgID, alertmanagertypes.NFLogStateName, server.nflog)
return server.store.SetState(ctx, server.orgID, alertmanagertypes.NFLogStateName, server.nflog)
})
}()
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, nil, server.logger, server.registry)
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, nil, server.settings.Logger(), server.settings.PrometheusRegisterer())
if err != nil {
return nil, err
}
server.pipelineBuilder = notify.NewPipelineBuilder(server.registry, featurecontrol.NoopFlags{})
server.dispatcherMetrics = dispatch.NewDispatcherMetrics(false, server.registry)
server.pipelineBuilder = notify.NewPipelineBuilder(server.settings.PrometheusRegisterer(), featurecontrol.NoopFlags{})
server.dispatcherMetrics = dispatch.NewDispatcherMetrics(false, server.settings.PrometheusRegisterer())
return server, nil
}
func (server *Server) Start(ctx context.Context) error {
config, err := server.store.GetConfig(ctx, server.orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
if config == nil {
config = alertmanagertypes.NewDefaultConfig(
server.srvConfig.ResolveTimeout,
server.srvConfig.SMTP.Hello,
server.srvConfig.SMTP.From,
server.srvConfig.SMTP.Host,
server.srvConfig.SMTP.Port,
server.srvConfig.SMTP.AuthUsername,
server.srvConfig.SMTP.AuthPassword,
server.srvConfig.SMTP.AuthSecret,
server.srvConfig.SMTP.AuthIdentity,
server.srvConfig.SMTP.RequireTLS,
server.srvConfig.Route.GroupBy,
server.srvConfig.Route.GroupInterval,
server.srvConfig.Route.GroupWait,
server.srvConfig.Route.RepeatInterval,
server.orgID,
)
}
return server.SetConfig(ctx, config)
}
func (server *Server) GetAlerts(ctx context.Context, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
return alertmanagertypes.NewGettableAlertsFromAlertProvider(server.alerts, server.alertmanagerConfig, server.marker.Status, func(labels model.LabelSet) {
server.inhibitor.Mutes(labels)
@@ -163,7 +189,7 @@ func (server *Server) GetAlerts(ctx context.Context, params alertmanagertypes.Ge
}
func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanagertypes.PostableAlerts) error {
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(postableAlerts, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(postableAlerts, server.srvConfig.ResolveTimeout, time.Now())
// Notification sending alert takes precedence over validation errors.
if err := server.alerts.Put(alerts...); err != nil {
@@ -177,6 +203,14 @@ func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanager
return nil
}
func (server *Server) ConfigHash() [16]byte {
return server.alertmanagerConfigHash
}
func (server *Server) ConfigRaw() []byte {
return server.alertmanagerConfigRaw
}
func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertmanagertypes.Config) error {
config := alertmanagerConfig.AlertmanagerConfig()
@@ -201,10 +235,10 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
for _, rcv := range config.Receivers {
if _, found := activeReceivers[rcv.Name]; !found {
// No need to build a receiver if no route is using it.
server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", "receiver", rcv.Name)
server.settings.Logger().InfoContext(ctx, "skipping creation of receiver not referenced by any route", "receiver", rcv.Name)
continue
}
integrations, err := alertmanagertypes.NewReceiverIntegrations(rcv, server.tmpl, server.logger)
integrations, err := alertmanagertypes.NewReceiverIntegrations(rcv, server.tmpl, server.settings.Logger())
if err != nil {
return err
}
@@ -232,9 +266,9 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
server.dispatcher.Stop()
}
server.inhibitor = inhibit.NewInhibitor(server.alerts, config.InhibitRules, server.marker, server.logger)
server.inhibitor = inhibit.NewInhibitor(server.alerts, config.InhibitRules, server.marker, server.settings.Logger())
server.timeIntervals = timeIntervals
server.silencer = silence.NewSilencer(server.silences, server.marker, server.logger)
server.silencer = silence.NewSilencer(server.silences, server.marker, server.settings.Logger())
var pipelinePeer notify.Peer
pipeline := server.pipelineBuilder.New(
@@ -262,22 +296,25 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
server.marker,
timeoutFunc,
nil,
server.logger,
server.settings.Logger(),
server.dispatcherMetrics,
)
// Do not try to add these to server.wg as there seems to be a race condition if
// Do not try to add these to `server.wg as there seems to be a race condition if
// we call Start() and Stop() in quick succession.
// Both these goroutines will run indefinitely.
go server.dispatcher.Run()
go server.inhibitor.Run()
server.alertmanagerConfigHash = alertmanagerConfig.Hash()
server.alertmanagerConfigRaw = alertmanagerConfig.Raw()
server.alertmanagerConfig = alertmanagerConfig
return nil
}
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
return alertmanagertypes.TestReceiver(ctx, receiver, server.tmpl, server.logger)
return alertmanagertypes.TestReceiver(ctx, receiver, server.tmpl, server.settings.Logger())
}
func (server *Server) Stop(ctx context.Context) error {

View File

@@ -1,126 +0,0 @@
package server
import (
"bytes"
"context"
"io"
"log/slog"
"net"
"net/http"
"net/url"
"testing"
"time"
"github.com/go-openapi/strfmt"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/client_golang/prometheus"
commoncfg "github.com/prometheus/common/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
"go.signoz.io/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
)
func TestServerSetConfigAndStop(t *testing.T) {
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore())
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{}, "1")
require.NoError(t, err)
assert.NoError(t, server.SetConfig(context.Background(), amConfig))
assert.NoError(t, server.Stop(context.Background()))
}
func TestServerTestReceiverTypeWebhook(t *testing.T) {
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore())
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{}, "1")
require.NoError(t, err)
webhookListener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
requestBody := new(bytes.Buffer)
webhookServer := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := requestBody.ReadFrom(r.Body)
require.NoError(t, err)
w.WriteHeader(http.StatusOK)
}),
}
go func() {
require.NoError(t, webhookServer.Serve(webhookListener))
}()
require.NoError(t, server.SetConfig(context.Background(), amConfig))
defer require.NoError(t, server.Stop(context.Background()))
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
require.NoError(t, err)
err = server.TestReceiver(context.Background(), alertmanagertypes.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: &config.SecretURL{URL: webhookURL},
},
},
})
assert.NoError(t, err)
assert.Contains(t, requestBody.String(), "test-receiver")
assert.Contains(t, requestBody.String(), "firing")
}
func TestServerPutAlerts(t *testing.T) {
stateStore := alertmanagertypestest.NewStateStore()
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(&config.Route{Receiver: "test-receiver", Continue: true}, alertmanagertypes.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: &config.SecretURL{URL: &url.URL{Host: "localhost", Path: "/test-receiver"}},
},
},
}))
require.NoError(t, server.SetConfig(context.Background(), amConfig))
require.NoError(t, server.PutAlerts(context.Background(), alertmanagertypes.PostableAlerts{
{
Annotations: models.LabelSet{"alertname": "test-alert"},
StartsAt: strfmt.DateTime(time.Now().Add(-time.Hour)),
EndsAt: strfmt.DateTime(time.Now().Add(time.Hour)),
Alert: models.Alert{
GeneratorURL: "http://localhost:8080/test-alert",
Labels: models.LabelSet{"alertname": "test-alert"},
},
},
}))
require.NotEmpty(t, server.alerts)
dummyRequest, err := http.NewRequest(http.MethodGet, "/alerts", nil)
require.NoError(t, err)
params, err := alertmanagertypes.NewGettableAlertsParams(dummyRequest)
require.NoError(t, err)
gettableAlerts, err := server.GetAlerts(context.Background(), params)
require.NoError(t, err)
assert.Equal(t, 1, len(gettableAlerts))
assert.Equal(t, gettableAlerts[0].Alert.Labels["alertname"], "test-alert")
assert.NoError(t, server.Stop(context.Background()))
}

View File

@@ -0,0 +1,84 @@
package alertmanager
import (
"bytes"
"context"
"net"
"net/http"
"net/url"
"testing"
"github.com/prometheus/alertmanager/config"
commoncfg "github.com/prometheus/common/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/alertmanagerstoretest"
"go.signoz.io/signoz/pkg/factory/factorytest"
"go.signoz.io/signoz/pkg/factory/providertest"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
func TestServerStartStop(t *testing.T) {
store, err := alertmanagerstoretest.New(context.Background(), providertest.NewSettings(), alertmanagerstore.NewConfig().(alertmanagerstore.Config), []string{"1"})
require.NoError(t, err)
server, err := NewForOrg(context.Background(), factorytest.NewSettings(), newConfig().(Config), "1", store)
require.NoError(t, err)
require.NoError(t, server.Start(context.Background()))
require.NoError(t, server.Stop(context.Background()))
}
func TestServerWithDefaultConfig(t *testing.T) {
store, err := alertmanagerstoretest.New(context.Background(), providertest.NewSettings(), alertmanagerstore.NewConfig().(alertmanagerstore.Config), []string{"1"})
require.NoError(t, err)
server, err := NewForOrg(context.Background(), factorytest.NewSettings(), newConfig().(Config), "1", store)
require.NoError(t, err)
require.NoError(t, server.Start(context.Background()))
defer require.NoError(t, server.Stop(context.Background()))
assert.Equal(t, `{"global":{"resolve_timeout":"5m","http_config":{"tls_config":{"insecure_skip_verify":false},"follow_redirects":true,"enable_http2":true,"proxy_url":null},"smtp_from":"alertmanager@signoz.io","smtp_hello":"localhost","smtp_smarthost":"localhost:25","smtp_require_tls":true,"smtp_tls_config":{"insecure_skip_verify":false},"pagerduty_url":"https://events.pagerduty.com/v2/enqueue","opsgenie_api_url":"https://api.opsgenie.com/","wechat_api_url":"https://qyapi.weixin.qq.com/cgi-bin/","victorops_api_url":"https://alert.victorops.com/integrations/generic/20131114/alert/","telegram_api_url":"https://api.telegram.org","webex_api_url":"https://webexapis.com/v1/messages","rocketchat_api_url":"https://open.rocket.chat/"},"route":{"receiver":"default-receiver","group_by":["alertname"],"group_wait":"30s","group_interval":"5m","repeat_interval":"4h"},"receivers":[{"name":"default-receiver"}],"templates":null}`, string(server.alertmanagerConfigRaw))
}
func TestServerTestReceiverWebhook(t *testing.T) {
store, err := alertmanagerstoretest.New(context.Background(), providertest.NewSettings(), alertmanagerstore.NewConfig().(alertmanagerstore.Config), []string{"1"})
require.NoError(t, err)
server, err := NewForOrg(context.Background(), factorytest.NewSettings(), newConfig().(Config), "1", store)
require.NoError(t, err)
webhookListener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
requestBody := new(bytes.Buffer)
webhookServer := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := requestBody.ReadFrom(r.Body)
require.NoError(t, err)
w.WriteHeader(http.StatusOK)
}),
}
go func() {
require.NoError(t, webhookServer.Serve(webhookListener))
}()
require.NoError(t, server.Start(context.Background()))
defer require.NoError(t, server.Stop(context.Background()))
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
require.NoError(t, err)
err = server.TestReceiver(context.Background(), alertmanagertypes.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: &config.SecretURL{URL: webhookURL},
},
},
})
require.NoError(t, err)
require.Contains(t, requestBody.String(), "test-receiver")
require.Contains(t, requestBody.String(), "firing")
}

214
pkg/alertmanager/servers.go Normal file
View File

@@ -0,0 +1,214 @@
package alertmanager
import (
"context"
"sync"
"time"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
var (
ErrCodeAlertmanagerNotFound = errors.MustNewCode("alertmanager_not_found")
)
var _ factory.Service = (*Servers)(nil)
var _ Client = (*Servers)(nil)
type Servers struct {
config Config
// Store is the store for the alertmanager
store alertmanagerstore.Store
// Map of organization id to server
servers map[string]*Server
// Mutex to protect the servers map
serversMtx sync.RWMutex
}
func New(ctx context.Context, settings factory.Settings, config Config, store alertmanagerstore.Store) (*Servers, error) {
servers := &Servers{
config: config,
store: store,
servers: map[string]*Server{},
serversMtx: sync.RWMutex{},
}
orgIDs, err := store.ListOrgIDs(ctx)
if err != nil {
return nil, err
}
for _, orgID := range orgIDs {
server, err := NewForOrg(ctx, settings, config, orgID, store)
if err != nil {
return nil, err
}
servers.servers[orgID] = server
}
return servers, nil
}
func (ss *Servers) Start(ctx context.Context) error {
for _, server := range ss.servers {
err := server.Start(ctx)
if err != nil {
return err
}
}
ticker := time.NewTicker(ss.config.PollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
for _, server := range ss.servers {
config, err := ss.store.GetConfig(ctx, server.orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
server.settings.Logger().ErrorContext(ctx, "failed to get config", "error", err, "orgID", server.orgID)
continue
}
if config == nil {
config = alertmanagertypes.NewDefaultConfig(
server.srvConfig.ResolveTimeout,
server.srvConfig.SMTP.Hello,
server.srvConfig.SMTP.From,
server.srvConfig.SMTP.Host,
server.srvConfig.SMTP.Port,
server.srvConfig.SMTP.AuthUsername,
server.srvConfig.SMTP.AuthPassword,
server.srvConfig.SMTP.AuthSecret,
server.srvConfig.SMTP.AuthIdentity,
server.srvConfig.SMTP.RequireTLS,
server.srvConfig.Route.GroupBy,
server.srvConfig.Route.GroupInterval,
server.srvConfig.Route.GroupWait,
server.srvConfig.Route.RepeatInterval,
server.orgID,
)
}
if err := server.SetConfig(ctx, config); err != nil {
server.settings.Logger().ErrorContext(ctx, "failed to set config in alertmanager", "error", err, "orgID", server.orgID)
}
}
}
}
}
func (ss *Servers) Stop(ctx context.Context) error {
for _, server := range ss.servers {
server.Stop(ctx)
}
return nil
}
func (ss *Servers) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
server, ok := ss.servers[orgID]
if !ok {
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerNotFound, "alertmanager not found for orgID %q", orgID)
}
return server.GetAlerts(ctx, params)
}
func (ss *Servers) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
server, ok := ss.servers[orgID]
if !ok {
return errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerNotFound, "alertmanager not found for orgID %q", orgID)
}
return server.PutAlerts(ctx, alerts)
}
func (ss *Servers) SetConfig(ctx context.Context, orgID string, postableConfig alertmanagertypes.PostableConfig) error {
cfg, err := ss.store.GetConfig(ctx, orgID)
if err != nil {
return err
}
err = cfg.MergeWithPostableConfig(postableConfig)
if err != nil {
return err
}
if err := ss.store.SetConfig(ctx, orgID, cfg); err != nil {
return err
}
return nil
}
func (ss *Servers) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
server, ok := ss.servers[orgID]
if !ok {
return errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerNotFound, "alertmanager not found for orgID %q", orgID)
}
return server.TestReceiver(ctx, receiver)
}
func (ss *Servers) CreateChannel(ctx context.Context, orgID string, channel *alertmanagertypes.Channel) error {
receiver, err := alertmanagertypes.NewReceiverFromChannel(channel)
if err != nil {
return err
}
return ss.SetConfig(ctx, orgID, alertmanagertypes.PostableConfig{
Action: alertmanagertypes.PostableConfigActionCreate,
Receiver: receiver,
})
}
func (ss *Servers) GetChannel(ctx context.Context, orgID string, id uint64) (*alertmanagertypes.Channel, error) {
return ss.store.GetChannel(ctx, orgID, id)
}
func (ss *Servers) DelChannel(ctx context.Context, orgID string, id uint64) error {
channel, err := ss.store.GetChannel(ctx, orgID, id)
if err != nil {
return err
}
receiver, err := alertmanagertypes.NewReceiverFromChannel(channel)
if err != nil {
return err
}
return ss.SetConfig(ctx, orgID, alertmanagertypes.PostableConfig{
Action: alertmanagertypes.PostableConfigActionDelete,
Receiver: receiver,
})
}
func (ss *Servers) ListChannels(ctx context.Context, orgID string) (alertmanagertypes.Channels, error) {
return ss.store.ListChannels(ctx, orgID)
}
func (ss *Servers) UpdateChannel(ctx context.Context, orgID string, id uint64, data string) error {
existingChannel, err := ss.store.GetChannel(ctx, orgID, id)
if err != nil {
return err
}
existingChannel.Data = data
existingChannel.UpdatedAt = time.Now()
receiver, err := alertmanagertypes.NewReceiverFromChannel(existingChannel)
if err != nil {
return err
}
return ss.SetConfig(ctx, orgID, alertmanagertypes.PostableConfig{
Action: alertmanagertypes.PostableConfigActionUpdate,
Receiver: receiver,
})
}

View File

@@ -0,0 +1,10 @@
package factorytest
import (
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/instrumentation/instrumentationtest"
)
func NewSettings() factory.Settings {
return instrumentationtest.New().ToFactorySettings()
}

View File

@@ -2,9 +2,12 @@ package factory
import (
"fmt"
"log/slog"
"regexp"
)
var _ slog.LogValuer = Name{}
var (
// nameRegex is a regex that matches a valid name.
// It must start with a alphabet, and can only contain alphabets, numbers, underscores or hyphens.
@@ -15,6 +18,10 @@ type Name struct {
name string
}
func (n Name) LogValue() slog.Value {
return slog.StringValue(n.name)
}
func (n Name) String() string {
return n.name
}

View File

@@ -1,6 +1,8 @@
package factory
import "context"
import (
"context"
)
type Service interface {
// Starts a service. The service should return an error if it cannot be started.
@@ -8,3 +10,24 @@ type Service interface {
// Stops a service.
Stop(context.Context) error
}
type NamedService interface {
Named
Service
}
type namedService struct {
name Name
Service
}
func (s *namedService) Name() Name {
return s.name
}
func NewNamedService(name Name, service Service) NamedService {
return &namedService{
name: name,
Service: service,
}
}

View File

@@ -8,6 +8,56 @@ import (
sdktrace "go.opentelemetry.io/otel/trace"
)
type Settings struct {
// Logger is the logger.
Logger *slog.Logger
// MeterProvider is the meter provider.
MeterProvider sdkmetric.MeterProvider
// TracerProvider is the tracer provider.
TracerProvider sdktrace.TracerProvider
// PrometheusRegisterer is the prometheus registerer.
PrometheusRegisterer prometheus.Registerer
}
type NamespacedSettings interface {
Logger() *slog.Logger
Meter() sdkmetric.Meter
Tracer() sdktrace.Tracer
PrometheusRegisterer() prometheus.Registerer
}
type namespacedSettings struct {
logger *slog.Logger
meter sdkmetric.Meter
tracer sdktrace.Tracer
prometheusRegisterer prometheus.Registerer
}
func NewNamespacedSettings(settings Settings, pkgName string) NamespacedSettings {
return &namespacedSettings{
logger: settings.Logger.With("pkg", pkgName),
meter: settings.MeterProvider.Meter(pkgName),
tracer: settings.TracerProvider.Tracer(pkgName),
prometheusRegisterer: prometheus.WrapRegistererWith(prometheus.Labels{"pkg": pkgName}, settings.PrometheusRegisterer),
}
}
func (s *namespacedSettings) Logger() *slog.Logger {
return s.logger
}
func (s *namespacedSettings) Meter() sdkmetric.Meter {
return s.meter
}
func (s *namespacedSettings) Tracer() sdktrace.Tracer {
return s.tracer
}
func (s *namespacedSettings) PrometheusRegisterer() prometheus.Registerer {
return s.prometheusRegisterer
}
type ProviderSettings struct {
// SlogLogger is the slog logger.
Logger *slog.Logger

View File

@@ -8,8 +8,16 @@ import (
sdkresource "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/trace"
"go.signoz.io/signoz/pkg/factory"
"go.uber.org/zap/zapcore"
)
var zapLogLevelToSlogLevel = map[zapcore.Level]slog.Level{
zapcore.DebugLevel: slog.LevelDebug,
zapcore.InfoLevel: slog.LevelInfo,
zapcore.WarnLevel: slog.LevelWarn,
zapcore.ErrorLevel: slog.LevelError,
}
// Instrumentation provides the core components for application instrumentation.
type Instrumentation interface {
// Logger returns the Slog logger.
@@ -22,6 +30,8 @@ type Instrumentation interface {
PrometheusRegisterer() prometheus.Registerer
// ToProviderSettings converts instrumentation to provider settings.
ToProviderSettings() factory.ProviderSettings
// ToFactorySettings converts instrumentation to factory settings.
ToFactorySettings() factory.Settings
}
// Merges the input attributes with the resource attributes.

View File

@@ -51,3 +51,12 @@ func (i *noopInstrumentation) ToProviderSettings() factory.ProviderSettings {
PrometheusRegisterer: i.PrometheusRegisterer(),
}
}
func (i *noopInstrumentation) ToFactorySettings() factory.Settings {
return factory.Settings{
Logger: i.Logger(),
MeterProvider: i.MeterProvider(),
TracerProvider: i.TracerProvider(),
PrometheusRegisterer: i.PrometheusRegisterer(),
}
}

View File

@@ -131,3 +131,12 @@ func (i *SDK) ToProviderSettings() factory.ProviderSettings {
PrometheusRegisterer: i.PrometheusRegisterer(),
}
}
func (i *SDK) ToFactorySettings() factory.Settings {
return factory.Settings{
Logger: i.Logger(),
MeterProvider: i.MeterProvider(),
TracerProvider: i.TracerProvider(),
PrometheusRegisterer: i.PrometheusRegisterer(),
}
}

View File

@@ -190,10 +190,18 @@ func validateServiceDefinition(s *CloudServiceDetails) error {
// Validate dashboard data
seenDashboardIds := map[string]interface{}{}
for _, dd := range s.Assets.Dashboards {
if _, seen := seenDashboardIds[dd.Id]; seen {
return fmt.Errorf("multiple dashboards found with id %s", dd.Id)
did, exists := dd["id"]
if !exists {
return fmt.Errorf("id is required. not specified in dashboard titled %v", dd["title"])
}
seenDashboardIds[dd.Id] = nil
dashboardId, ok := did.(string)
if !ok {
return fmt.Errorf("id must be string in dashboard titled %v", dd["title"])
}
if _, seen := seenDashboardIds[dashboardId]; seen {
return fmt.Errorf("multiple dashboards found with id %s", dashboardId)
}
seenDashboardIds[dashboardId] = nil
}
if s.TelemetryCollectionStrategy == nil {

View File

@@ -3,13 +3,10 @@ package cloudintegrations
import (
"context"
"fmt"
"net/url"
"slices"
"strings"
"time"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
"go.signoz.io/signoz/pkg/query-service/model"
"golang.org/x/exp/maps"
)
@@ -123,30 +120,12 @@ func (c *Controller) GenerateConnectionUrl(
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
}
// TODO(Raj): parameterized this in follow up changes
agentVersion := "0.0.1"
// TODO(Raj): Add actual cloudformation template for AWS integration after it has been shipped.
connectionUrl := fmt.Sprintf(
"https://%s.console.aws.amazon.com/cloudformation/home?region=%s#/stacks/quickcreate?",
"https://%s.console.aws.amazon.com/cloudformation/home?region=%s#/stacks/quickcreate?stackName=SigNozIntegration/",
req.AgentConfig.Region, req.AgentConfig.Region,
)
for qp, value := range map[string]string{
"param_SigNozIntegrationAgentVersion": agentVersion,
"param_SigNozApiUrl": req.AgentConfig.SigNozAPIUrl,
"param_SigNozApiKey": req.AgentConfig.SigNozAPIKey,
"param_SigNozAccountId": account.Id,
"param_IngestionUrl": req.AgentConfig.IngestionUrl,
"param_IngestionKey": req.AgentConfig.IngestionKey,
"stackName": "signoz-integration",
"templateURL": fmt.Sprintf(
"https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json",
agentVersion,
),
} {
connectionUrl += fmt.Sprintf("&%s=%s", qp, url.QueryEscape(value))
}
return &GenerateConnectionUrlResponse{
AccountId: account.Id,
ConnectionUrl: connectionUrl,
@@ -424,18 +403,6 @@ func (c *Controller) GetServiceDetails(
if config != nil {
service.Config = config
if config.Metrics != nil && config.Metrics.Enabled {
// add links to service dashboards, making them clickable.
for i, d := range service.Assets.Dashboards {
dashboardUuid := c.dashboardUuid(
cloudProvider, serviceId, d.Id,
)
service.Assets.Dashboards[i].Url = fmt.Sprintf(
"/dashboard/%s", dashboardUuid,
)
}
}
}
}
@@ -489,134 +456,3 @@ func (c *Controller) UpdateServiceConfig(
Config: *updatedConfig,
}, nil
}
// All dashboards that are available based on cloud integrations configuration
// across all cloud providers
func (c *Controller) AvailableDashboards(ctx context.Context) (
[]dashboards.Dashboard, *model.ApiError,
) {
allDashboards := []dashboards.Dashboard{}
for _, provider := range []string{"aws"} {
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, provider)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, fmt.Sprintf("couldn't get available dashboards for %s", provider),
)
}
allDashboards = append(allDashboards, providerDashboards...)
}
return allDashboards, nil
}
func (c *Controller) AvailableDashboardsForCloudProvider(
ctx context.Context, cloudProvider string,
) ([]dashboards.Dashboard, *model.ApiError) {
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
}
// for v0, service dashboards are only available when metrics are enabled.
servicesWithAvailableMetrics := map[string]*time.Time{}
for _, ar := range accountRecords {
if ar.CloudAccountId != nil {
configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount(
ctx, cloudProvider, *ar.CloudAccountId,
)
if apiErr != nil {
return nil, apiErr
}
for svcId, config := range configsBySvcId {
if config.Metrics != nil && config.Metrics.Enabled {
servicesWithAvailableMetrics[svcId] = &ar.CreatedAt
}
}
}
}
allServices, apiErr := listCloudProviderServices(cloudProvider)
if apiErr != nil {
return nil, apiErr
}
svcDashboards := []dashboards.Dashboard{}
for _, svc := range allServices {
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
if serviceDashboardsCreatedAt != nil {
for _, d := range svc.Assets.Dashboards {
isLocked := 1
author := fmt.Sprintf("%s-integration", cloudProvider)
svcDashboards = append(svcDashboards, dashboards.Dashboard{
Uuid: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
Locked: &isLocked,
Data: *d.Definition,
CreatedAt: *serviceDashboardsCreatedAt,
CreateBy: &author,
UpdatedAt: *serviceDashboardsCreatedAt,
UpdateBy: &author,
})
}
servicesWithAvailableMetrics[svc.Id] = nil
}
}
return svcDashboards, nil
}
func (c *Controller) GetDashboardById(
ctx context.Context,
dashboardUuid string,
) (*dashboards.Dashboard, *model.ApiError) {
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
if apiErr != nil {
return nil, apiErr
}
allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, fmt.Sprintf("couldn't list available dashboards"),
)
}
for _, d := range allDashboards {
if d.Uuid == dashboardUuid {
return &d, nil
}
}
return nil, model.NotFoundError(fmt.Errorf(
"couldn't find dashboard with uuid: %s", dashboardUuid,
))
}
func (c *Controller) dashboardUuid(
cloudProvider string, svcId string, dashboardId string,
) string {
return fmt.Sprintf(
"cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId,
)
}
func (c *Controller) parseDashboardUuid(dashboardUuid string) (
cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError,
) {
parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 || parts[0] != "cloud-integration" {
return "", "", "", model.BadRequest(fmt.Errorf(
"invalid cloud integration dashboard id",
))
}
return parts[1], parts[2], parts[3], nil
}
func (c *Controller) IsCloudIntegrationDashboardUuid(dashboardUuid string) bool {
_, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
return apiErr == nil
}

View File

@@ -183,16 +183,7 @@ type CloudServiceMetricsConfig struct {
}
type CloudServiceAssets struct {
Dashboards []CloudServiceDashboard `json:"dashboards"`
}
type CloudServiceDashboard struct {
Id string `json:"id"`
Url string `json:"url"`
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
Definition *dashboards.Data `json:"definition,omitempty"`
Dashboards []dashboards.Data `json:"dashboards"`
}
type SupportedSignals struct {

View File

@@ -3,6 +3,9 @@
"title": "EC2",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"assets": {
"dashboards": []
},
"supported_signals": {
"metrics": true,
"logs": false
@@ -10,484 +13,16 @@
"data_collected": {
"metrics": [
{
"name": "aws_EC2_CPUCreditBalance_count",
"unit": "Count",
"name": "ec2_cpuutilization_average",
"type": "Gauge",
"description": ""
"unit": "number",
"description": "CloudWatch metric CPUUtilization"
},
{
"name": "aws_EC2_CPUCreditBalance_max",
"unit": "Count",
"name": "ec2_cpuutilization_maximum",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUCreditBalance_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUCreditBalance_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUCreditUsage_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUCreditUsage_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUCreditUsage_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUCreditUsage_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditBalance_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditBalance_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditBalance_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditBalance_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditsCharged_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditsCharged_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditsCharged_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUSurplusCreditsCharged_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_CPUUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSByteBalance__count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSByteBalance__max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSByteBalance__min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSByteBalance__sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSIOBalance__count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSIOBalance__max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSIOBalance__min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSIOBalance__sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadBytes_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadBytes_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadBytes_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadBytes_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadOps_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadOps_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadOps_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSReadOps_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteBytes_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteBytes_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteBytes_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteBytes_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteOps_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteOps_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteOps_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_EBSWriteOps_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_MetadataNoToken_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_MetadataNoToken_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_MetadataNoToken_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_MetadataNoToken_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkIn_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkIn_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkIn_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkIn_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkOut_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkOut_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkOut_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkOut_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsIn_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsIn_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsIn_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsIn_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsOut_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsOut_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsOut_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_NetworkPacketsOut_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_Instance_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_Instance_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_Instance_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_Instance_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_System_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_System_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_System_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_System_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_EC2_StatusCheckFailed_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
"unit": "number",
"description": "CloudWatch metric CPUUtilization"
}
],
"logs": []
@@ -500,16 +35,5 @@
}
]
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "EC2 Overview",
"description": "Overview of EC2",
"image": "file://assets/dashboards/overview.png",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -3,6 +3,9 @@
"title": "Amazon RDS",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"assets": {
"dashboards": []
},
"supported_signals": {
"metrics": true,
"logs": true
@@ -10,767 +13,19 @@
"data_collected": {
"metrics": [
{
"name": "aws_RDS_BurstBalance_count",
"unit": "Percent",
"name": "rds_postgres_cpuutilization_average",
"type": "Gauge",
"description": ""
"unit": "number",
"description": "CloudWatch metric CPUUtilization"
},
{
"name": "aws_RDS_BurstBalance_max",
"unit": "Percent",
"name": "rds_postgres_cpuutilization_maximum",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_BurstBalance_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_BurstBalance_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditBalance_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditBalance_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditBalance_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditBalance_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditUsage_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditUsage_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditUsage_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUCreditUsage_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditBalance_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditBalance_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditBalance_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditBalance_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditsCharged_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditsCharged_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditsCharged_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUSurplusCreditsCharged_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CPUUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CheckpointLag_count",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CheckpointLag_max",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CheckpointLag_min",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_CheckpointLag_sum",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadCPU_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadCPU_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadCPU_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadCPU_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadNonCPU_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadNonCPU_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadNonCPU_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadNonCPU_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadRelativeToNumVCPUs_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadRelativeToNumVCPUs_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadRelativeToNumVCPUs_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoadRelativeToNumVCPUs_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoad_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoad_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoad_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DBLoad_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DatabaseConnections_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DatabaseConnections_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DatabaseConnections_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DatabaseConnections_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DiskQueueDepth_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DiskQueueDepth_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DiskQueueDepth_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_DiskQueueDepth_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSByteBalance__count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSByteBalance__max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSByteBalance__min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSByteBalance__sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSIOBalance__count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSIOBalance__max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSIOBalance__min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_EBSIOBalance__sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeStorageSpace_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeStorageSpace_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeStorageSpace_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeStorageSpace_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeableMemory_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeableMemory_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeableMemory_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_FreeableMemory_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_MaximumUsedTransactionIDs_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_MaximumUsedTransactionIDs_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_MaximumUsedTransactionIDs_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_MaximumUsedTransactionIDs_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkReceiveThroughput_count",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkReceiveThroughput_max",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkReceiveThroughput_min",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkReceiveThroughput_sum",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkTransmitThroughput_count",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkTransmitThroughput_max",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkTransmitThroughput_min",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_NetworkTransmitThroughput_sum",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_OldestReplicationSlotLag_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_OldestReplicationSlotLag_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_OldestReplicationSlotLag_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_OldestReplicationSlotLag_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadIOPS_count",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadIOPS_max",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadIOPS_min",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadIOPS_sum",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadLatency_count",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadLatency_max",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadLatency_min",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadLatency_sum",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadThroughput_count",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadThroughput_max",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadThroughput_min",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReadThroughput_sum",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReplicationSlotDiskUsage_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReplicationSlotDiskUsage_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReplicationSlotDiskUsage_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_ReplicationSlotDiskUsage_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_SwapUsage_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_SwapUsage_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_SwapUsage_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_SwapUsage_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsDiskUsage_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsDiskUsage_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsDiskUsage_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsDiskUsage_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsGeneration_count",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsGeneration_max",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsGeneration_min",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_TransactionLogsGeneration_sum",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteIOPS_count",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteIOPS_max",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteIOPS_min",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteIOPS_sum",
"unit": "Count/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteLatency_count",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteLatency_max",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteLatency_min",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteLatency_sum",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteThroughput_count",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteThroughput_max",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteThroughput_min",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_RDS_WriteThroughput_sum",
"unit": "Bytes/Second",
"type": "Gauge",
"description": ""
"unit": "number",
"description": "CloudWatch metric CPUUtilization"
}
],
"logs": [
{
"name": "Account Id",
"path": "resources.cloud.account.id",
"type": "string"
},
{
"name": "Log Group Name",
"path": "resources.aws.cloudwatch.log_group_name",
"type": "string"
},
{
"name": "Log Stream Name",
"path": "resources.aws.cloudwatch.log_stream_name",
"type": "string"
}
]
"logs": []
},
"telemetry_collection_strategy": {
"aws_metrics": {
@@ -788,16 +43,5 @@
}
]
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "RDS Overview",
"description": "Overview of RDS",
"image": "file://assets/dashboards/overview.png",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -24,6 +24,7 @@ import (
_ "github.com/mattn/go-sqlite3"
"github.com/prometheus/prometheus/promql"
"go.signoz.io/signoz/pkg/http/render"
"go.signoz.io/signoz/pkg/query-service/agentConf"
"go.signoz.io/signoz/pkg/query-service/app/cloudintegrations"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
@@ -49,6 +50,8 @@ import (
"go.signoz.io/signoz/pkg/query-service/contextlinks"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/postprocess"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
"go.uber.org/zap"
@@ -126,6 +129,7 @@ type APIHandler struct {
jobsRepo *inframetrics.JobsRepo
pvcsRepo *inframetrics.PvcsRepo
SigNoz *signoz.SigNoz
}
type APIHandlerOpts struct {
@@ -165,6 +169,8 @@ type APIHandlerOpts struct {
UseLogsNewSchema bool
UseTraceNewSchema bool
SigNoz *signoz.SigNoz
}
// NewAPIHandler returns an APIHandler
@@ -237,6 +243,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
statefulsetsRepo: statefulsetsRepo,
jobsRepo: jobsRepo,
pvcsRepo: pvcsRepo,
SigNoz: opts.SigNoz,
}
logsQueryBuilder := logsv3.PrepareLogsQuery
@@ -1026,16 +1033,8 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
installedIntegrationDashboards, err := ic.GetDashboardsForInstalledIntegrations(r.Context())
if err != nil {
zap.L().Error("failed to get dashboards for installed integrations", zap.Error(err))
} else {
allDashboards = append(allDashboards, installedIntegrationDashboards...)
}
cloudIntegrationDashboards, err := aH.CloudIntegrationsController.AvailableDashboards(r.Context())
if err != nil {
zap.L().Error("failed to get cloud dashboards", zap.Error(err))
} else {
allDashboards = append(allDashboards, cloudIntegrationDashboards...)
}
allDashboards = append(allDashboards, installedIntegrationDashboards...)
tagsFromReq, ok := r.URL.Query()["tags"]
if !ok || len(tagsFromReq) == 0 || tagsFromReq[0] == "" {
@@ -1191,24 +1190,12 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) {
return
}
if aH.CloudIntegrationsController.IsCloudIntegrationDashboardUuid(uuid) {
dashboard, apiError = aH.CloudIntegrationsController.GetDashboardById(
r.Context(), uuid,
)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
} else {
dashboard, apiError = aH.IntegrationsController.GetInstalledIntegrationDashboardById(
r.Context(), uuid,
)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
dashboard, apiError = aH.IntegrationsController.GetInstalledIntegrationDashboardById(
r.Context(), uuid,
)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
}
@@ -1330,36 +1317,74 @@ func (aH *APIHandler) editRule(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) getChannel(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
channel, apiErrorObj := aH.ruleManager.RuleDB().GetChannel(id)
if apiErrorObj != nil {
RespondError(w, apiErrorObj, nil)
idVar := mux.Vars(r)["id"]
id, err := strconv.ParseUint(idVar, 10, 64)
if err != nil {
render.Error(w, err)
return
}
orgId, err := auth.GetOrgIdFromJwt(r.Context())
if err != nil {
render.Error(w, err)
return
}
channel, err := aH.SigNoz.AlertmanagerClient.GetChannel(r.Context(), orgId, id)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, channel)
}
func (aH *APIHandler) deleteChannel(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
apiErrorObj := aH.ruleManager.RuleDB().DeleteChannel(id)
if apiErrorObj != nil {
RespondError(w, apiErrorObj, nil)
idVar := mux.Vars(r)["id"]
id, err := strconv.ParseUint(idVar, 10, 64)
if err != nil {
render.Error(w, err)
return
}
orgId, err := auth.GetOrgIdFromJwt(r.Context())
if err != nil {
render.Error(w, err)
return
}
err = aH.SigNoz.AlertmanagerClient.DelChannel(r.Context(), orgId, id)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, "notification channel successfully deleted")
}
func (aH *APIHandler) listChannels(w http.ResponseWriter, r *http.Request) {
channels, apiErrorObj := aH.ruleManager.RuleDB().GetChannels()
if apiErrorObj != nil {
RespondError(w, apiErrorObj, nil)
orgId, err := auth.GetOrgIdFromJwt(r.Context())
if err != nil {
render.Error(w, err)
return
}
channels, err := aH.SigNoz.AlertmanagerClient.ListChannels(r.Context(), orgId)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, channels)
}
// testChannels sends test alert to all registered channels
func (aH *APIHandler) testChannel(w http.ResponseWriter, r *http.Request) {
orgId, err := auth.GetOrgIdFromJwt(r.Context())
if err != nil {
render.Error(w, err)
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
@@ -1369,24 +1394,34 @@ func (aH *APIHandler) testChannel(w http.ResponseWriter, r *http.Request) {
return
}
receiver := &am.Receiver{}
if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer
zap.L().Error("Error in parsing req body of testChannel API\n", zap.Error(err))
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
receiver, err := alertmanagertypes.NewReceiverFromString(string(body))
if err != nil {
render.Error(w, err)
return
}
// send alert
apiErrorObj := aH.alertManager.TestReceiver(receiver)
if apiErrorObj != nil {
RespondError(w, apiErrorObj, nil)
err = aH.SigNoz.AlertmanagerClient.TestReceiver(r.Context(), orgId, receiver)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, "test alert sent")
}
func (aH *APIHandler) editChannel(w http.ResponseWriter, r *http.Request) {
idVar := mux.Vars(r)["id"]
id, err := strconv.ParseUint(idVar, 10, 64)
if err != nil {
render.Error(w, err)
return
}
id := mux.Vars(r)["id"]
orgId, err := auth.GetOrgIdFromJwt(r.Context())
if err != nil {
render.Error(w, err)
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
@@ -1396,17 +1431,9 @@ func (aH *APIHandler) editChannel(w http.ResponseWriter, r *http.Request) {
return
}
receiver := &am.Receiver{}
if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer
zap.L().Error("Error in parsing req body of editChannel API", zap.Error(err))
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
_, apiErrorObj := aH.ruleManager.RuleDB().EditChannel(receiver, id)
if apiErrorObj != nil {
RespondError(w, apiErrorObj, nil)
err = aH.SigNoz.AlertmanagerClient.UpdateChannel(r.Context(), orgId, id, string(body))
if err != nil {
render.Error(w, err)
return
}
@@ -1415,6 +1442,11 @@ func (aH *APIHandler) editChannel(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) createChannel(w http.ResponseWriter, r *http.Request) {
orgId, err := auth.GetOrgIdFromJwt(r.Context())
if err != nil {
render.Error(w, err)
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
@@ -1424,41 +1456,41 @@ func (aH *APIHandler) createChannel(w http.ResponseWriter, r *http.Request) {
return
}
receiver := &am.Receiver{}
if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer
zap.L().Error("Error in parsing req body of createChannel API", zap.Error(err))
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
channel, err := alertmanagertypes.NewChannelFromReceiverString(string(body), orgId)
if err != nil {
render.Error(w, err)
return
}
_, apiErrorObj := aH.ruleManager.RuleDB().CreateChannel(receiver)
if apiErrorObj != nil {
RespondError(w, apiErrorObj, nil)
err = aH.SigNoz.AlertmanagerClient.CreateChannel(r.Context(), orgId, channel)
if err != nil {
render.Error(w, err)
return
}
aH.Respond(w, nil)
}
func (aH *APIHandler) getAlerts(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
amEndpoint := constants.GetAlertManagerApiPrefix()
resp, err := http.Get(amEndpoint + "v1/alerts" + "?" + params.Encode())
orgId, err := auth.GetOrgIdFromJwt(r.Context())
if err != nil {
render.Error(w, err)
return
}
params, err := alertmanagertypes.NewGettableAlertsParams(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
alerts, err := aH.SigNoz.AlertmanagerClient.GetAlerts(r.Context(), orgId, params)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, string(body))
aH.Respond(w, alerts)
}
func (aH *APIHandler) createRule(w http.ResponseWriter, r *http.Request) {

View File

@@ -231,16 +231,6 @@ func readFileIfUri(fs embed.FS, maybeFileUri string, basedir string) (interface{
dataUri := fmt.Sprintf("data:image/svg+xml;base64,%s", base64Svg)
return dataUri, nil
} else if strings.HasSuffix(maybeFileUri, ".jpeg") || strings.HasSuffix(maybeFileUri, ".jpg") {
base64Contents := base64.StdEncoding.EncodeToString(fileContents)
dataUri := fmt.Sprintf("data:image/jpeg;base64,%s", base64Contents)
return dataUri, nil
} else if strings.HasSuffix(maybeFileUri, ".png") {
base64Contents := base64.StdEncoding.EncodeToString(fileContents)
dataUri := fmt.Sprintf("data:image/png;base64,%s", base64Contents)
return dataUri, nil
}
return nil, fmt.Errorf("unsupported file type %s", maybeFileUri)

View File

@@ -200,6 +200,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FluxInterval: fluxInterval,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
SigNoz: serverOptions.SigNoz,
})
if err != nil {
return nil, err

View File

@@ -132,3 +132,17 @@ func GetEmailFromJwt(ctx context.Context) (string, error) {
return claims["email"].(string), nil
}
func GetOrgIdFromJwt(ctx context.Context) (string, error) {
jwt, ok := ExtractJwtFromContext(ctx)
if !ok {
return "", model.InternalError(fmt.Errorf("failed to extract jwt from context"))
}
claims, err := ParseJWT(jwt)
if err != nil {
return "", model.InternalError(fmt.Errorf("failed get claims from jwt %v", err))
}
return claims["orgId"].(string), nil
}

Some files were not shown because too many files have changed in this diff Show More