mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-19 23:10:25 +01:00
Compare commits
11 Commits
v0.73.0-cl
...
alertmanag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05e279bcd | ||
|
|
f1c5d873f7 | ||
|
|
aec239cc7c | ||
|
|
59e26652dc | ||
|
|
e02afc5e97 | ||
|
|
3eac8ac30b | ||
|
|
382c4f58e1 | ||
|
|
73ea632a3f | ||
|
|
00fa8810c0 | ||
|
|
6cee330d44 | ||
|
|
871c6e642c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -76,3 +76,5 @@ dist/
|
||||
|
||||
# ignore user_scripts that is fetched by init-clickhouse
|
||||
deploy/common/clickhouse/user_scripts/
|
||||
# queries.active
|
||||
queries.active
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -27,12 +27,6 @@ jest.mock('uplot', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
let mockWindowOpen: jest.Mock;
|
||||
|
||||
window.ResizeObserver =
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface NavigateToTraceProps {
|
||||
maxTime: number;
|
||||
selectedTraceTags: string;
|
||||
apmToTraceQuery: Query;
|
||||
safeNavigate: (path: string) => void;
|
||||
}
|
||||
|
||||
export interface DatabaseCallsRPSProps extends DatabaseCallProps {
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' : ''
|
||||
|
||||
@@ -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.." />;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -47,5 +47,4 @@ export interface IDashboardContext {
|
||||
setDashboardQueryRangeCalled: (value: boolean) => void;
|
||||
selectedRowWidgetId: string | null;
|
||||
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
isDashboardFetching: boolean;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
17
pkg/alertmanager/alertmanagerstore/config.go
Normal file
17
pkg/alertmanager/alertmanagerstore/config.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
44
pkg/alertmanager/alertmanagerstore/store.go
Normal file
44
pkg/alertmanager/alertmanagerstore/store.go
Normal 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)
|
||||
}
|
||||
36
pkg/alertmanager/client.go
Normal file
36
pkg/alertmanager/client.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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()))
|
||||
}
|
||||
84
pkg/alertmanager/server_test.go
Normal file
84
pkg/alertmanager/server_test.go
Normal 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
214
pkg/alertmanager/servers.go
Normal 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,
|
||||
})
|
||||
}
|
||||
10
pkg/factory/factorytest/settings.go
Normal file
10
pkg/factory/factorytest/settings.go
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user