Compare commits

..

25 Commits

Author SHA1 Message Date
grandwizard28
057dcbe6e4 fix: remove unused files 2026-04-16 02:26:18 +05:30
grandwizard28
3a28d741a3 fix: remove unused files 2026-04-16 02:24:20 +05:30
grandwizard28
223e83154f style: formatting and test cleanup from review
Restructure Validate nil check, rename expectErr to fail with
early-return, trim trailing newlines in test assertions, remove
t.Parallel from subtests, inline short config literals, restore
struct field comments in web.Config.
2026-04-16 02:17:14 +05:30
grandwizard28
50ae51cdaa fix(web): resolve lint errors in provider and template
Fix errcheck on rw.Write in serveIndex, use ErrorContext instead of
Error in NewIndex for sloglint compliance. Move serveIndex below
ServeHTTP to order public methods before private ones.
2026-04-16 02:05:25 +05:30
grandwizard28
c8ae8476c3 style: add blank lines between logical blocks 2026-04-16 01:57:24 +05:30
grandwizard28
daaa66e1fc chore: remove redundant comments from added code 2026-04-16 01:54:14 +05:30
grandwizard28
b0717d6a69 refactor(web): use table-driven tests with named path cases
Replace for-loop path iteration with explicit table-driven test cases
for each path. Each path (root, non-existent, directory) is a named
subtest case in all three template tests.
2026-04-16 01:49:07 +05:30
grandwizard28
4aefe44313 refactor(web): rename get test helper to httpGet 2026-04-16 01:47:35 +05:30
grandwizard28
4dc6f6fe7b style(web): use raw string literals for expected test values 2026-04-16 01:44:46 +05:30
grandwizard28
d3e0c46ba2 test(web): use exact match instead of contains in template tests
Match the full expected response body in TestServeTemplatedIndex
instead of using assert.Contains.
2026-04-16 01:43:23 +05:30
grandwizard28
0fed17e11a test(web): add SPA fallback paths to no_template and invalid_template tests
Test /, /does-not-exist, and /assets in all three template test cases
to verify SPA fallback behavior (non-existent paths and directories
serve the index) regardless of template type.
2026-04-16 01:38:46 +05:30
grandwizard28
a2264b4960 refactor(web): rename test fixtures to no_template, valid_template, invalid_template
Drop the index_ prefix from test fixtures. Use web instead of w for
the variable name in test helpers.
2026-04-16 01:32:50 +05:30
grandwizard28
2740964106 test(web): add no-template and invalid-template index test cases
Add three distinct index fixtures in testdata:
- index.html: correct [[ ]] template with BaseHref
- index_no_template.html: plain HTML, no placeholders
- index_invalid_template.html: malformed template syntax

Tests verify: template substitution works, plain files pass through
unchanged, and invalid templates fall back to serving raw bytes.
Consolidate test helpers into startServer/get.
2026-04-16 01:28:37 +05:30
grandwizard28
0ca22dd7fe refactor(web): collapse testdata_basepath into testdata
Use a single testdata directory with a templated index.html for all
routerweb tests. Remove the redundant testdata_basepath directory.
2026-04-16 01:22:54 +05:30
grandwizard28
a3b6bddac8 refactor(web): make index filename configurable via web.index
Move the hardcoded indexFileName const from routerweb/provider.go to
web.Config.Index with default "index.html". This allows overriding the
SPA entrypoint file via configuration.
2026-04-16 01:19:35 +05:30
grandwizard28
d908ce321a refactor(global): rename RoutePrefix to ExternalPath, add ExternalPathTrailing
Rename RoutePrefix() to ExternalPath() to accurately reflect what it
returns: the path component of the external URL. Add
ExternalPathTrailing() which returns the path with a trailing slash,
used for HTML base href injection.
2026-04-16 01:13:16 +05:30
grandwizard28
c221a44f3d refactor(web): extract index.html templating into web.NewIndex
Move the template parsing and execution logic from routerweb provider
into pkg/web/template.go. NewIndex logs and returns raw bytes on
template failure; NewIndexE returns the error for callers that need it.

Rename BasePath to BaseHref to match the HTML attribute it populates.
Inject global.Config into routerweb via the factory closure pattern.
2026-04-16 01:08:46 +05:30
grandwizard28
22fb4daaf9 feat(web): template index.html with dynamic base href from global.external_url
Read index.html at startup, parse as Go template with [[ ]] delimiters,
execute with BasePath derived from global.external_url, and cache the
rendered bytes in memory. This injects <base href="/signoz/" /> (or
whatever the route prefix is) so the browser resolves relative URLs
correctly when SigNoz is served at a sub-path.

Inject global.Config into the routerweb provider via the factory closure
pattern. Static files (JS, CSS, images) are still served from disk
unchanged.
2026-04-16 00:58:20 +05:30
grandwizard28
1bdc059d76 feat(apiserver): derive HTTP route prefix from global.external_url
The path component of global.external_url is now used as the base path
for all HTTP routes (API and web frontend), enabling SigNoz to be served
behind a reverse proxy at a sub-path (e.g. https://example.com/signoz/).

The prefix is applied via http.StripPrefix at the outermost handler
level, requiring zero changes to route registration code. Health
endpoints (/api/v1/health, /api/v2/healthz, /api/v2/readyz,
/api/v2/livez) remain accessible without the prefix for container
healthchecks.

Removes web.prefix config in favor of the unified global.external_url
approach, avoiding the desync bugs seen in projects with separate
API/UI prefix configs (ArgoCD, Prometheus).

closes SigNoz/platform-pod#1775
2026-04-16 00:38:55 +05:30
Pandey
b3da6fb251 refactor(alertmanager): move API handlers to signozapiserver (#10941)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* refactor(alertmanager): move API handlers to signozapiserver

Extract Handler interface in pkg/alertmanager/handler.go and move
the implementation from api.go to signozalertmanager/handler.go.

Register all alertmanager routes (channels, route policies, alerts)
in signozapiserver via handler.New() with OpenAPIDef. Remove
AlertmanagerAPI injection from http_handler.go.

This enables future AuditDef instrumentation on these routes.

* fix(review): rename param, add /api/v1/channels/test endpoint

- Rename `am` to `alertmanagerService` in NewHandlers
- Add /api/v1/channels/test as the canonical test endpoint
- Mark /api/v1/testChannel as deprecated
- Regenerate OpenAPI spec

* fix(review): use camelCase for channel orgId json tag

* fix(review): remove section comments from alertmanager routes

* fix(review): use routepolicies tag without hyphen

* chore: regenerate frontend API clients for alertmanager routes

* fix: add required/nullable/enum tags to alertmanager OpenAPI types

- PostableRoutePolicy: mark expression, name, channels as required
- GettableRoutePolicy: change CreatedAt/UpdatedAt from pointer to value
- Channel: mark name, type, data, orgId as required
- ExpressionKind: add Enum() for rule/policy values
- Regenerate OpenAPI spec and frontend clients

* fix: use typed response for GetAlerts endpoint

* fix: add Receiver request type to channel mutation endpoints

CreateChannel, UpdateChannelByID, TestChannel, and TestChannelDeprecated
all read req.Body as a Receiver config. The OpenAPIDef must declare
the request type so the generated SDK includes the body parameter.

* fix: change CreateChannel access from EditAccess to AdminAccess

Aligns CreateChannel endpoint with the rest of the channel mutation
endpoints (update/delete) which all require admin access. This is
consistent with the frontend where notifications are not accessible
to editors.
2026-04-15 17:31:43 +00:00
primus-bot[bot]
be1a0fa3a5 chore(release): bump to v0.119.0 (#10936)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
Co-authored-by: Priyanshu Shrivastava <priyanshu@signoz.io>
2026-04-15 08:35:52 +00:00
Nikhil Mantri
6ad2711c7a feat(/fields/*) : introduce a new param metricNamespace in the APIs for prefix match (#10779)
* chore: initial commit

* chore: added metricNamespace as a new param

* chore: go generate openapi, update spec

* chore: frontend yarn generate:api

* chore: added metricnamespace support in /fields/values as well as added integration tests

* chore: corrected comment

* chore: added unit tests for getMetricsKeys and getMeterSourceMetricKeys

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-04-15 07:26:42 +00:00
swapnil-signoz
4f59cb0de3 chore: updating cloud integration agent version (#10933)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-04-15 05:01:26 +00:00
Srikanth Chekuri
304c39e08c fix(route-policy): allow undefined variables for expression (#10934)
* fix(route-policy): allow undefined variables for expression

* chore: fix lint
2026-04-15 04:55:21 +00:00
swapnil-signoz
3df0da3a4e chore: removing underscore attribute based cloud integration dashboards (#10929)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-04-14 13:08:07 +00:00
149 changed files with 6962 additions and 39241 deletions

View File

@@ -75,7 +75,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
return signoz.NewSQLSchemaProviderFactories(sqlstore)
},

View File

@@ -96,7 +96,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {

View File

@@ -6,6 +6,8 @@
##################### Global #####################
global:
# the url under which the signoz apiserver is externally reachable.
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
@@ -50,8 +52,8 @@ pprof:
web:
# Whether to enable the web frontend
enabled: true
# The prefix to serve web on
prefix: /
# The index file to use as the SPA entrypoint.
index: index.html
# The directory containing the static build files.
directory: /etc/signoz/web

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.118.0
image: signoz/signoz:v0.119.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.118.0
image: signoz/signoz:v0.119.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.118.0}
image: signoz/signoz:${VERSION:-v0.119.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.118.0}
image: signoz/signoz:${VERSION:-v0.119.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
@@ -49,7 +48,6 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),

View File

@@ -262,6 +262,20 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
return nil, err
}
routePrefix := s.config.Global.ExternalPath()
if routePrefix != "" {
prefixed := http.StripPrefix(routePrefix, handler)
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
r.ServeHTTP(w, req)
return
}
prefixed.ServeHTTP(w, req)
})
}
return &http.Server{
Handler: handler,
}, nil

View File

@@ -277,23 +277,8 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
annotations := make(ruletypes.Labels, 0, len(r.Annotations().Map()))
for name, value := range r.Annotations().Map() {
// no need to expand custom templating annotations — they get expanded in the notifier layer
if ruletypes.IsCustomTemplatingAnnotation(name) {
annotations = append(annotations, ruletypes.Label{Name: name, Value: value})
continue
}
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
}
// Add values to be used in notifier layer for notification templates
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationValue, Value: value})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationThresholdValue, Value: threshold})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationCompareOp, Value: smpl.CompareOperator.Literal()})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationMatchType, Value: smpl.MatchType.Literal()})
if smpl.IsRecovering {
lb.Set(ruletypes.LabelIsRecovering, "true")
}
if smpl.IsMissing {
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
lb.Set(ruletypes.NoDataLabel, "true")

View File

@@ -0,0 +1,97 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { GetAlerts200, RenderErrorResponseDTO } from '../sigNoz.schemas';
/**
* This endpoint returns alerts for the organization
* @summary Get alerts
*/
export const getAlerts = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetAlerts200>({
url: `/api/v1/alerts`,
method: 'GET',
signal,
});
};
export const getGetAlertsQueryKey = () => {
return [`/api/v1/alerts`] as const;
};
export const getGetAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getAlerts>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAlertsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAlerts>>> = ({
signal,
}) => getAlerts(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAlerts>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAlertsQueryResult = NonNullable<
Awaited<ReturnType<typeof getAlerts>>
>;
export type GetAlertsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get alerts
*/
export function useGetAlerts<
TData = Awaited<ReturnType<typeof getAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getAlerts>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAlertsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get alerts
*/
export const invalidateGetAlerts = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAlertsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -0,0 +1,646 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
ConfigReceiverDTO,
CreateChannel201,
DeleteChannelByIDPathParameters,
GetChannelByID200,
GetChannelByIDPathParameters,
ListChannels200,
RenderErrorResponseDTO,
UpdateChannelByIDPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all notification channels for the organization
* @summary List notification channels
*/
export const listChannels = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListChannels200>({
url: `/api/v1/channels`,
method: 'GET',
signal,
});
};
export const getListChannelsQueryKey = () => {
return [`/api/v1/channels`] as const;
};
export const getListChannelsQueryOptions = <
TData = Awaited<ReturnType<typeof listChannels>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListChannelsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listChannels>>> = ({
signal,
}) => listChannels(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListChannelsQueryResult = NonNullable<
Awaited<ReturnType<typeof listChannels>>
>;
export type ListChannelsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List notification channels
*/
export function useListChannels<
TData = Awaited<ReturnType<typeof listChannels>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListChannelsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List notification channels
*/
export const invalidateListChannels = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListChannelsQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a notification channel
* @summary Create notification channel
*/
export const createChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateChannel201>({
url: `/api/v1/channels`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getCreateChannelMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['createChannel'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return createChannel(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof createChannel>>
>;
export type CreateChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type CreateChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create notification channel
*/
export const useCreateChannel = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getCreateChannelMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a notification channel by ID
* @summary Delete notification channel
*/
export const deleteChannelByID = ({ id }: DeleteChannelByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'DELETE',
});
};
export const getDeleteChannelByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteChannelByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteChannelByID>>,
{ pathParams: DeleteChannelByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteChannelByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteChannelByID>>
>;
export type DeleteChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete notification channel
*/
export const useDeleteChannelByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteChannelByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns a notification channel by ID
* @summary Get notification channel by ID
*/
export const getChannelByID = (
{ id }: GetChannelByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetChannelByID200>({
url: `/api/v1/channels/${id}`,
method: 'GET',
signal,
});
};
export const getGetChannelByIDQueryKey = ({
id,
}: GetChannelByIDPathParameters) => {
return [`/api/v1/channels/${id}`] as const;
};
export const getGetChannelByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getChannelByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetChannelByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetChannelByIDQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getChannelByID>>> = ({
signal,
}) => getChannelByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetChannelByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getChannelByID>>
>;
export type GetChannelByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get notification channel by ID
*/
export function useGetChannelByID<
TData = Awaited<ReturnType<typeof getChannelByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetChannelByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetChannelByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get notification channel by ID
*/
export const invalidateGetChannelByID = async (
queryClient: QueryClient,
{ id }: GetChannelByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetChannelByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a notification channel by ID
* @summary Update notification channel
*/
export const updateChannelByID = (
{ id }: UpdateChannelByIDPathParameters,
configReceiverDTO: BodyType<ConfigReceiverDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
});
};
export const getUpdateChannelByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
const mutationKey = ['updateChannelByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateChannelByID>>,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateChannelByID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateChannelByID>>
>;
export type UpdateChannelByIDMutationBody = BodyType<ConfigReceiverDTO>;
export type UpdateChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update notification channel
*/
export const useUpdateChannelByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
const mutationOptions = getUpdateChannelByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint tests a notification channel by sending a test notification
* @summary Test notification channel
*/
export const testChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getTestChannelMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannel'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return testChannel(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannel>>
>;
export type TestChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type TestChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test notification channel
*/
export const useTestChannel = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getTestChannelMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Deprecated: use /api/v1/channels/test instead
* @deprecated
* @summary Test notification channel (deprecated)
*/
export const testChannelDeprecated = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/testChannel`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getTestChannelDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannelDeprecated'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannelDeprecated>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return testChannelDeprecated(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestChannelDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannelDeprecated>>
>;
export type TestChannelDeprecatedMutationBody = BodyType<ConfigReceiverDTO>;
export type TestChannelDeprecatedMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Test notification channel (deprecated)
*/
export const useTestChannelDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getTestChannelDeprecatedMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -0,0 +1,482 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AlertmanagertypesPostableRoutePolicyDTO,
CreateRoutePolicy201,
DeleteRoutePolicyByIDPathParameters,
GetAllRoutePolicies200,
GetRoutePolicyByID200,
GetRoutePolicyByIDPathParameters,
RenderErrorResponseDTO,
UpdateRoutePolicy200,
UpdateRoutePolicyPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all route policies for the organization
* @summary List route policies
*/
export const getAllRoutePolicies = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetAllRoutePolicies200>({
url: `/api/v1/route_policies`,
method: 'GET',
signal,
});
};
export const getGetAllRoutePoliciesQueryKey = () => {
return [`/api/v1/route_policies`] as const;
};
export const getGetAllRoutePoliciesQueryOptions = <
TData = Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAllRoutePoliciesQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getAllRoutePolicies>>
> = ({ signal }) => getAllRoutePolicies(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAllRoutePoliciesQueryResult = NonNullable<
Awaited<ReturnType<typeof getAllRoutePolicies>>
>;
export type GetAllRoutePoliciesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List route policies
*/
export function useGetAllRoutePolicies<
TData = Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAllRoutePoliciesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List route policies
*/
export const invalidateGetAllRoutePolicies = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAllRoutePoliciesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a route policy
* @summary Create route policy
*/
export const createRoutePolicy = (
alertmanagertypesPostableRoutePolicyDTO: BodyType<AlertmanagertypesPostableRoutePolicyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRoutePolicy201>({
url: `/api/v1/route_policies`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostableRoutePolicyDTO,
signal,
});
};
export const getCreateRoutePolicyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
> => {
const mutationKey = ['createRoutePolicy'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRoutePolicy>>,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> }
> = (props) => {
const { data } = props ?? {};
return createRoutePolicy(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateRoutePolicyMutationResult = NonNullable<
Awaited<ReturnType<typeof createRoutePolicy>>
>;
export type CreateRoutePolicyMutationBody = BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
export type CreateRoutePolicyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create route policy
*/
export const useCreateRoutePolicy = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
> => {
const mutationOptions = getCreateRoutePolicyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a route policy by ID
* @summary Delete route policy
*/
export const deleteRoutePolicyByID = ({
id,
}: DeleteRoutePolicyByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/route_policies/${id}`,
method: 'DELETE',
});
};
export const getDeleteRoutePolicyByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteRoutePolicyByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
{ pathParams: DeleteRoutePolicyByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteRoutePolicyByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteRoutePolicyByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>
>;
export type DeleteRoutePolicyByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete route policy
*/
export const useDeleteRoutePolicyByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteRoutePolicyByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns a route policy by ID
* @summary Get route policy by ID
*/
export const getRoutePolicyByID = (
{ id }: GetRoutePolicyByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRoutePolicyByID200>({
url: `/api/v1/route_policies/${id}`,
method: 'GET',
signal,
});
};
export const getGetRoutePolicyByIDQueryKey = ({
id,
}: GetRoutePolicyByIDPathParameters) => {
return [`/api/v1/route_policies/${id}`] as const;
};
export const getGetRoutePolicyByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRoutePolicyByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRoutePolicyByIDQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRoutePolicyByID>>
> = ({ signal }) => getRoutePolicyByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRoutePolicyByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getRoutePolicyByID>>
>;
export type GetRoutePolicyByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get route policy by ID
*/
export function useGetRoutePolicyByID<
TData = Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRoutePolicyByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRoutePolicyByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get route policy by ID
*/
export const invalidateGetRoutePolicyByID = async (
queryClient: QueryClient,
{ id }: GetRoutePolicyByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRoutePolicyByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a route policy by ID
* @summary Update route policy
*/
export const updateRoutePolicy = (
{ id }: UpdateRoutePolicyPathParameters,
alertmanagertypesPostableRoutePolicyDTO: BodyType<AlertmanagertypesPostableRoutePolicyDTO>,
) => {
return GeneratedAPIInstance<UpdateRoutePolicy200>({
url: `/api/v1/route_policies/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostableRoutePolicyDTO,
});
};
export const getUpdateRoutePolicyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
> => {
const mutationKey = ['updateRoutePolicy'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateRoutePolicy>>,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRoutePolicy(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRoutePolicyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRoutePolicy>>
>;
export type UpdateRoutePolicyMutationBody = BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
export type UpdateRoutePolicyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update route policy
*/
export const useUpdateRoutePolicy = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
> => {
const mutationOptions = getUpdateRoutePolicyMutationOptions(options);
return useMutation(mutationOptions);
};

File diff suppressed because it is too large Load Diff

5
go.mod
View File

@@ -15,7 +15,6 @@ require (
github.com/coreos/go-oidc/v3 v3.17.0
github.com/dgraph-io/ristretto/v2 v2.3.0
github.com/dustin/go-humanize v1.0.1
github.com/emersion/go-smtp v0.24.0
github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron v1.30.1
github.com/go-openapi/runtime v0.29.2
@@ -64,7 +63,6 @@ require (
github.com/uptrace/bun/dialect/pgdialect v1.2.9
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
github.com/uptrace/bun/extra/bunotel v1.2.9
github.com/yuin/goldmark v1.7.16
go.opentelemetry.io/collector/confmap v1.51.0
go.opentelemetry.io/collector/otelcol v0.144.0
go.opentelemetry.io/collector/pdata v1.51.0
@@ -113,7 +111,6 @@ require (
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
@@ -167,7 +164,7 @@ require (
github.com/ClickHouse/ch-go v0.67.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/Yiling-J/theine-go v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/beevik/etree v1.1.0 // indirect

2
go.sum
View File

@@ -1144,8 +1144,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=

View File

@@ -1,536 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package email
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"log/slog"
"math/rand"
"mime"
"mime/multipart"
"mime/quotedprintable"
"net"
"net/mail"
"net/smtp"
"net/textproto"
"os"
"strings"
"sync"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
const (
Integration = "email"
)
// Email implements a Notifier for email notifications.
type Email struct {
conf *config.EmailConfig
tmpl *template.Template
logger *slog.Logger
hostname string
processor alertmanagertypes.NotificationProcessor
}
var errNoAuthUsernameConfigured = errors.NewInternalf(errors.CodeInternal, "no auth username configured")
// New returns a new Email notifier.
func New(c *config.EmailConfig, t *template.Template, l *slog.Logger, proc alertmanagertypes.NotificationProcessor) *Email {
if _, ok := c.Headers["Subject"]; !ok {
c.Headers["Subject"] = config.DefaultEmailSubject
}
if _, ok := c.Headers["To"]; !ok {
c.Headers["To"] = c.To
}
if _, ok := c.Headers["From"]; !ok {
c.Headers["From"] = c.From
}
h, err := os.Hostname()
// If we can't get the hostname, we'll use localhost
if err != nil {
h = "localhost.localdomain"
}
return &Email{conf: c, tmpl: t, logger: l, hostname: h, processor: proc}
}
// auth resolves a string of authentication mechanisms.
func (n *Email) auth(mechs string) (smtp.Auth, error) {
username := n.conf.AuthUsername
// If no username is set, return custom error which can be ignored if needed.
if strings.TrimSpace(username) == "" {
return nil, errNoAuthUsernameConfigured
}
var errs error
for mech := range strings.SplitSeq(mechs, " ") {
switch mech {
case "CRAM-MD5":
secret, secretErr := n.getAuthSecret()
if secretErr != nil {
errs = errors.Join(errs, secretErr)
continue
}
if secret == "" {
errs = errors.Join(errs, errors.NewInternalf(errors.CodeInternal, "missing secret for CRAM-MD5 auth mechanism"))
continue
}
return smtp.CRAMMD5Auth(username, secret), nil
case "PLAIN":
password, passwordErr := n.getPassword()
if passwordErr != nil {
errs = errors.Join(errs, passwordErr)
continue
}
if password == "" {
errs = errors.Join(errs, errors.NewInternalf(errors.CodeInternal, "missing password for PLAIN auth mechanism"))
continue
}
return smtp.PlainAuth(n.conf.AuthIdentity, username, password, n.conf.Smarthost.Host), nil
case "LOGIN":
password, passwordErr := n.getPassword()
if passwordErr != nil {
errs = errors.Join(errs, passwordErr)
continue
}
if password == "" {
errs = errors.Join(errs, errors.NewInternalf(errors.CodeInternal, "missing password for LOGIN auth mechanism"))
continue
}
return LoginAuth(username, password), nil
default:
errs = errors.Join(errs, errors.NewInternalf(errors.CodeUnsupported, "unknown auth mechanism: %s", mech))
}
}
return nil, errs
}
// Notify implements the Notifier interface.
func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
var (
c *smtp.Client
conn net.Conn
err error
success = false
)
// Determine whether to use Implicit TLS
var useImplicitTLS bool
if n.conf.ForceImplicitTLS != nil {
useImplicitTLS = *n.conf.ForceImplicitTLS
} else {
// Default logic: port 465 uses implicit TLS (backward compatibility)
useImplicitTLS = n.conf.Smarthost.Port == "465"
}
if useImplicitTLS {
tlsConfig, err := commoncfg.NewTLSConfig(n.conf.TLSConfig)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "parse TLS configuration")
}
if tlsConfig.ServerName == "" {
tlsConfig.ServerName = n.conf.Smarthost.Host
}
conn, err = tls.Dial("tcp", n.conf.Smarthost.String(), tlsConfig)
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "establish TLS connection to server")
}
} else {
var (
d = net.Dialer{}
err error
)
conn, err = d.DialContext(ctx, "tcp", n.conf.Smarthost.String())
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "establish connection to server")
}
}
c, err = smtp.NewClient(conn, n.conf.Smarthost.Host)
if err != nil {
conn.Close()
return true, errors.WrapInternalf(err, errors.CodeInternal, "create SMTP client")
}
defer func() {
// Try to clean up after ourselves but don't log anything if something has failed.
if err := c.Quit(); success && err != nil {
n.logger.WarnContext(ctx, "failed to close SMTP connection", slog.Any("err", err))
}
}()
if n.conf.Hello != "" {
err = c.Hello(n.conf.Hello)
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "send EHLO command")
}
}
// Global Config guarantees RequireTLS is not nil.
if *n.conf.RequireTLS && !useImplicitTLS {
if ok, _ := c.Extension("STARTTLS"); !ok {
return true, errors.WrapInternalf(err, errors.CodeInternal, "'require_tls' is true (default) but %q does not advertise the STARTTLS extension", n.conf.Smarthost)
}
tlsConf, err := commoncfg.NewTLSConfig(n.conf.TLSConfig)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "parse TLS configuration")
}
if tlsConf.ServerName == "" {
tlsConf.ServerName = n.conf.Smarthost.Host
}
if err := c.StartTLS(tlsConf); err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "send STARTTLS command")
}
}
if ok, mech := c.Extension("AUTH"); ok {
auth, err := n.auth(mech)
if err != nil && err != errNoAuthUsernameConfigured {
return true, errors.WrapInternalf(err, errors.CodeInternal, "find auth mechanism")
} else if err == errNoAuthUsernameConfigured {
n.logger.DebugContext(ctx, "no auth username configured. Attempting to send email without authenticating")
}
if auth != nil {
if err := c.Auth(auth); err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "%T auth", auth)
}
}
}
var (
tmplErr error
data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
tmpl = notify.TmplText(n.tmpl, data, &tmplErr)
)
from := tmpl(n.conf.From)
if tmplErr != nil {
return false, errors.WrapInternalf(tmplErr, errors.CodeInternal, "execute 'from' template")
}
to := tmpl(n.conf.To)
if tmplErr != nil {
return false, errors.WrapInternalf(tmplErr, errors.CodeInternal, "execute 'to' template")
}
addrs, err := mail.ParseAddressList(from)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "parse 'from' addresses")
}
if len(addrs) != 1 {
return false, errors.NewInternalf(errors.CodeInternal, "must be exactly one 'from' address (got: %d)", len(addrs))
}
if err = c.Mail(addrs[0].Address); err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "send MAIL command")
}
addrs, err = mail.ParseAddressList(to)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "parse 'to' addresses")
}
for _, addr := range addrs {
if err = c.Rcpt(addr.Address); err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "send RCPT command")
}
}
// Prepare the content for the email
title, htmlBody, err := n.prepareContent(ctx, as, data)
if err != nil {
n.logger.ErrorContext(ctx, "failed to prepare notification content", errors.Attr(err))
return false, err
}
if title != "" {
n.conf.Headers["Subject"] = title
}
// Send the email headers and body.
message, err := c.Data()
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "send DATA command")
}
closeOnce := sync.OnceValue(func() error {
return message.Close()
})
// Close the message when this method exits in order to not leak resources. Even though we're calling this explicitly
// further down, the method may exit before then.
defer func() {
// If we try close an already-closed writer, it'll send a subsequent request to the server which is invalid.
_ = closeOnce()
}()
buffer := &bytes.Buffer{}
for header, t := range n.conf.Headers {
value, err := n.tmpl.ExecuteTextString(t, data)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "execute %q header template", header)
}
fmt.Fprintf(buffer, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value))
}
if _, ok := n.conf.Headers["Message-Id"]; !ok {
fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), n.hostname))
}
if n.conf.Threading.Enabled {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
// Add threading headers. All notifications for the same alert group
// (identified by key hash) are threaded together.
threadBy := ""
if n.conf.Threading.ThreadByDate != "none" {
// ThreadByDate is 'daily':
// Use current date so all mails for this alert today thread together.
threadBy = time.Now().Format("2006-01-02")
}
keyHash := key.Hash()
if len(keyHash) > 16 {
keyHash = keyHash[:16]
}
// The thread root ID is a Message-ID that doesn't correspond to
// any actual email. Email clients following the (commonly used) JWZ
// algorithm will create a dummy container to group these messages.
threadRootID := fmt.Sprintf("<alert-%s-%s@alertmanager>", keyHash, threadBy)
fmt.Fprintf(buffer, "References: %s\r\n", threadRootID)
fmt.Fprintf(buffer, "In-Reply-To: %s\r\n", threadRootID)
}
multipartBuffer := &bytes.Buffer{}
multipartWriter := multipart.NewWriter(multipartBuffer)
fmt.Fprintf(buffer, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
fmt.Fprintf(buffer, "Content-Type: multipart/alternative; boundary=%s\r\n", multipartWriter.Boundary())
fmt.Fprintf(buffer, "MIME-Version: 1.0\r\n\r\n")
// TODO: Add some useful headers here, such as URL of the alertmanager
// and active/resolved.
_, err = message.Write(buffer.Bytes())
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "write headers")
}
if len(n.conf.Text) > 0 {
// Text template
w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
"Content-Transfer-Encoding": {"quoted-printable"},
"Content-Type": {"text/plain; charset=UTF-8"},
})
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "create part for text template")
}
body, err := n.tmpl.ExecuteTextString(n.conf.Text, data)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "execute text template")
}
qw := quotedprintable.NewWriter(w)
_, err = qw.Write([]byte(body))
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "write text part")
}
err = qw.Close()
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "close text part")
}
}
if htmlBody != "" {
// Html template
// Preferred alternative placed last per section 5.1.4 of RFC 2046
// https://www.ietf.org/rfc/rfc2046.txt
w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
"Content-Transfer-Encoding": {"quoted-printable"},
"Content-Type": {"text/html; charset=UTF-8"},
})
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "create part for html template")
}
qw := quotedprintable.NewWriter(w)
_, err = qw.Write([]byte(htmlBody))
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "write HTML part")
}
err = qw.Close()
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "close HTML part")
}
}
err = multipartWriter.Close()
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "close multipartWriter")
}
_, err = message.Write(multipartBuffer.Bytes())
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "write body buffer")
}
// Complete the message and await response.
if err = closeOnce(); err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "delivery failure")
}
success = true
return false, nil
}
// prepareContent extracts custom templates from alerts, runs the notification processor,
// and returns the resolved subject title (if any) and the HTML body for the email.
func (n *Email) prepareContent(ctx context.Context, alerts []*types.Alert, data *template.Data) (string, string, error) {
// run the notification processor to get the title and body
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(alerts)
result, err := n.processor.ProcessAlertNotification(ctx, alertmanagertypes.NotificationProcessorInput{
TitleTemplate: customTitle,
BodyTemplate: customBody,
DefaultTitleTemplate: n.conf.Headers["Subject"],
// no templating needed for email body as it will be handled with legacy templating
DefaultBodyTemplate: alertmanagertypes.NoOpTemplateString,
}, alerts, markdownrenderer.MarkdownFormatHTML)
if err != nil {
return "", "", err
}
title := result.Title
// If custom templated, render via the HTML layout template
if result.IsCustomTemplated() {
// Add buttons to each of the bodies if the related logs and traces links are present in annotations
for i := range result.Body {
relatedLogsLink := alerts[i].Annotations[ruletypes.AnnotationRelatedLogs]
relatedTracesLink := alerts[i].Annotations[ruletypes.AnnotationRelatedTraces]
if relatedLogsLink != "" {
result.Body[i] += htmlButton("View Related Logs", string(relatedLogsLink))
}
if relatedTracesLink != "" {
result.Body[i] += htmlButton("View Related Traces", string(relatedTracesLink))
}
}
htmlContent, renderErr := n.processor.RenderEmailNotification(ctx, emailtypes.TemplateNameAlertEmailNotification, result, alerts)
if renderErr == nil {
return title, htmlContent, nil
}
n.logger.WarnContext(ctx, "custom email template rendering failed, falling back to default", errors.Attr(renderErr))
}
// Default templated body: use the HTML config template if available
if len(n.conf.HTML) > 0 {
body, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data)
if err != nil {
return "", "", errors.WrapInternalf(err, errors.CodeInternal, "execute html template")
}
return title, body, nil
}
// No HTML template configured, fallback to plain HTML templating
if result.IsCustomTemplated() {
var b strings.Builder
for _, part := range result.Body {
b.WriteString("<div>")
b.WriteString(part)
b.WriteString("</div>")
}
return title, b.String(), nil
}
return title, "", nil
}
func htmlButton(text, url string) string {
return fmt.Sprintf(`
<a href="%s" target="_blank" style="text-decoration: none;">
<button style="
padding: 6px 16px;
/* Default System Font */
font-family: sans-serif;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
/* Light Theme & Dynamic Background (Solid) */
color: #111827;
background-color: #f9fafb;
/* Static Outline */
border: 1px solid #d1d5db;
border-radius: 4px;
cursor: pointer;
">
%s
</button>
</a>`, url, text)
}
type loginAuth struct {
username, password string
}
func LoginAuth(username, password string) smtp.Auth {
return &loginAuth{username, password}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", []byte{}, nil
}
// Used for AUTH LOGIN. (Maybe password should be encrypted).
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch strings.ToLower(string(fromServer)) {
case "username:":
return []byte(a.username), nil
case "password:":
return []byte(a.password), nil
default:
return nil, errors.NewInternalf(errors.CodeInternal, "unexpected server challenge")
}
}
return nil, nil
}
func (n *Email) getPassword() (string, error) {
if len(n.conf.AuthPasswordFile) > 0 {
content, err := os.ReadFile(n.conf.AuthPasswordFile)
if err != nil {
return "", errors.NewInternalf(errors.CodeInternal, "could not read %s: %v", n.conf.AuthPasswordFile, err)
}
return strings.TrimSpace(string(content)), nil
}
return string(n.conf.AuthPassword), nil
}
func (n *Email) getAuthSecret() (string, error) {
if len(n.conf.AuthSecretFile) > 0 {
content, err := os.ReadFile(n.conf.AuthSecretFile)
if err != nil {
return "", errors.NewInternalf(errors.CodeInternal, "could not read %s: %v", n.conf.AuthSecretFile, err)
}
return string(content), nil
}
return string(n.conf.AuthSecret), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
smarthost: 127.0.0.1:1026
server: http://127.0.0.1:1081/
username: user
password: pass

View File

@@ -1,4 +0,0 @@
smarthost: maildev-auth:1025
server: http://maildev-auth:1080/
username: user
password: pass

View File

@@ -1,2 +0,0 @@
smarthost: 127.0.0.1:1025
server: http://127.0.0.1:1080/

View File

@@ -1,2 +0,0 @@
smarthost: maildev-noauth:1025
server: http://maildev-noauth:1080/

View File

@@ -1,16 +1,3 @@
// Copyright 2024 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package msteamsv2
import (
@@ -24,10 +11,7 @@ import (
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -43,10 +27,6 @@ const (
colorGrey = "Warning"
)
const (
Integration = "msteamsv2"
)
type Notifier struct {
conf *config.MSTeamsV2Config
titleLink string
@@ -56,7 +36,6 @@ type Notifier struct {
retrier *notify.Retrier
webhookURL *config.SecretURL
postJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)
processor alertmanagertypes.NotificationProcessor
}
// https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1#adaptivecarditemschema
@@ -107,8 +86,8 @@ type teamsMessage struct {
}
// New returns a new notifier that uses the Microsoft Teams Power Platform connector.
func New(c *config.MSTeamsV2Config, t *template.Template, titleLink string, l *slog.Logger, proc alertmanagertypes.NotificationProcessor, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := notify.NewClientWithTracing(*c.HTTPConfig, Integration, httpOpts...)
func New(c *config.MSTeamsV2Config, t *template.Template, titleLink string, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "msteamsv2", httpOpts...)
if err != nil {
return nil, err
}
@@ -122,7 +101,6 @@ func New(c *config.MSTeamsV2Config, t *template.Template, titleLink string, l *s
retrier: &notify.Retrier{},
webhookURL: c.WebhookURL,
postJSONFunc: notify.PostJSON,
processor: proc,
}
return n, nil
@@ -142,11 +120,25 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return false, err
}
title := tmpl(n.conf.Title)
if err != nil {
return false, err
}
titleLink := tmpl(n.titleLink)
if err != nil {
return false, err
}
alerts := types.Alerts(as...)
color := colorGrey
switch alerts.Status() {
case model.AlertFiring:
color = colorRed
case model.AlertResolved:
color = colorGreen
}
var url string
if n.conf.WebhookURL != nil {
url = n.conf.WebhookURL.String()
@@ -158,12 +150,6 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
url = strings.TrimSpace(string(content))
}
bodyBlocks, err := n.prepareContent(ctx, as)
if err != nil {
n.logger.ErrorContext(ctx, "failed to prepare notification content", errors.Attr(err))
return false, err
}
// A message as referenced in https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1%2Cdotnet#request-body-schema
t := teamsMessage{
Type: "message",
@@ -175,7 +161,17 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
Schema: "http://adaptivecards.io/schemas/adaptive-card.json",
Type: "AdaptiveCard",
Version: "1.2",
Body: bodyBlocks,
Body: []Body{
{
Type: "TextBlock",
Text: title,
Weight: "Bolder",
Size: "Medium",
Wrap: true,
Style: "heading",
Color: color,
},
},
Actions: []Action{
{
Type: "Action.OpenUrl",
@@ -191,6 +187,20 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
},
}
// add labels and annotations to the body of all alerts
for _, alert := range as {
t.Attachments[0].Content.Body = append(t.Attachments[0].Content.Body, Body{
Type: "TextBlock",
Text: "Alerts",
Weight: "Bolder",
Size: "Medium",
Wrap: true,
Color: color,
})
t.Attachments[0].Content.Body = append(t.Attachments[0].Content.Body, n.createLabelsAndAnnotationsBody(alert)...)
}
var payload bytes.Buffer
if err = json.NewEncoder(&payload).Encode(t); err != nil {
return false, err
@@ -210,79 +220,6 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return shouldRetry, err
}
// prepareContent prepares the body blocks for the templated title and body.
func (n *Notifier) prepareContent(ctx context.Context, alerts []*types.Alert) ([]Body, error) {
// run the notification processor to get the title and body
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(alerts)
result, err := n.processor.ProcessAlertNotification(ctx, alertmanagertypes.NotificationProcessorInput{
TitleTemplate: customTitle,
BodyTemplate: customBody,
DefaultTitleTemplate: n.conf.Title,
// the default body template is not used and instead we add collection of labels and annotations for each alert
DefaultBodyTemplate: alertmanagertypes.NoOpTemplateString,
}, alerts, markdownrenderer.MarkdownFormatNoop)
if err != nil {
return nil, err
}
blocks := []Body{}
// common color for the title block
aggregateAlerts := types.Alerts(alerts...)
color := colorGrey
switch aggregateAlerts.Status() {
case model.AlertFiring:
color = colorRed
case model.AlertResolved:
color = colorGreen
}
// add title block
blocks = append(blocks, Body{
Type: "TextBlock",
Text: result.Title,
Weight: "Bolder",
Size: "Medium",
Wrap: true,
Style: "heading",
Color: color,
})
// handle default templated body
if result.IsDefaultTemplatedBody {
for _, alert := range alerts {
blocks = append(blocks, Body{
Type: "TextBlock",
Text: "Alerts",
Weight: "Bolder",
Size: "Medium",
Wrap: true,
Color: color,
})
blocks = append(blocks, n.createLabelsAndAnnotationsBody(alert)...)
}
} else {
for i, body := range result.Body {
b := Body{
Type: "TextBlock",
Text: body,
Wrap: true,
Color: colorGrey,
}
if i < len(alerts) {
if alerts[i].Resolved() {
b.Color = colorGreen
} else {
b.Color = colorRed
}
}
blocks = append(blocks, b)
}
}
return blocks, nil
}
func (*Notifier) createLabelsAndAnnotationsBody(alert *types.Alert) []Body {
bodies := []Body{}
bodies = append(bodies, Body{

View File

@@ -1,23 +1,9 @@
// Copyright 2024 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package msteamsv2
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
@@ -25,12 +11,6 @@ import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/alertmanager/alertnotificationprocessor"
"github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
@@ -39,31 +19,21 @@ import (
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
func newTestProcessor(tmpl *template.Template) alertmanagertypes.NotificationProcessor {
logger := slog.Default()
templater := alertmanagertemplate.New(tmpl, logger)
renderer := markdownrenderer.NewMarkdownRenderer(logger)
return alertnotificationprocessor.New(templater, renderer, filetemplatestore.NewEmptyStore(), logger)
}
// This is a test URL that has been modified to not be valid.
var testWebhookURL, _ = url.Parse("https://example.westeurope.logic.azure.com:443/workflows/xxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx")
func TestMSTeamsV2Retry(t *testing.T) {
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURL: &config.SecretURL{URL: testWebhookURL},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
test.CreateTmpl(t),
`{{ template "msteamsv2.default.titleLink" . }}`,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
@@ -90,16 +60,14 @@ func TestNotifier_Notify_WithReason(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURL: &config.SecretURL{URL: testWebhookURL},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
test.CreateTmpl(t),
`{{ template "msteamsv2.default.titleLink" . }}`,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
@@ -181,8 +149,7 @@ func TestMSTeamsV2Templating(t *testing.T) {
t.Run(tc.title, func(t *testing.T) {
tc.cfg.WebhookURL = &config.SecretURL{URL: u}
tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}
tmpl := test.CreateTmpl(t)
pd, err := New(tc.cfg, tmpl, tc.titleLink, promslog.NewNopLogger(), newTestProcessor(tmpl))
pd, err := New(tc.cfg, test.CreateTmpl(t), tc.titleLink, promslog.NewNopLogger())
require.NoError(t, err)
ctx := context.Background()
@@ -215,124 +182,20 @@ func TestMSTeamsV2RedactedURL(t *testing.T) {
defer fn()
secret := "secret"
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURL: &config.SecretURL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
test.CreateTmpl(t),
`{{ template "msteamsv2.default.titleLink" . }}`,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)
}
func TestPrepareContent(t *testing.T) {
t.Run("default template - firing alerts", func(t *testing.T) {
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURL: &config.SecretURL{URL: testWebhookURL},
HTTPConfig: &commoncfg.HTTPClientConfig{},
Title: "Alertname: {{ .CommonLabels.alertname }}",
},
tmpl,
`{{ template "msteamsv2.default.titleLink" . }}`,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
alerts := []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "test"},
// Custom body template
Annotations: model.LabelSet{
ruletypes.AnnotationBodyTemplate: "Firing alert: $alertname",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
},
}
blocks, err := notifier.prepareContent(ctx, alerts)
require.NoError(t, err)
require.NotEmpty(t, blocks)
// First block should be the title with color (firing = red)
require.Equal(t, "Bolder", blocks[0].Weight)
require.Equal(t, colorRed, blocks[0].Color)
// verify title text
require.Equal(t, "Alertname: test", blocks[0].Text)
// verify body text
require.Equal(t, "Firing alert: test", blocks[1].Text)
})
t.Run("custom template - per-alert color", func(t *testing.T) {
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURL: &config.SecretURL{URL: testWebhookURL},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
`{{ template "msteamsv2.default.titleLink" . }}`,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
alerts := []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "test1"},
Annotations: model.LabelSet{
"summary": "test",
ruletypes.AnnotationTitleTemplate: "Custom Title",
ruletypes.AnnotationBodyTemplate: "custom body $alertname",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
},
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "test2"},
Annotations: model.LabelSet{
"summary": "test",
ruletypes.AnnotationTitleTemplate: "Custom Title",
ruletypes.AnnotationBodyTemplate: "custom body $alertname",
},
StartsAt: time.Now().Add(-time.Hour),
EndsAt: time.Now().Add(-time.Minute),
},
},
}
blocks, err := notifier.prepareContent(ctx, alerts)
require.NoError(t, err)
require.NotEmpty(t, blocks)
// total 3 blocks: title and 2 body blocks
require.True(t, len(blocks) == 3)
// First block: title color is overall color of the alerts
require.Equal(t, colorRed, blocks[0].Color)
// verify title text
require.Equal(t, "Custom Title", blocks[0].Text)
// Body blocks should have per-alert color
require.Equal(t, colorRed, blocks[1].Color) // firing
require.Equal(t, colorGreen, blocks[2].Color) // resolved
})
}
func TestMSTeamsV2ReadingURLFromFile(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
@@ -342,16 +205,14 @@ func TestMSTeamsV2ReadingURLFromFile(t *testing.T) {
_, err = f.WriteString(u.String() + "\n")
require.NoError(t, err, "writing to temp file failed")
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.MSTeamsV2Config{
WebhookURLFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
test.CreateTmpl(t),
`{{ template "msteamsv2.default.titleLink" . }}`,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)

View File

@@ -1,2 +0,0 @@
my_secret_api_key

View File

@@ -1,350 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package opsgenie
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"maps"
"net/http"
"os"
"strings"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
const (
Integration = "opsgenie"
)
// https://docs.opsgenie.com/docs/alert-api - 130 characters meaning runes.
const maxMessageLenRunes = 130
// Notifier implements a Notifier for OpsGenie notifications.
type Notifier struct {
conf *config.OpsGenieConfig
tmpl *template.Template
logger *slog.Logger
client *http.Client
retrier *notify.Retrier
processor alertmanagertypes.NotificationProcessor
}
// New returns a new OpsGenie notifier.
func New(c *config.OpsGenieConfig, t *template.Template, l *slog.Logger, proc alertmanagertypes.NotificationProcessor, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := notify.NewClientWithTracing(*c.HTTPConfig, Integration, httpOpts...)
if err != nil {
return nil, err
}
return &Notifier{
conf: c,
tmpl: t,
logger: l,
client: client,
retrier: &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},
processor: proc,
}, nil
}
type opsGenieCreateMessage struct {
Alias string `json:"alias"`
Message string `json:"message"`
Description string `json:"description,omitempty"`
Details map[string]string `json:"details"`
Source string `json:"source"`
Responders []opsGenieCreateMessageResponder `json:"responders,omitempty"`
Tags []string `json:"tags,omitempty"`
Note string `json:"note,omitempty"`
Priority string `json:"priority,omitempty"`
Entity string `json:"entity,omitempty"`
Actions []string `json:"actions,omitempty"`
}
type opsGenieCreateMessageResponder struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Type string `json:"type"` // team, user, escalation, schedule etc.
}
type opsGenieCloseMessage struct {
Source string `json:"source"`
}
type opsGenieUpdateMessageMessage struct {
Message string `json:"message,omitempty"`
}
type opsGenieUpdateDescriptionMessage struct {
Description string `json:"description,omitempty"`
}
// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
requests, retry, err := n.createRequests(ctx, as...)
if err != nil {
return retry, err
}
for _, req := range requests {
req.Header.Set("User-Agent", notify.UserAgentHeader)
resp, err := n.client.Do(req) //nolint:bodyclose
if err != nil {
return true, err
}
shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
notify.Drain(resp)
if err != nil {
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
}
return true, nil
}
// Like Split but filter out empty strings.
func safeSplit(s, sep string) []string {
a := strings.Split(strings.TrimSpace(s), sep)
b := a[:0]
for _, x := range a {
if x != "" {
b = append(b, x)
}
}
return b
}
// prepareContent extracts custom templates from alert annotations, runs the
// notification processor, and returns a ready-to-use title (truncated to the
// OpsGenie 130-rune limit) and description.
func (n *Notifier) prepareContent(ctx context.Context, alerts []*types.Alert) (string, string, error) {
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(alerts)
result, err := n.processor.ProcessAlertNotification(ctx, alertmanagertypes.NotificationProcessorInput{
TitleTemplate: customTitle,
BodyTemplate: customBody,
DefaultTitleTemplate: n.conf.Message,
DefaultBodyTemplate: n.conf.Description,
}, alerts, markdownrenderer.MarkdownFormatHTML)
if err != nil {
return "", "", err
}
title := result.Title
description := strings.Join(result.Body, "\n")
if result.IsCustomTemplated() {
// OpsGenie uses basic HTML for alert description previews, so we
// separate each per-alert body with an <hr> divider.
var b strings.Builder
for i, part := range result.Body {
if i > 0 {
b.WriteString("<hr>")
}
b.WriteString("<div>")
b.WriteString(part)
b.WriteString("</div>")
}
description = b.String()
}
title, truncated := notify.TruncateInRunes(title, maxMessageLenRunes)
if truncated {
n.logger.WarnContext(ctx, "Truncated message", slog.Int("max_runes", maxMessageLenRunes))
}
return title, description, nil
}
// Create requests for a list of alerts.
func (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*http.Request, bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return nil, false, err
}
logger := n.logger.With(slog.Any("group_key", key))
logger.DebugContext(ctx, "extracted group key")
data := notify.GetTemplateData(ctx, n.tmpl, as, logger)
tmpl := notify.TmplText(n.tmpl, data, &err)
details := make(map[string]string)
maps.Copy(details, data.CommonLabels)
for k, v := range n.conf.Details {
details[k] = tmpl(v)
}
requests := []*http.Request{}
var (
alias = key.Hash()
alerts = types.Alerts(as...)
)
switch alerts.Status() {
case model.AlertResolved:
resolvedEndpointURL := n.conf.APIURL.Copy()
resolvedEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/close", alias)
q := resolvedEndpointURL.Query()
q.Set("identifierType", "alias")
resolvedEndpointURL.RawQuery = q.Encode()
msg := &opsGenieCloseMessage{Source: tmpl(n.conf.Source)}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return nil, false, err
}
req, err := http.NewRequest("POST", resolvedEndpointURL.String(), &buf)
if err != nil {
return nil, true, err
}
requests = append(requests, req.WithContext(ctx))
default:
message, description, err := n.prepareContent(ctx, as)
if err != nil {
n.logger.ErrorContext(ctx, "failed to prepare notification content", errors.Attr(err))
return nil, false, err
}
createEndpointURL := n.conf.APIURL.Copy()
createEndpointURL.Path += "v2/alerts"
var responders []opsGenieCreateMessageResponder
for _, r := range n.conf.Responders {
responder := opsGenieCreateMessageResponder{
ID: tmpl(r.ID),
Name: tmpl(r.Name),
Username: tmpl(r.Username),
Type: tmpl(r.Type),
}
if responder == (opsGenieCreateMessageResponder{}) {
// Filter out empty responders. This is useful if you want to fill
// responders dynamically from alert's common labels.
continue
}
if responder.Type == "teams" {
teams := safeSplit(responder.Name, ",")
for _, team := range teams {
newResponder := opsGenieCreateMessageResponder{
Name: tmpl(team),
Type: tmpl("team"),
}
responders = append(responders, newResponder)
}
continue
}
responders = append(responders, responder)
}
msg := &opsGenieCreateMessage{
Alias: alias,
Message: message,
Description: description,
Details: details,
Source: tmpl(n.conf.Source),
Responders: responders,
Tags: safeSplit(tmpl(n.conf.Tags), ","),
Note: tmpl(n.conf.Note),
Priority: tmpl(n.conf.Priority),
Entity: tmpl(n.conf.Entity),
Actions: safeSplit(tmpl(n.conf.Actions), ","),
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return nil, false, err
}
req, err := http.NewRequest("POST", createEndpointURL.String(), &buf)
if err != nil {
return nil, true, err
}
requests = append(requests, req.WithContext(ctx))
if n.conf.UpdateAlerts {
updateMessageEndpointURL := n.conf.APIURL.Copy()
updateMessageEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/message", alias)
q := updateMessageEndpointURL.Query()
q.Set("identifierType", "alias")
updateMessageEndpointURL.RawQuery = q.Encode()
updateMsgMsg := &opsGenieUpdateMessageMessage{
Message: msg.Message,
}
var updateMessageBuf bytes.Buffer
if err := json.NewEncoder(&updateMessageBuf).Encode(updateMsgMsg); err != nil {
return nil, false, err
}
req, err := http.NewRequest("PUT", updateMessageEndpointURL.String(), &updateMessageBuf)
if err != nil {
return nil, true, err
}
requests = append(requests, req)
updateDescriptionEndpointURL := n.conf.APIURL.Copy()
updateDescriptionEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/description", alias)
q = updateDescriptionEndpointURL.Query()
q.Set("identifierType", "alias")
updateDescriptionEndpointURL.RawQuery = q.Encode()
updateDescMsg := &opsGenieUpdateDescriptionMessage{
Description: msg.Description,
}
var updateDescriptionBuf bytes.Buffer
if err := json.NewEncoder(&updateDescriptionBuf).Encode(updateDescMsg); err != nil {
return nil, false, err
}
req, err = http.NewRequest("PUT", updateDescriptionEndpointURL.String(), &updateDescriptionBuf)
if err != nil {
return nil, true, err
}
requests = append(requests, req.WithContext(ctx))
}
}
var apiKey string
if n.conf.APIKey != "" {
apiKey = tmpl(string(n.conf.APIKey))
} else {
content, err := os.ReadFile(n.conf.APIKeyFile)
if err != nil {
return nil, false, errors.WrapInternalf(err, errors.CodeInternal, "read key_file error")
}
apiKey = tmpl(string(content))
apiKey = strings.TrimSpace(string(apiKey))
}
if err != nil {
return nil, false, errors.WrapInternalf(err, errors.CodeInternal, "templating error")
}
for _, req := range requests {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", apiKey))
}
return requests, true, nil
}

View File

@@ -1,462 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package opsgenie
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/alertmanager/alertnotificationprocessor"
"github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/notify/test"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
func newTestProcessor(tmpl *template.Template) alertmanagertypes.NotificationProcessor {
logger := slog.Default()
templater := alertmanagertemplate.New(tmpl, logger)
renderer := markdownrenderer.NewMarkdownRenderer(logger)
return alertnotificationprocessor.New(templater, renderer, filetemplatestore.NewEmptyStore(), logger)
}
func TestOpsGenieRetry(t *testing.T) {
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.OpsGenieConfig{
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
retryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)
for statusCode, expected := range test.RetryTests(retryCodes) {
actual, _ := notifier.retrier.Check(statusCode, nil)
require.Equal(t, expected, actual, "error on status %d", statusCode)
}
}
func TestOpsGenieRedactedURL(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
key := "key"
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.OpsGenieConfig{
APIURL: &config.URL{URL: u},
APIKey: config.Secret(key),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)
}
func TestGettingOpsGegineApikeyFromFile(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
key := "key"
f, err := os.CreateTemp(t.TempDir(), "opsgenie_test")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(key)
require.NoError(t, err, "writing to temp file failed")
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.OpsGenieConfig{
APIURL: &config.URL{URL: u},
APIKeyFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)
}
func TestOpsGenie(t *testing.T) {
u, err := url.Parse("https://opsgenie/api")
if err != nil {
t.Fatalf("failed to parse URL: %v", err)
}
logger := promslog.NewNopLogger()
tmpl := test.CreateTmpl(t)
for _, tc := range []struct {
title string
cfg *config.OpsGenieConfig
expectedEmptyAlertBody string
expectedBody string
}{
{
title: "config without details",
cfg: &config.OpsGenieConfig{
NotifierConfig: config.NotifierConfig{
VSendResolved: true,
},
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
Source: `{{ .CommonLabels.Source }}`,
Responders: []config.OpsGenieConfigResponder{
{
Name: `{{ .CommonLabels.ResponderName1 }}`,
Type: `{{ .CommonLabels.ResponderType1 }}`,
},
{
Name: `{{ .CommonLabels.ResponderName2 }}`,
Type: `{{ .CommonLabels.ResponderType2 }}`,
},
},
Tags: `{{ .CommonLabels.Tags }}`,
Note: `{{ .CommonLabels.Note }}`,
Priority: `{{ .CommonLabels.Priority }}`,
Entity: `{{ .CommonLabels.Entity }}`,
Actions: `{{ .CommonLabels.Actions }}`,
APIKey: `{{ .ExternalURL }}`,
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{},"source":""}
`,
expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"EscalationA","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"]}
`,
},
{
title: "config with details",
cfg: &config.OpsGenieConfig{
NotifierConfig: config.NotifierConfig{
VSendResolved: true,
},
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
Source: `{{ .CommonLabels.Source }}`,
Details: map[string]string{
"Description": `adjusted {{ .CommonLabels.Description }}`,
},
Responders: []config.OpsGenieConfigResponder{
{
Name: `{{ .CommonLabels.ResponderName1 }}`,
Type: `{{ .CommonLabels.ResponderType1 }}`,
},
{
Name: `{{ .CommonLabels.ResponderName2 }}`,
Type: `{{ .CommonLabels.ResponderType2 }}`,
},
},
Tags: `{{ .CommonLabels.Tags }}`,
Note: `{{ .CommonLabels.Note }}`,
Priority: `{{ .CommonLabels.Priority }}`,
Entity: `{{ .CommonLabels.Entity }}`,
Actions: `{{ .CommonLabels.Actions }}`,
APIKey: `{{ .ExternalURL }}`,
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{"Description":"adjusted "},"source":""}
`,
expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"EscalationA","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"]}
`,
},
{
title: "config with multiple teams",
cfg: &config.OpsGenieConfig{
NotifierConfig: config.NotifierConfig{
VSendResolved: true,
},
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
Source: `{{ .CommonLabels.Source }}`,
Details: map[string]string{
"Description": `adjusted {{ .CommonLabels.Description }}`,
},
Responders: []config.OpsGenieConfigResponder{
{
Name: `{{ .CommonLabels.ResponderName3 }}`,
Type: `{{ .CommonLabels.ResponderType3 }}`,
},
},
Tags: `{{ .CommonLabels.Tags }}`,
Note: `{{ .CommonLabels.Note }}`,
Priority: `{{ .CommonLabels.Priority }}`,
APIKey: `{{ .ExternalURL }}`,
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{"Description":"adjusted "},"source":""}
`,
expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"TeamB","type":"team"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1"}
`,
},
} {
t.Run(tc.title, func(t *testing.T) {
notifier, err := New(tc.cfg, tmpl, logger, newTestProcessor(tmpl))
require.NoError(t, err)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
expectedURL, _ := url.Parse("https://opsgenie/apiv2/alerts")
// Empty alert.
alert1 := &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
req, retry, err := notifier.createRequests(ctx, alert1)
require.NoError(t, err)
require.Len(t, req, 1)
require.True(t, retry)
require.Equal(t, expectedURL, req[0].URL)
require.Equal(t, "GenieKey http://am", req[0].Header.Get("Authorization"))
require.Equal(t, tc.expectedEmptyAlertBody, readBody(t, req[0]))
// Fully defined alert.
alert2 := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"Message": "message",
"Description": "description",
"Source": "http://prometheus",
"ResponderName1": "TeamA",
"ResponderType1": "team",
"ResponderName2": "EscalationA",
"ResponderType2": "escalation",
"ResponderName3": "TeamA,TeamB",
"ResponderType3": "teams",
"Tags": "tag1,tag2",
"Note": "this is a note",
"Priority": "P1",
"Entity": "test-domain",
"Actions": "doThis,doThat",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
req, retry, err = notifier.createRequests(ctx, alert2)
require.NoError(t, err)
require.True(t, retry)
require.Len(t, req, 1)
require.Equal(t, tc.expectedBody, readBody(t, req[0]))
// Broken API Key Template.
tc.cfg.APIKey = "{{ kaput "
_, _, err = notifier.createRequests(ctx, alert2)
require.Error(t, err)
require.Equal(t, "template: :1: function \"kaput\" not defined", err.Error())
})
}
}
func TestOpsGenieWithUpdate(t *testing.T) {
u, err := url.Parse("https://test-opsgenie-url")
require.NoError(t, err)
tmpl := test.CreateTmpl(t)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
opsGenieConfigWithUpdate := config.OpsGenieConfig{
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
UpdateAlerts: true,
APIKey: "test-api-key",
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
}
notifierWithUpdate, err := New(&opsGenieConfigWithUpdate, tmpl, promslog.NewNopLogger(), newTestProcessor(tmpl))
alert := &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
Labels: model.LabelSet{
"Message": "new message",
"Description": "new description",
},
},
}
require.NoError(t, err)
requests, retry, err := notifierWithUpdate.createRequests(ctx, alert)
require.NoError(t, err)
require.True(t, retry)
require.Len(t, requests, 3)
body0 := readBody(t, requests[0])
body1 := readBody(t, requests[1])
body2 := readBody(t, requests[2])
key, _ := notify.ExtractGroupKey(ctx)
alias := key.Hash()
require.Equal(t, "https://test-opsgenie-url/v2/alerts", requests[0].URL.String())
require.NotEmpty(t, body0)
require.Equal(t, requests[1].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/message?identifierType=alias", alias))
require.JSONEq(t, `{"message":"new message"}`, body1)
require.Equal(t, requests[2].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/description?identifierType=alias", alias))
require.JSONEq(t, `{"description":"new description"}`, body2)
}
func TestOpsGenieApiKeyFile(t *testing.T) {
u, err := url.Parse("https://test-opsgenie-url")
require.NoError(t, err)
tmpl := test.CreateTmpl(t)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
opsGenieConfigWithUpdate := config.OpsGenieConfig{
APIKeyFile: `./api_key_file`,
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
}
notifierWithUpdate, err := New(&opsGenieConfigWithUpdate, tmpl, promslog.NewNopLogger(), newTestProcessor(tmpl))
require.NoError(t, err)
requests, _, err := notifierWithUpdate.createRequests(ctx)
require.NoError(t, err)
require.Equal(t, "GenieKey my_secret_api_key", requests[0].Header.Get("Authorization"))
}
func TestPrepareContent(t *testing.T) {
t.Run("default template", func(t *testing.T) {
tmpl := test.CreateTmpl(t)
logger := promslog.NewNopLogger()
proc := newTestProcessor(tmpl)
notifier := &Notifier{
conf: &config.OpsGenieConfig{
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
},
tmpl: tmpl,
logger: logger,
processor: proc,
}
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
alert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"Message": "Firing alert: test",
"Description": "Check runbook for more details",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
alerts := []*types.Alert{alert}
title, desc, prepErr := notifier.prepareContent(ctx, alerts)
require.NoError(t, prepErr)
require.Equal(t, "Firing alert: test", title)
require.Equal(t, "Check runbook for more details", desc)
})
t.Run("custom template", func(t *testing.T) {
tmpl := test.CreateTmpl(t)
logger := promslog.NewNopLogger()
proc := newTestProcessor(tmpl)
notifier := &Notifier{
conf: &config.OpsGenieConfig{
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
},
tmpl: tmpl,
logger: logger,
processor: proc,
}
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
alert1 := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"service": "payment",
"namespace": "potter-the-harry",
},
Annotations: model.LabelSet{
ruletypes.AnnotationTitleTemplate: "High request throughput for $service",
ruletypes.AnnotationBodyTemplate: "Alert firing in NS: $labels.namespace",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
alert2 := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"service": "payment",
"namespace": "smart-the-rat",
},
Annotations: model.LabelSet{
ruletypes.AnnotationTitleTemplate: "High request throughput for $service",
ruletypes.AnnotationBodyTemplate: "Alert firing in NS: $labels.namespace",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
alerts := []*types.Alert{alert1, alert2}
title, desc, err := notifier.prepareContent(ctx, alerts)
require.NoError(t, err)
require.Equal(t, "High request throughput for payment", title)
// Each alert body wrapped in <div>, separated by <hr>
require.Equal(t, "<div><p>Alert firing in NS: potter-the-harry</p><p></p></div><hr><div><p>Alert firing in NS: smart-the-rat</p><p></p></div>", desc)
})
}
func readBody(t *testing.T, r *http.Request) string {
t.Helper()
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
return string(body)
}

View File

@@ -1,415 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pagerduty
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"strings"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/alecthomas/units"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
const (
Integration = "pagerduty"
)
const (
maxEventSize int = 512000
// https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTc4-send-a-v1-event - 1024 characters or runes.
maxV1DescriptionLenRunes = 1024
// https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgx-send-an-alert-event - 1024 characters or runes.
maxV2SummaryLenRunes = 1024
)
// Notifier implements a Notifier for PagerDuty notifications.
type Notifier struct {
conf *config.PagerdutyConfig
tmpl *template.Template
logger *slog.Logger
apiV1 string // for tests.
client *http.Client
retrier *notify.Retrier
processor alertmanagertypes.NotificationProcessor
}
// New returns a new PagerDuty notifier.
func New(c *config.PagerdutyConfig, t *template.Template, l *slog.Logger, proc alertmanagertypes.NotificationProcessor, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := notify.NewClientWithTracing(*c.HTTPConfig, Integration, httpOpts...)
if err != nil {
return nil, err
}
n := &Notifier{conf: c, tmpl: t, logger: l, client: client, processor: proc}
if c.ServiceKey != "" || c.ServiceKeyFile != "" {
n.apiV1 = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
// Retrying can solve the issue on 403 (rate limiting) and 5xx response codes.
// https://developer.pagerduty.com/docs/events-api-v1-overview#api-response-codes--retry-logic
n.retrier = &notify.Retrier{RetryCodes: []int{http.StatusForbidden}, CustomDetailsFunc: errDetails}
} else {
// Retrying can solve the issue on 429 (rate limiting) and 5xx response codes.
// https://developer.pagerduty.com/docs/events-api-v2-overview#response-codes--retry-logic
n.retrier = &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}, CustomDetailsFunc: errDetails}
}
return n, nil
}
const (
pagerDutyEventTrigger = "trigger"
pagerDutyEventResolve = "resolve"
)
type pagerDutyMessage struct {
RoutingKey string `json:"routing_key,omitempty"`
ServiceKey string `json:"service_key,omitempty"`
DedupKey string `json:"dedup_key,omitempty"`
IncidentKey string `json:"incident_key,omitempty"`
EventType string `json:"event_type,omitempty"`
Description string `json:"description,omitempty"`
EventAction string `json:"event_action"`
Payload *pagerDutyPayload `json:"payload"`
Client string `json:"client,omitempty"`
ClientURL string `json:"client_url,omitempty"`
Details map[string]any `json:"details,omitempty"`
Images []pagerDutyImage `json:"images,omitempty"`
Links []pagerDutyLink `json:"links,omitempty"`
}
type pagerDutyLink struct {
HRef string `json:"href"`
Text string `json:"text"`
}
type pagerDutyImage struct {
Src string `json:"src"`
Alt string `json:"alt"`
Href string `json:"href"`
}
type pagerDutyPayload struct {
Summary string `json:"summary"`
Source string `json:"source"`
Severity string `json:"severity"`
Timestamp string `json:"timestamp,omitempty"`
Class string `json:"class,omitempty"`
Component string `json:"component,omitempty"`
Group string `json:"group,omitempty"`
CustomDetails map[string]any `json:"custom_details,omitempty"`
}
func (n *Notifier) encodeMessage(ctx context.Context, msg *pagerDutyMessage) (bytes.Buffer, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return buf, errors.WrapInternalf(err, errors.CodeInternal, "failed to encode PagerDuty message")
}
if buf.Len() > maxEventSize {
truncatedMsg := fmt.Sprintf("Custom details have been removed because the original event exceeds the maximum size of %s", units.MetricBytes(maxEventSize).String())
if n.apiV1 != "" {
msg.Details = map[string]any{"error": truncatedMsg}
} else {
msg.Payload.CustomDetails = map[string]any{"error": truncatedMsg}
}
n.logger.WarnContext(ctx, "Truncated Details because message of size exceeds limit", slog.String("message_size", units.MetricBytes(buf.Len()).String()), slog.String("max_size", units.MetricBytes(maxEventSize).String()))
buf.Reset()
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return buf, errors.WrapInternalf(err, errors.CodeInternal, "failed to encode PagerDuty message")
}
}
return buf, nil
}
func (n *Notifier) notifyV1(
ctx context.Context,
eventType string,
key notify.Key,
data *template.Data,
details map[string]any,
title string,
) (bool, error) {
var tmplErr error
tmpl := notify.TmplText(n.tmpl, data, &tmplErr)
description, truncated := notify.TruncateInRunes(title, maxV1DescriptionLenRunes)
if truncated {
n.logger.WarnContext(ctx, "Truncated description", slog.Any("key", key), slog.Int("max_runes", maxV1DescriptionLenRunes))
}
serviceKey := string(n.conf.ServiceKey)
if serviceKey == "" {
content, fileErr := os.ReadFile(n.conf.ServiceKeyFile)
if fileErr != nil {
return false, errors.WrapInternalf(fileErr, errors.CodeInternal, "failed to read service key from file")
}
serviceKey = strings.TrimSpace(string(content))
}
msg := &pagerDutyMessage{
ServiceKey: tmpl(serviceKey),
EventType: eventType,
IncidentKey: key.Hash(),
Description: description,
Details: details,
}
if eventType == pagerDutyEventTrigger {
msg.Client = tmpl(n.conf.Client)
msg.ClientURL = tmpl(n.conf.ClientURL)
}
if tmplErr != nil {
return false, errors.WrapInternalf(tmplErr, errors.CodeInternal, "failed to template PagerDuty v1 message")
}
// Ensure that the service key isn't empty after templating.
if msg.ServiceKey == "" {
return false, errors.NewInternalf(errors.CodeInternal, "service key cannot be empty")
}
encodedMsg, err := n.encodeMessage(ctx, msg)
if err != nil {
return false, err
}
resp, err := notify.PostJSON(ctx, n.client, n.apiV1, &encodedMsg) //nolint:bodyclose
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "failed to post message to PagerDuty v1")
}
defer notify.Drain(resp)
return n.retrier.Check(resp.StatusCode, resp.Body)
}
func (n *Notifier) notifyV2(
ctx context.Context,
eventType string,
key notify.Key,
data *template.Data,
details map[string]any,
title string,
) (bool, error) {
var tmplErr error
tmpl := notify.TmplText(n.tmpl, data, &tmplErr)
if n.conf.Severity == "" {
n.conf.Severity = "error"
}
summary, truncated := notify.TruncateInRunes(title, maxV2SummaryLenRunes)
if truncated {
n.logger.WarnContext(ctx, "Truncated summary", slog.Any("key", key), slog.Int("max_runes", maxV2SummaryLenRunes))
}
routingKey := string(n.conf.RoutingKey)
if routingKey == "" {
content, fileErr := os.ReadFile(n.conf.RoutingKeyFile)
if fileErr != nil {
return false, errors.WrapInternalf(fileErr, errors.CodeInternal, "failed to read routing key from file")
}
routingKey = strings.TrimSpace(string(content))
}
msg := &pagerDutyMessage{
Client: tmpl(n.conf.Client),
ClientURL: tmpl(n.conf.ClientURL),
RoutingKey: tmpl(routingKey),
EventAction: eventType,
DedupKey: key.Hash(),
Images: make([]pagerDutyImage, 0, len(n.conf.Images)),
Links: make([]pagerDutyLink, 0, len(n.conf.Links)),
Payload: &pagerDutyPayload{
Summary: summary,
Source: tmpl(n.conf.Source),
Severity: tmpl(n.conf.Severity),
CustomDetails: details,
Class: tmpl(n.conf.Class),
Component: tmpl(n.conf.Component),
Group: tmpl(n.conf.Group),
},
}
for _, item := range n.conf.Images {
image := pagerDutyImage{
Src: tmpl(item.Src),
Alt: tmpl(item.Alt),
Href: tmpl(item.Href),
}
if image.Src != "" {
msg.Images = append(msg.Images, image)
}
}
for _, item := range n.conf.Links {
link := pagerDutyLink{
HRef: tmpl(item.Href),
Text: tmpl(item.Text),
}
if link.HRef != "" {
msg.Links = append(msg.Links, link)
}
}
if tmplErr != nil {
return false, errors.WrapInternalf(tmplErr, errors.CodeInternal, "failed to template PagerDuty v2 message")
}
// Ensure that the routing key isn't empty after templating.
if msg.RoutingKey == "" {
return false, errors.NewInternalf(errors.CodeInternal, "routing key cannot be empty")
}
encodedMsg, err := n.encodeMessage(ctx, msg)
if err != nil {
return false, err
}
resp, err := notify.PostJSON(ctx, n.client, n.conf.URL.String(), &encodedMsg) //nolint:bodyclose
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "failed to post message to PagerDuty")
}
defer notify.Drain(resp)
retry, err := n.retrier.Check(resp.StatusCode, resp.Body)
if err != nil {
return retry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
return retry, err
}
// prepareContent extracts custom templates from alert annotations, runs the
// notification processor, and returns the processed title ready for use.
func (n *Notifier) prepareContent(ctx context.Context, alerts []*types.Alert) (string, error) {
customTitle, _ := alertmanagertemplate.ExtractTemplatesFromAnnotations(alerts)
result, err := n.processor.ProcessAlertNotification(ctx, alertmanagertypes.NotificationProcessorInput{
TitleTemplate: customTitle,
DefaultTitleTemplate: n.conf.Description,
BodyTemplate: alertmanagertypes.NoOpTemplateString,
DefaultBodyTemplate: alertmanagertypes.NoOpTemplateString,
}, alerts, markdownrenderer.MarkdownFormatNoop)
if err != nil {
return "", err
}
return result.Title, nil
}
// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
logger := n.logger.With(slog.Any("group_key", key))
var (
alerts = types.Alerts(as...)
data = notify.GetTemplateData(ctx, n.tmpl, as, logger)
eventType = pagerDutyEventTrigger
)
if alerts.Status() == model.AlertResolved {
eventType = pagerDutyEventResolve
}
logger.DebugContext(ctx, "extracted group key", slog.String("event_type", eventType))
details, err := n.renderDetails(data)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "failed to render details: %v", err)
}
if n.conf.Timeout > 0 {
nfCtx, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, errors.NewInternalf(errors.CodeTimeout, "configured pagerduty timeout reached (%s)", n.conf.Timeout))
defer cancel()
ctx = nfCtx
}
title, err := n.prepareContent(ctx, as)
if err != nil {
n.logger.ErrorContext(ctx, "failed to prepare notification content", errors.Attr(err))
return false, err
}
nf := n.notifyV2
if n.apiV1 != "" {
nf = n.notifyV1
}
retry, err := nf(ctx, eventType, key, data, details, title)
if err != nil {
if ctx.Err() != nil {
err = errors.WrapInternalf(err, errors.CodeInternal, "failed to notify PagerDuty: %v", context.Cause(ctx))
}
return retry, err
}
return retry, nil
}
func errDetails(status int, body io.Reader) string {
// See https://v2.developer.pagerduty.com/docs/trigger-events for the v1 events API.
// See https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2 for the v2 events API.
if status != http.StatusBadRequest || body == nil {
return ""
}
var pgr struct {
Status string `json:"status"`
Message string `json:"message"`
Errors []string `json:"errors"`
}
if err := json.NewDecoder(body).Decode(&pgr); err != nil {
return ""
}
return fmt.Sprintf("%s: %s", pgr.Message, strings.Join(pgr.Errors, ","))
}
func (n *Notifier) renderDetails(
data *template.Data,
) (map[string]any, error) {
var (
tmplTextErr error
tmplText = notify.TmplText(n.tmpl, data, &tmplTextErr)
tmplTextFunc = func(tmpl string) (string, error) {
return tmplText(tmpl), tmplTextErr
}
)
var err error
rendered := make(map[string]any, len(n.conf.Details))
for k, v := range n.conf.Details {
rendered[k], err = template.DeepCopyWithTemplate(v, tmplTextFunc)
if err != nil {
return nil, err
}
}
return rendered, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,8 @@ package alertmanagernotify
import (
"log/slog"
"slices"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/email"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/msteamsv2"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/opsgenie"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/pagerduty"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/slack"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/webhook"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/config/receiver"
"github.com/prometheus/alertmanager/notify"
@@ -17,16 +11,7 @@ import (
"github.com/prometheus/alertmanager/types"
)
var customNotifierIntegrations = []string{
webhook.Integration,
email.Integration,
pagerduty.Integration,
opsgenie.Integration,
slack.Integration,
msteamsv2.Integration,
}
func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, proc alertmanagertypes.NotificationProcessor) ([]notify.Integration, error) {
func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger) ([]notify.Integration, error) {
upstreamIntegrations, err := receiver.BuildReceiverIntegrations(nc, tmpl, logger)
if err != nil {
return nil, err
@@ -46,30 +31,15 @@ func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Templ
)
for _, integration := range upstreamIntegrations {
// skip upstream integration if we support custom integration for it
if !slices.Contains(customNotifierIntegrations, integration.Name()) {
// skip upstream msteamsv2 integration
if integration.Name() != "msteamsv2" {
integrations = append(integrations, integration)
}
}
for i, c := range nc.WebhookConfigs {
add(webhook.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) { return webhook.New(c, tmpl, l, proc) })
}
for i, c := range nc.EmailConfigs {
add(email.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) { return email.New(c, tmpl, l, proc), nil })
}
for i, c := range nc.PagerdutyConfigs {
add(pagerduty.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) { return pagerduty.New(c, tmpl, l, proc) })
}
for i, c := range nc.OpsGenieConfigs {
add(opsgenie.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) { return opsgenie.New(c, tmpl, l, proc) })
}
for i, c := range nc.SlackConfigs {
add(slack.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) { return slack.New(c, tmpl, l, proc) })
}
for i, c := range nc.MSTeamsV2Configs {
add(msteamsv2.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) {
return msteamsv2.New(c, tmpl, `{{ template "msteamsv2.default.titleLink" . }}`, l, proc)
add("msteamsv2", i, c, func(l *slog.Logger) (notify.Notifier, error) {
return msteamsv2.New(c, tmpl, `{{ template "msteamsv2.default.titleLink" . }}`, l)
})
}

View File

@@ -1,378 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package slack
import (
"bytes"
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"os"
"strings"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
const (
Integration = "slack"
colorRed = "#FF0000"
colorGreen = "#00FF00"
)
// https://api.slack.com/reference/messaging/attachments#legacy_fields - 1024, no units given, assuming runes or characters.
const maxTitleLenRunes = 1024
// Notifier implements a Notifier for Slack notifications.
type Notifier struct {
conf *config.SlackConfig
tmpl *template.Template
logger *slog.Logger
client *http.Client
retrier *notify.Retrier
processor alertmanagertypes.NotificationProcessor
postJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)
}
// New returns a new Slack notification handler.
func New(c *config.SlackConfig, t *template.Template, l *slog.Logger, proc alertmanagertypes.NotificationProcessor, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := notify.NewClientWithTracing(*c.HTTPConfig, Integration, httpOpts...)
if err != nil {
return nil, err
}
return &Notifier{
conf: c,
tmpl: t,
logger: l,
client: client,
retrier: &notify.Retrier{},
processor: proc,
postJSONFunc: notify.PostJSON,
}, nil
}
// request is the request for sending a slack notification.
type request struct {
Channel string `json:"channel,omitempty"`
Username string `json:"username,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
IconURL string `json:"icon_url,omitempty"`
LinkNames bool `json:"link_names,omitempty"`
Text string `json:"text,omitempty"`
Attachments []attachment `json:"attachments"`
}
// attachment is used to display a richly-formatted message block.
type attachment struct {
Title string `json:"title,omitempty"`
TitleLink string `json:"title_link,omitempty"`
Pretext string `json:"pretext,omitempty"`
Text string `json:"text"`
Fallback string `json:"fallback"`
CallbackID string `json:"callback_id"`
Fields []config.SlackField `json:"fields,omitempty"`
Actions []config.SlackAction `json:"actions,omitempty"`
ImageURL string `json:"image_url,omitempty"`
ThumbURL string `json:"thumb_url,omitempty"`
Footer string `json:"footer,omitempty"`
Color string `json:"color,omitempty"`
MrkdwnIn []string `json:"mrkdwn_in,omitempty"`
Blocks []any `json:"blocks,omitempty"`
}
// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
logger := n.logger.With(slog.Any("group_key", key))
logger.DebugContext(ctx, "extracted group key")
var (
data = notify.GetTemplateData(ctx, n.tmpl, as, logger)
tmplText = notify.TmplText(n.tmpl, data, &err)
)
attachments, err := n.prepareContent(ctx, as, tmplText)
if err != nil {
n.logger.ErrorContext(ctx, "failed to prepare notification content", errors.Attr(err))
return false, err
}
if len(attachments) > 0 {
n.addFieldsAndActions(&attachments[0], tmplText)
}
req := &request{
Channel: tmplText(n.conf.Channel),
Username: tmplText(n.conf.Username),
IconEmoji: tmplText(n.conf.IconEmoji),
IconURL: tmplText(n.conf.IconURL),
LinkNames: n.conf.LinkNames,
Text: tmplText(n.conf.MessageText),
Attachments: attachments,
}
if err != nil {
return false, err
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return false, err
}
var u string
if n.conf.APIURL != nil {
u = n.conf.APIURL.String()
} else {
content, err := os.ReadFile(n.conf.APIURLFile)
if err != nil {
return false, err
}
u = strings.TrimSpace(string(content))
}
if n.conf.Timeout > 0 {
postCtx, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, errors.NewInternalf(errors.CodeTimeout, "configured slack timeout reached (%s)", n.conf.Timeout))
defer cancel()
ctx = postCtx
}
resp, err := n.postJSONFunc(ctx, n.client, u, &buf) //nolint:bodyclose
if err != nil {
if ctx.Err() != nil {
err = errors.NewInternalf(errors.CodeInternal, "failed to post JSON to slack: %v", context.Cause(ctx))
}
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)
// Use a retrier to generate an error message for non-200 responses and
// classify them as retriable or not.
retry, err := n.retrier.Check(resp.StatusCode, resp.Body)
if err != nil {
err = errors.NewInternalf(errors.CodeInternal, "channel %q: %v", req.Channel, err)
return retry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
// Slack web API might return errors with a 200 response code.
// https://docs.slack.dev/tools/node-slack-sdk/web-api/#handle-errors
retry, err = checkResponseError(resp)
if err != nil {
err = errors.NewInternalf(errors.CodeInternal, "channel %q: %v", req.Channel, err)
return retry, notify.NewErrorWithReason(notify.ClientErrorReason, err)
}
return retry, nil
}
// prepareContent extracts custom templates from alert annotations, runs the
// notification processor, and returns the Slack attachment(s) ready to send.
func (n *Notifier) prepareContent(ctx context.Context, alerts []*types.Alert, tmplText func(string) string) ([]attachment, error) {
// Extract custom templates and process them
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(alerts)
result, err := n.processor.ProcessAlertNotification(ctx, alertmanagertypes.NotificationProcessorInput{
TitleTemplate: customTitle,
BodyTemplate: customBody,
DefaultTitleTemplate: n.conf.Title,
// use default body templating to prepare the attachment
// as default template uses plain text markdown rendering instead of blockkit
DefaultBodyTemplate: alertmanagertypes.NoOpTemplateString,
}, alerts, markdownrenderer.MarkdownFormatSlackMrkdwn)
if err != nil {
return nil, err
}
title, truncated := notify.TruncateInRunes(result.Title, maxTitleLenRunes)
if truncated {
n.logger.WarnContext(ctx, "Truncated title", slog.Int("max_runes", maxTitleLenRunes))
}
if result.IsDefaultTemplatedBody {
var markdownIn []string
if len(n.conf.MrkdwnIn) == 0 {
markdownIn = []string{"fallback", "pretext", "text"}
} else {
markdownIn = n.conf.MrkdwnIn
}
attachments := []attachment{
{
Title: title,
TitleLink: tmplText(n.conf.TitleLink),
Pretext: tmplText(n.conf.Pretext),
Text: tmplText(n.conf.Text),
Fallback: tmplText(n.conf.Fallback),
CallbackID: tmplText(n.conf.CallbackID),
ImageURL: tmplText(n.conf.ImageURL),
ThumbURL: tmplText(n.conf.ThumbURL),
Footer: tmplText(n.conf.Footer),
Color: tmplText(n.conf.Color),
MrkdwnIn: markdownIn,
},
}
return attachments, nil
}
// Custom template path: one title attachment + one attachment per alert body.
// Each alert body gets its own attachment so we can set per-alert color
// (red for firing, green for resolved).
attachments := make([]attachment, 0, 1+len(result.Body))
// Title-only attachment (no color)
attachments = append(attachments, attachment{
Title: title,
TitleLink: tmplText(n.conf.TitleLink),
})
for i, body := range result.Body {
color := colorRed // red for firing
if i < len(alerts) && alerts[i].Resolved() {
color = colorGreen // green for resolved
}
// If alert has related logs and traces, add them to the attachment as action buttons
var actionButtons []config.SlackAction
relatedLogsLink := alerts[i].Annotations[ruletypes.AnnotationRelatedLogs]
relatedTracesLink := alerts[i].Annotations[ruletypes.AnnotationRelatedTraces]
if relatedLogsLink != "" {
actionButtons = append(actionButtons, config.SlackAction{
Type: "button",
Text: "View Related Logs",
URL: string(relatedLogsLink),
})
}
if relatedTracesLink != "" {
actionButtons = append(actionButtons, config.SlackAction{
Type: "button",
Text: "View Related Traces",
URL: string(relatedTracesLink),
})
}
attachments = append(attachments, attachment{
Text: body,
Color: color,
MrkdwnIn: []string{"text"},
Actions: actionButtons,
})
}
return attachments, nil
}
// addFieldsAndActions populates fields and actions on the attachment from the Slack config.
func (n *Notifier) addFieldsAndActions(att *attachment, tmplText func(string) string) {
numFields := len(n.conf.Fields)
if numFields > 0 {
fields := make([]config.SlackField, numFields)
for index, field := range n.conf.Fields {
var short bool
if field.Short != nil {
short = *field.Short
} else {
short = n.conf.ShortFields
}
fields[index] = config.SlackField{
Title: tmplText(field.Title),
Value: tmplText(field.Value),
Short: &short,
}
}
att.Fields = fields
}
numActions := len(n.conf.Actions)
if numActions > 0 {
actions := make([]config.SlackAction, numActions)
for index, action := range n.conf.Actions {
slackAction := config.SlackAction{
Type: tmplText(action.Type),
Text: tmplText(action.Text),
URL: tmplText(action.URL),
Style: tmplText(action.Style),
Name: tmplText(action.Name),
Value: tmplText(action.Value),
}
if action.ConfirmField != nil {
slackAction.ConfirmField = &config.SlackConfirmationField{
Title: tmplText(action.ConfirmField.Title),
Text: tmplText(action.ConfirmField.Text),
OkText: tmplText(action.ConfirmField.OkText),
DismissText: tmplText(action.ConfirmField.DismissText),
}
}
actions[index] = slackAction
}
att.Actions = actions
}
}
// checkResponseError parses out the error message from Slack API response.
func checkResponseError(resp *http.Response) (bool, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return true, errors.WrapInternalf(err, errors.CodeInternal, "could not read response body")
}
if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
return checkJSONResponseError(body)
}
return checkTextResponseError(body)
}
// checkTextResponseError classifies plaintext responses from Slack.
// A plaintext (non-JSON) response is successful if it's a string "ok".
// This is typically a response for an Incoming Webhook
// (https://api.slack.com/messaging/webhooks#handling_errors)
func checkTextResponseError(body []byte) (bool, error) {
if !bytes.Equal(body, []byte("ok")) {
return false, errors.NewInternalf(errors.CodeInternal, "received an error response from Slack: %s", string(body))
}
return false, nil
}
// checkJSONResponseError classifies JSON responses from Slack.
func checkJSONResponseError(body []byte) (bool, error) {
// response is for parsing out errors from the JSON response.
type response struct {
OK bool `json:"ok"`
Error string `json:"error"`
}
var data response
if err := json.Unmarshal(body, &data); err != nil {
return true, errors.NewInternalf(errors.CodeInternal, "could not unmarshal JSON response %q: %v", string(body), err)
}
if !data.OK {
return false, errors.NewInternalf(errors.CodeInternal, "error response from Slack: %s", data.Error)
}
return false, nil
}

View File

@@ -1,596 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package slack
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/alertmanager/alertnotificationprocessor"
"github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/notify/test"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
func newTestProcessor(tmpl *template.Template) alertmanagertypes.NotificationProcessor {
logger := slog.Default()
templater := alertmanagertemplate.New(tmpl, logger)
renderer := markdownrenderer.NewMarkdownRenderer(logger)
return alertnotificationprocessor.New(templater, renderer, filetemplatestore.NewEmptyStore(), logger)
}
func TestSlackRetry(t *testing.T) {
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.SlackConfig{
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {
actual, _ := notifier.retrier.Check(statusCode, nil)
require.Equal(t, expected, actual, "error on status %d", statusCode)
}
}
func TestSlackRedactedURL(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.SlackConfig{
APIURL: &config.SecretURL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())
}
func TestGettingSlackURLFromFile(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
f, err := os.CreateTemp(t.TempDir(), "slack_test")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(u.String())
require.NoError(t, err, "writing to temp file failed")
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.SlackConfig{
APIURLFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())
}
func TestTrimmingSlackURLFromFile(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
f, err := os.CreateTemp(t.TempDir(), "slack_test_newline")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(u.String() + "\n\n")
require.NoError(t, err, "writing to temp file failed")
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.SlackConfig{
APIURLFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())
}
func TestNotifier_Notify_WithReason(t *testing.T) {
tests := []struct {
name string
statusCode int
responseBody string
expectedReason notify.Reason
expectedErr string
expectedRetry bool
noError bool
}{
{
name: "with a 4xx status code",
statusCode: http.StatusUnauthorized,
expectedReason: notify.ClientErrorReason,
expectedRetry: false,
expectedErr: "unexpected status code 401",
},
{
name: "with a 5xx status code",
statusCode: http.StatusInternalServerError,
expectedReason: notify.ServerErrorReason,
expectedRetry: true,
expectedErr: "unexpected status code 500",
},
{
name: "with a 3xx status code",
statusCode: http.StatusTemporaryRedirect,
expectedReason: notify.DefaultReason,
expectedRetry: false,
expectedErr: "unexpected status code 307",
},
{
name: "with a 1xx status code",
statusCode: http.StatusSwitchingProtocols,
expectedReason: notify.DefaultReason,
expectedRetry: false,
expectedErr: "unexpected status code 101",
},
{
name: "2xx response with invalid JSON",
statusCode: http.StatusOK,
responseBody: `{"not valid json"}`,
expectedReason: notify.ClientErrorReason,
expectedRetry: true,
expectedErr: "could not unmarshal",
},
{
name: "2xx response with a JSON error",
statusCode: http.StatusOK,
responseBody: `{"ok":false,"error":"error_message"}`,
expectedReason: notify.ClientErrorReason,
expectedRetry: false,
expectedErr: "error response from Slack: error_message",
},
{
name: "2xx response with a plaintext error",
statusCode: http.StatusOK,
responseBody: "no_channel",
expectedReason: notify.ClientErrorReason,
expectedRetry: false,
expectedErr: "error response from Slack: no_channel",
},
{
name: "successful JSON response",
statusCode: http.StatusOK,
responseBody: `{"ok":true}`,
noError: true,
},
{
name: "successful plaintext response",
statusCode: http.StatusOK,
responseBody: "ok",
noError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
apiurl, _ := url.Parse("https://slack.com/post.Message")
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.SlackConfig{
NotifierConfig: config.NotifierConfig{},
HTTPConfig: &commoncfg.HTTPClientConfig{},
APIURL: &config.SecretURL{URL: apiurl},
Channel: "channelname",
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
notifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {
resp := httptest.NewRecorder()
if strings.HasPrefix(tt.responseBody, "{") {
resp.Header().Add("Content-Type", "application/json; charset=utf-8")
}
resp.WriteHeader(tt.statusCode)
_, _ = resp.WriteString(tt.responseBody)
return resp.Result(), nil
}
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
alert1 := &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
retry, err := notifier.Notify(ctx, alert1)
require.Equal(t, tt.expectedRetry, retry)
if tt.noError {
require.NoError(t, err)
} else {
var reasonError *notify.ErrorWithReason
require.ErrorAs(t, err, &reasonError)
require.Equal(t, tt.expectedReason, reasonError.Reason)
require.Contains(t, err.Error(), tt.expectedErr)
require.Contains(t, err.Error(), "channelname")
}
})
}
}
func TestSlackTimeout(t *testing.T) {
tests := map[string]struct {
latency time.Duration
timeout time.Duration
wantErr bool
}{
"success": {latency: 100 * time.Millisecond, timeout: 120 * time.Millisecond, wantErr: false},
"error": {latency: 100 * time.Millisecond, timeout: 80 * time.Millisecond, wantErr: true},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
u, _ := url.Parse("https://slack.com/post.Message")
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.SlackConfig{
NotifierConfig: config.NotifierConfig{},
HTTPConfig: &commoncfg.HTTPClientConfig{},
APIURL: &config.SecretURL{URL: u},
Channel: "channelname",
Timeout: tt.timeout,
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
notifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(tt.latency):
resp := httptest.NewRecorder()
resp.Header().Set("Content-Type", "application/json; charset=utf-8")
resp.WriteHeader(http.StatusOK)
_, _ = resp.WriteString(`{"ok":true}`)
return resp.Result(), nil
}
}
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
alert := &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
_, err = notifier.Notify(ctx, alert)
require.Equal(t, tt.wantErr, err != nil)
})
}
}
// setupTestContext creates a context with group key, receiver name, and group labels
// required by the notification processor.
func setupTestContext() context.Context {
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group")
ctx = notify.WithReceiverName(ctx, "slack")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{
"alertname": "TestAlert",
"severity": "critical",
})
return ctx
}
func TestPrepareContent(t *testing.T) {
t.Run("default template uses go text template config for title and body", func(t *testing.T) {
// When alerts have no custom annotation templates (title_template / body_template),
tmpl := test.CreateTmpl(t)
proc := newTestProcessor(tmpl)
notifier := &Notifier{
conf: &config.SlackConfig{
Title: `{{ .CommonLabels.alertname }} ({{ .Status | toUpper }})`,
Text: `{{ range .Alerts }}Alert: {{ .Labels.alertname }} - severity {{ .Labels.severity }}{{ end }}`,
Color: `{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}`,
TitleLink: "https://alertmanager.signoz.com",
},
tmpl: tmpl,
logger: slog.New(slog.DiscardHandler),
processor: proc,
}
ctx := setupTestContext()
alerts := []*types.Alert{
{Alert: model.Alert{
Labels: model.LabelSet{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
}},
}
// Build tmplText the same way Notify does
var err error
data := notify.GetTemplateData(ctx, tmpl, alerts, slog.New(slog.DiscardHandler))
tmplText := notify.TmplText(tmpl, data, &err)
atts, attErr := notifier.prepareContent(ctx, alerts, tmplText)
require.NoError(t, attErr)
require.NoError(t, err)
require.Len(t, atts, 1)
require.Equal(t, "HighCPU (FIRING)", atts[0].Title)
require.Equal(t, "Alert: HighCPU - severity critical", atts[0].Text)
// Color is templated — firing alert should be "danger"
require.Equal(t, "danger", atts[0].Color)
// No BlockKit blocks for default template
require.Nil(t, atts[0].Blocks)
// Default markdownIn when config has none
require.Equal(t, []string{"fallback", "pretext", "text"}, atts[0].MrkdwnIn)
})
t.Run("custom template produces 1+N attachments with per-alert color", func(t *testing.T) {
// When alerts carry custom $variable annotation templates (title_template / body_template)
tmpl := test.CreateTmpl(t)
proc := newTestProcessor(tmpl)
notifier := &Notifier{
conf: &config.SlackConfig{
Title: "default title fallback",
Text: "default text fallback",
TitleLink: "https://alertmanager.signoz.com",
},
tmpl: tmpl,
logger: slog.New(slog.DiscardHandler),
processor: proc,
}
tmplText := func(s string) string { return s }
bodyTemplate := `## $rule_name
**Service:** *$labels.service*
**Instance:** *$labels.instance*
**Region:** *$labels.region*
**Method:** *$labels.http_method*
---
| Metric | Value |
|--------|-------|
| **Current** | *$value* |
| **Threshold** | *$threshold* |
**Status:** $status | **Severity:** $severity`
titleTemplate := "[$status] $rule_name — $labels.service"
ctx := setupTestContext()
firingAlert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical", "service": "api-server", "instance": "i-0abc123", "region": "us-east-1", "http_method": "GET"},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
Annotations: model.LabelSet{
ruletypes.AnnotationTitleTemplate: model.LabelValue(titleTemplate),
ruletypes.AnnotationBodyTemplate: model.LabelValue(bodyTemplate),
"value": "100",
"threshold.value": "200",
},
},
}
resolvedAlert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical", "service": "api-server", "instance": "i-0abc123", "region": "us-east-1", "http_method": "GET"},
StartsAt: time.Now().Add(-2 * time.Hour),
EndsAt: time.Now().Add(-time.Hour),
Annotations: model.LabelSet{
ruletypes.AnnotationTitleTemplate: model.LabelValue(titleTemplate),
ruletypes.AnnotationBodyTemplate: model.LabelValue(bodyTemplate),
"value": "50",
"threshold.value": "200",
},
},
}
atts, err := notifier.prepareContent(ctx, []*types.Alert{firingAlert, resolvedAlert}, tmplText)
require.NoError(t, err)
// 1 title attachment + 2 body attachments (one per alert)
require.Len(t, atts, 3)
// First attachment: title-only, no color, no blocks
require.Equal(t, "[firing] HighCPU — api-server", atts[0].Title)
require.Empty(t, atts[0].Color)
require.Nil(t, atts[0].Blocks)
require.Equal(t, "https://alertmanager.signoz.com", atts[0].TitleLink)
expectedFiringBody := "*HighCPU*\n\n" +
"*Service:* _api-server_\n*Instance:* _i-0abc123_\n*Region:* _us-east-1_\n*Method:* _GET_\n\n" +
"---\n\n" +
"```\nMetric | Value\n----------|------\nCurrent | 100 \nThreshold | 200 \n```\n\n" +
"*Status:* firing | *Severity:* critical\n\n"
expectedResolvedBody := "*HighCPU*\n\n" +
"*Service:* _api-server_\n*Instance:* _i-0abc123_\n*Region:* _us-east-1_\n*Method:* _GET_\n\n" +
"---\n\n" +
"```\nMetric | Value\n----------|------\nCurrent | 50 \nThreshold | 200 \n```\n\n" +
"*Status:* resolved | *Severity:* critical\n\n"
// Second attachment: firing alert body rendered as slack mrkdwn text, red color
require.Nil(t, atts[1].Blocks)
require.Equal(t, "#FF0000", atts[1].Color)
require.Equal(t, []string{"text"}, atts[1].MrkdwnIn)
require.Equal(t, expectedFiringBody, atts[1].Text)
// Third attachment: resolved alert body rendered as slack mrkdwn text, green color
require.Nil(t, atts[2].Blocks)
require.Equal(t, "#00FF00", atts[2].Color)
require.Equal(t, []string{"text"}, atts[2].MrkdwnIn)
require.Equal(t, expectedResolvedBody, atts[2].Text)
})
t.Run("default template with fields and actions", func(t *testing.T) {
// Verifies that addFieldsAndActions (called from Notify after prepareContent)
// correctly populates fields and actions on the attachment from config.
tmpl := test.CreateTmpl(t)
proc := newTestProcessor(tmpl)
short := true
notifier := &Notifier{
conf: &config.SlackConfig{
Title: `{{ .CommonLabels.alertname }}`,
Text: "alert text",
Color: "warning",
Fields: []*config.SlackField{
{Title: "Severity", Value: "critical", Short: &short},
{Title: "Service", Value: "api-server", Short: &short},
},
Actions: []*config.SlackAction{
{Type: "button", Text: "View Alert", URL: "https://alertmanager.signoz.com"},
},
TitleLink: "https://alertmanager.signoz.com",
},
tmpl: tmpl,
logger: slog.New(slog.DiscardHandler),
processor: proc,
}
tmplText := func(s string) string { return s }
ctx := setupTestContext()
alerts := []*types.Alert{
{Alert: model.Alert{
Labels: model.LabelSet{ruletypes.LabelAlertName: "TestAlert"},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
}},
}
atts, err := notifier.prepareContent(ctx, alerts, tmplText)
require.NoError(t, err)
require.Len(t, atts, 1)
// prepareContent does not populate fields/actions — that's done by
// addFieldsAndActions which is called from Notify.
require.Nil(t, atts[0].Fields)
require.Nil(t, atts[0].Actions)
// Simulate what Notify does after prepareContent
notifier.addFieldsAndActions(&atts[0], tmplText)
// Verify fields
require.Len(t, atts[0].Fields, 2)
require.Equal(t, "Severity", atts[0].Fields[0].Title)
require.Equal(t, "critical", atts[0].Fields[0].Value)
require.True(t, *atts[0].Fields[0].Short)
require.Equal(t, "Service", atts[0].Fields[1].Title)
require.Equal(t, "api-server", atts[0].Fields[1].Value)
// Verify actions
require.Len(t, atts[0].Actions, 1)
require.Equal(t, "button", atts[0].Actions[0].Type)
require.Equal(t, "View Alert", atts[0].Actions[0].Text)
require.Equal(t, "https://alertmanager.signoz.com", atts[0].Actions[0].URL)
})
}
func TestSlackMessageField(t *testing.T) {
// 1. Setup a fake Slack server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
// 2. VERIFY: Top-level text exists
if body["text"] != "My Top Level Message" {
t.Errorf("Expected top-level 'text' to be 'My Top Level Message', got %v", body["text"])
}
// 3. VERIFY: Old attachments still exist
attachments, ok := body["attachments"].([]any)
if !ok || len(attachments) == 0 {
t.Errorf("Expected attachments to exist")
} else {
first := attachments[0].(map[string]any)
if first["title"] != "Old Attachment Title" {
t.Errorf("Expected attachment title 'Old Attachment Title', got %v", first["title"])
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok": true}`))
}))
defer server.Close()
// 4. Configure Notifier with BOTH new and old fields
u, _ := url.Parse(server.URL)
conf := &config.SlackConfig{
APIURL: &config.SecretURL{URL: u},
MessageText: "My Top Level Message", // Your NEW field
Title: "Old Attachment Title", // An OLD field
Channel: "#test-channel",
HTTPConfig: &commoncfg.HTTPClientConfig{},
}
tmpl, err := template.FromGlobs([]string{})
if err != nil {
t.Fatal(err)
}
tmpl.ExternalURL = u
logger := slog.New(slog.DiscardHandler)
notifier, err := New(conf, tmpl, logger, newTestProcessor(tmpl))
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group-key")
if _, err := notifier.Notify(ctx); err != nil {
t.Fatal("Notify failed:", err)
}
}

View File

@@ -1,192 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package webhook
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"net/http"
"os"
"strings"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
const (
Integration = "webhook"
)
// Notifier implements a Notifier for generic webhooks.
type Notifier struct {
conf *config.WebhookConfig
tmpl *template.Template
logger *slog.Logger
client *http.Client
retrier *notify.Retrier
processor alertmanagertypes.NotificationProcessor
}
// New returns a new Webhook.
func New(conf *config.WebhookConfig, t *template.Template, l *slog.Logger, proc alertmanagertypes.NotificationProcessor, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := notify.NewClientWithTracing(*conf.HTTPConfig, Integration, httpOpts...)
if err != nil {
return nil, err
}
return &Notifier{
conf: conf,
tmpl: t,
logger: l,
client: client,
processor: proc,
// Webhooks are assumed to respond with 2xx response codes on a successful
// request and 5xx response codes are assumed to be recoverable.
retrier: &notify.Retrier{},
}, nil
}
// Message defines the JSON object send to webhook endpoints.
type Message struct {
*template.Data
// The protocol version.
Version string `json:"version"`
GroupKey string `json:"groupKey"`
TruncatedAlerts uint64 `json:"truncatedAlerts"`
}
func truncateAlerts(maxAlerts uint64, alerts []*types.Alert) ([]*types.Alert, uint64) {
if maxAlerts != 0 && uint64(len(alerts)) > maxAlerts {
return alerts[:maxAlerts], uint64(len(alerts)) - maxAlerts
}
return alerts, 0
}
// templateAlerts extracts custom templates from alert annotations, processes them,
// and updates each alert's annotations with the rendered title and body
// the idea is to send the templated annotations for title and body templates to the webhook endpoint.
func (n *Notifier) templateAlerts(ctx context.Context, alerts []*types.Alert) error {
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(alerts)
result, err := n.processor.ProcessAlertNotification(ctx, alertmanagertypes.NotificationProcessorInput{
TitleTemplate: customTitle,
BodyTemplate: customBody,
DefaultTitleTemplate: alertmanagertypes.NoOpTemplateString,
DefaultBodyTemplate: alertmanagertypes.NoOpTemplateString,
}, alerts, markdownrenderer.MarkdownFormatNoop)
if err != nil {
return err
}
for i, alert := range alerts {
if alert.Annotations == nil {
continue
}
// Update title_template annotation with rendered title, only if key exists and result is non-blank
if _, ok := alert.Annotations[ruletypes.AnnotationTitleTemplate]; ok && result.Title != "" {
alert.Annotations[ruletypes.AnnotationTitleTemplate] = model.LabelValue(result.Title)
}
// Update body_template annotation with rendered body, only if key exists and result is non-blank
if _, ok := alert.Annotations[ruletypes.AnnotationBodyTemplate]; ok && i < len(result.Body) && result.Body[i] != "" {
alert.Annotations[ruletypes.AnnotationBodyTemplate] = model.LabelValue(result.Body[i])
}
}
return nil
}
// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) {
alerts, numTruncated := truncateAlerts(n.conf.MaxAlerts, alerts)
// template alerts before preparing the notification data
if err := n.templateAlerts(ctx, alerts); err != nil {
n.logger.ErrorContext(ctx, "failed to prepare notification content", errors.Attr(err))
}
data := notify.GetTemplateData(ctx, n.tmpl, alerts, n.logger)
groupKey, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
logger := n.logger.With(slog.Any("group_key", groupKey))
logger.DebugContext(ctx, "extracted group key")
msg := &Message{
Version: "4",
Data: data,
GroupKey: groupKey.String(),
TruncatedAlerts: numTruncated,
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return false, err
}
var url string
var tmplErr error
tmpl := notify.TmplText(n.tmpl, data, &tmplErr)
if n.conf.URL != "" {
url = tmpl(string(n.conf.URL))
} else {
content, err := os.ReadFile(n.conf.URLFile)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "read url_file")
}
url = tmpl(strings.TrimSpace(string(content)))
}
if tmplErr != nil {
return false, errors.NewInternalf(errors.CodeInternal, "failed to template webhook URL: %v", tmplErr)
}
if url == "" {
return false, errors.NewInternalf(errors.CodeInternal, "webhook URL is empty after templating")
}
if n.conf.Timeout > 0 {
postCtx, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, errors.NewInternalf(errors.CodeTimeout, "configured webhook timeout reached (%s)", n.conf.Timeout))
defer cancel()
ctx = postCtx
}
resp, err := notify.PostJSON(ctx, n.client, url, &buf) //nolint:bodyclose
if err != nil {
if ctx.Err() != nil {
err = errors.NewInternalf(errors.CodeInternal, "failed to post JSON to webhook: %v", context.Cause(ctx))
}
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)
shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
if err != nil {
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
return shouldRetry, err
}

View File

@@ -1,342 +0,0 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package webhook
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/alertmanager/alertnotificationprocessor"
"github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/notify/test"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
func newTestProcessor(tmpl *template.Template) alertmanagertypes.NotificationProcessor {
logger := slog.Default()
templater := alertmanagertemplate.New(tmpl, logger)
renderer := markdownrenderer.NewMarkdownRenderer(logger)
return alertnotificationprocessor.New(templater, renderer, filetemplatestore.NewEmptyStore(), logger)
}
func TestWebhookRetry(t *testing.T) {
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.WebhookConfig{
URL: config.SecretTemplateURL("http://example.com"),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
if err != nil {
require.NoError(t, err)
}
t.Run("test retry status code", func(t *testing.T) {
for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {
actual, _ := notifier.retrier.Check(statusCode, nil)
require.Equal(t, expected, actual, "error on status %d", statusCode)
}
})
t.Run("test retry error details", func(t *testing.T) {
for _, tc := range []struct {
status int
body io.Reader
exp string
}{
{
status: http.StatusBadRequest,
body: bytes.NewBuffer([]byte(
`{"status":"invalid event"}`,
)),
exp: fmt.Sprintf(`unexpected status code %d: {"status":"invalid event"}`, http.StatusBadRequest),
},
{
status: http.StatusBadRequest,
exp: fmt.Sprintf(`unexpected status code %d`, http.StatusBadRequest),
},
} {
t.Run("", func(t *testing.T) {
_, err = notifier.retrier.Check(tc.status, tc.body)
require.Equal(t, tc.exp, err.Error())
})
}
})
}
func TestWebhookTruncateAlerts(t *testing.T) {
alerts := make([]*types.Alert, 10)
truncatedAlerts, numTruncated := truncateAlerts(0, alerts)
require.Len(t, truncatedAlerts, 10)
require.EqualValues(t, 0, numTruncated)
truncatedAlerts, numTruncated = truncateAlerts(4, alerts)
require.Len(t, truncatedAlerts, 4)
require.EqualValues(t, 6, numTruncated)
truncatedAlerts, numTruncated = truncateAlerts(100, alerts)
require.Len(t, truncatedAlerts, 10)
require.EqualValues(t, 0, numTruncated)
}
func TestWebhookRedactedURL(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
secret := "secret"
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.WebhookConfig{
URL: config.SecretTemplateURL(u.String()),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)
}
func TestWebhookReadingURLFromFile(t *testing.T) {
ctx, u, fn := test.GetContextWithCancelingURL()
defer fn()
f, err := os.CreateTemp(t.TempDir(), "webhook_url")
require.NoError(t, err, "creating temp file failed")
_, err = f.WriteString(u.String() + "\n")
require.NoError(t, err, "writing to temp file failed")
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.WebhookConfig{
URLFile: f.Name(),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())
}
func TestWebhookURLTemplating(t *testing.T) {
var calledURL string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calledURL = r.URL.Path
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
tests := []struct {
name string
url string
groupLabels model.LabelSet
alertLabels model.LabelSet
expectError bool
expectedErrMsg string
expectedPath string
}{
{
name: "templating with alert labels",
url: srv.URL + "/{{ .GroupLabels.alertname }}/{{ .CommonLabels.severity }}",
groupLabels: model.LabelSet{"alertname": "TestAlert"},
alertLabels: model.LabelSet{"alertname": "TestAlert", "severity": "critical"},
expectError: false,
expectedPath: "/TestAlert/critical",
},
{
name: "invalid template field",
url: srv.URL + "/{{ .InvalidField }}",
groupLabels: model.LabelSet{"alertname": "TestAlert"},
alertLabels: model.LabelSet{"alertname": "TestAlert"},
expectError: true,
expectedErrMsg: "failed to template webhook URL",
},
{
name: "template renders to empty string",
url: "{{ if .CommonLabels.nonexistent }}http://example.com{{ end }}",
groupLabels: model.LabelSet{"alertname": "TestAlert"},
alertLabels: model.LabelSet{"alertname": "TestAlert"},
expectError: true,
expectedErrMsg: "webhook URL is empty after templating",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
calledURL = "" // Reset for each test
tmpl := test.CreateTmpl(t)
notifier, err := New(
&config.WebhookConfig{
URL: config.SecretTemplateURL(tc.url),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
promslog.NewNopLogger(),
newTestProcessor(tmpl),
)
require.NoError(t, err)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group")
if tc.groupLabels != nil {
ctx = notify.WithGroupLabels(ctx, tc.groupLabels)
}
alerts := []*types.Alert{
{
Alert: model.Alert{
Labels: tc.alertLabels,
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
},
}
_, err = notifier.Notify(ctx, alerts...)
if tc.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErrMsg)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectedPath, calledURL)
}
})
}
}
func TestTemplateAlerts(t *testing.T) {
tmpl := test.CreateTmpl(t)
proc := newTestProcessor(tmpl)
notifier, err := New(
&config.WebhookConfig{
URL: config.SecretTemplateURL("http://example.com"),
HTTPConfig: &commoncfg.HTTPClientConfig{},
},
tmpl,
slog.Default(),
proc,
)
require.NoError(t, err)
t.Run("annotations are updated with custom title and body templates", func(t *testing.T) {
alerts := []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{
"alertname": "TestAlert",
"severity": "critical",
},
Annotations: model.LabelSet{
ruletypes.AnnotationTitleTemplate: "Alert: $labels.alertname",
ruletypes.AnnotationBodyTemplate: "Severity is $labels.severity",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
},
{
Alert: model.Alert{
Labels: model.LabelSet{
"alertname": "TestAlert",
"severity": "warning",
},
Annotations: model.LabelSet{
ruletypes.AnnotationTitleTemplate: "Alert: $labels.alertname",
ruletypes.AnnotationBodyTemplate: "Severity is $labels.severity",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
},
}
ctx := context.Background()
err := notifier.templateAlerts(ctx, alerts)
require.NoError(t, err)
// Both alerts should have their title_template updated to the rendered title
require.Equal(t, model.LabelValue("Alert: TestAlert"), alerts[0].Annotations[ruletypes.AnnotationTitleTemplate])
require.Equal(t, model.LabelValue("Alert: TestAlert"), alerts[1].Annotations[ruletypes.AnnotationTitleTemplate])
// Each alert should have its own body_template based on its labels
require.Equal(t, model.LabelValue("Severity is critical"), alerts[0].Annotations[ruletypes.AnnotationBodyTemplate])
require.Equal(t, model.LabelValue("Severity is warning"), alerts[1].Annotations[ruletypes.AnnotationBodyTemplate])
})
t.Run("annotations not updated when template keys are absent", func(t *testing.T) {
alerts := []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{
"alertname": "NoTemplateAlert",
},
Annotations: model.LabelSet{
"summary": "keep this",
"description": "keep this too",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
},
}
ctx := context.Background()
err := notifier.templateAlerts(ctx, alerts)
require.NoError(t, err)
// title_template and body_template keys should NOT be added
_, hasTitleTemplate := alerts[0].Annotations[ruletypes.AnnotationTitleTemplate]
_, hasBodyTemplate := alerts[0].Annotations[ruletypes.AnnotationBodyTemplate]
require.False(t, hasTitleTemplate, "title_template should not be added when absent")
require.False(t, hasBodyTemplate, "body_template should not be added when absent")
// Existing annotations should remain untouched
require.Equal(t, model.LabelValue("keep this"), alerts[0].Annotations["summary"])
require.Equal(t, model.LabelValue("keep this too"), alerts[0].Annotations["description"])
})
}

View File

@@ -28,9 +28,6 @@ type Config struct {
// Configuration for the notification log.
NFLog NFLogConfig `mapstructure:"nflog"`
// EmailTemplatesDirectory is the directory containing email layout templates (.gotmpl files).
EmailTemplatesDirectory string `mapstructure:"email_templates_directory"`
}
type AlertsConfig struct {
@@ -103,6 +100,5 @@ func NewConfig() Config {
MaintenanceInterval: 15 * time.Minute,
Retention: 120 * time.Hour,
},
EmailTemplatesDirectory: "/root/templates/email",
}
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/prometheus/alertmanager/types"
"golang.org/x/sync/errgroup"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/featurecontrol"
"github.com/prometheus/alertmanager/inhibit"
@@ -24,13 +23,9 @@ import (
"github.com/prometheus/common/model"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/alertmanager/alertnotificationprocessor"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
)
var (
@@ -71,8 +66,6 @@ type Server struct {
pipelineBuilder *notify.PipelineBuilder
marker *alertmanagertypes.MemMarker
tmpl *template.Template
processor alertmanagertypes.NotificationProcessor
emailTemplateStore emailtypes.TemplateStore
wg sync.WaitGroup
stopc chan struct{}
notificationManager nfmanager.NotificationManager
@@ -205,12 +198,6 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
server.pipelineBuilder = notify.NewPipelineBuilder(signozRegisterer, featurecontrol.NoopFlags{})
server.dispatcherMetrics = NewDispatcherMetrics(false, signozRegisterer)
emailTemplateStore, storeErr := filetemplatestore.NewStore(ctx, srvConfig.EmailTemplatesDirectory, emailtypes.Templates, server.logger)
if storeErr != nil {
server.logger.ErrorContext(ctx, "failed to create alert email template store, using empty store", errors.Attr(storeErr))
emailTemplateStore = filetemplatestore.NewEmptyStore()
}
server.emailTemplateStore = emailTemplateStore
return server, nil
}
@@ -247,11 +234,6 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
server.tmpl.ExternalURL = server.srvConfig.ExternalURL
// Construct the alert notification processor
templater := alertmanagertemplate.New(server.tmpl, server.logger)
renderer := markdownrenderer.NewMarkdownRenderer(server.logger)
server.processor = alertnotificationprocessor.New(templater, renderer, server.emailTemplateStore, server.logger)
// Build the routing tree and record which receivers are used.
routes := dispatch.NewRoute(config.Route, nil)
activeReceivers := make(map[string]struct{})
@@ -268,7 +250,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", slog.String("receiver", rcv.Name))
continue
}
integrations, err := alertmanagernotify.NewReceiverIntegrations(rcv, server.tmpl, server.logger, server.processor)
integrations, err := alertmanagernotify.NewReceiverIntegrations(rcv, server.tmpl, server.logger)
if err != nil {
return err
}
@@ -344,7 +326,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
testAlert := alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now())
return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, server.processor, testAlert.Labels, testAlert)
return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, testAlert.Labels, testAlert)
}
func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmanagertypes.PostableAlert][]string, config *alertmanagertypes.NotificationConfig) error {
@@ -427,7 +409,6 @@ func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmana
server.alertmanagerConfig,
server.tmpl,
server.logger,
server.processor,
group.groupLabels,
group.alerts...,
)

View File

@@ -1,292 +0,0 @@
package alertmanagertemplate
import (
"context"
"log/slog"
"sort"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)
// AlertManagerTemplater processes alert notification templates.
type AlertManagerTemplater interface {
// ProcessTemplates expands the title and body templates from input
// against the provided alerts and returns the expanded templates.
ProcessTemplates(ctx context.Context, input TemplateInput, alerts []*types.Alert) (*ExpandedTemplates, error)
// BuildNotificationTemplateData builds the NotificationTemplateData from context and alerts.
// This exposes the structured alert data that gets used in the notification templates.
BuildNotificationTemplateData(ctx context.Context, alerts []*types.Alert) *NotificationTemplateData
}
type alertManagerTemplater struct {
tmpl *template.Template
logger *slog.Logger
}
func New(tmpl *template.Template, logger *slog.Logger) AlertManagerTemplater {
return &alertManagerTemplater{tmpl: tmpl, logger: logger}
}
// ProcessTemplates expands the title and body templates from input
// against the provided alerts and returns the expanded templates.
func (at *alertManagerTemplater) ProcessTemplates(
ctx context.Context,
input TemplateInput,
alerts []*types.Alert,
) (*ExpandedTemplates, error) {
ntd := at.buildNotificationTemplateData(ctx, alerts)
missingVars := make(map[string]bool)
title, titleMissingVars, err := at.expandTitle(input.TitleTemplate, ntd)
if err != nil {
return nil, err
}
// if title template results in empty string, use default template
// this happens for rules where custom title annotation was not set
if title == "" && input.DefaultTitleTemplate != "" {
title, err = at.expandDefaultTemplate(ctx, input.DefaultTitleTemplate, alerts)
if err != nil {
return nil, err
}
} else {
mergeMissingVars(missingVars, titleMissingVars)
}
// isDefaultTemplated tracks whether the body is templated using default templates
isDefaultTemplated := false
body, bodyMissingVars, err := at.expandBody(input.BodyTemplate, ntd)
if err != nil {
return nil, err
}
// if body template results in nil, use default template
// this happens for rules where custom body annotation was not set
if body == nil {
isDefaultTemplated = true
defaultBody, err := at.expandDefaultTemplate(ctx, input.DefaultBodyTemplate, alerts)
if err != nil {
return nil, err
}
body = []string{defaultBody} // default template combines all alerts message into a single body
} else {
mergeMissingVars(missingVars, bodyMissingVars)
}
// convert the internal map to a sorted slice for returning missing variables
missingVarsList := make([]string, 0, len(missingVars))
for k := range missingVars {
missingVarsList = append(missingVarsList, k)
}
sort.Strings(missingVarsList)
return &ExpandedTemplates{
Title: title,
Body: body,
MissingVars: missingVarsList,
IsDefaultTemplatedBody: isDefaultTemplated,
}, nil
}
// BuildNotificationTemplateData builds the NotificationTemplateData from context and alerts.
func (at *alertManagerTemplater) BuildNotificationTemplateData(
ctx context.Context,
alerts []*types.Alert,
) *NotificationTemplateData {
return at.buildNotificationTemplateData(ctx, alerts)
}
// expandDefaultTemplate uses go-template to expand the default template.
func (at *alertManagerTemplater) expandDefaultTemplate(
ctx context.Context,
tmplStr string,
alerts []*types.Alert,
) (string, error) {
// if even the default template is empty, return empty string
// this is possible if user added channel with blank template
if tmplStr == "" {
at.logger.WarnContext(ctx, "default template is empty")
return "", nil
}
data := notify.GetTemplateData(ctx, at.tmpl, alerts, at.logger)
result, err := at.tmpl.ExecuteTextString(tmplStr, data)
if err != nil {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute default template: %s", err.Error())
}
return result, nil
}
// mergeMissingVars adds all keys from src into dst.
func mergeMissingVars(dst, src map[string]bool) {
for k := range src {
dst[k] = true
}
}
// expandTitle expands the title template. Returns empty string if the template is empty.
func (at *alertManagerTemplater) expandTitle(
titleTemplate string,
ntd *NotificationTemplateData,
) (string, map[string]bool, error) {
if titleTemplate == "" {
return "", nil, nil
}
processRes, err := PreProcessTemplateAndData(titleTemplate, ntd)
if err != nil {
return "", nil, err
}
result, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
if err != nil {
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom title template: %s", err.Error())
}
return strings.TrimSpace(result), processRes.UnknownVars, nil
}
// expandBody expands the body template for each individual alert. Returns nil if the template is empty.
func (at *alertManagerTemplater) expandBody(
bodyTemplate string,
ntd *NotificationTemplateData,
) ([]string, map[string]bool, error) {
if bodyTemplate == "" {
return nil, nil, nil
}
var sb []string
missingVars := make(map[string]bool)
for i := range ntd.Alerts {
processRes, err := PreProcessTemplateAndData(bodyTemplate, &ntd.Alerts[i])
if err != nil {
return nil, nil, err
}
part, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
if err != nil {
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom body template: %s", err.Error())
}
// add unknown variables and templated text to the result
for k := range processRes.UnknownVars {
missingVars[k] = true
}
if strings.TrimSpace(part) != "" {
sb = append(sb, strings.TrimSpace(part))
}
}
return sb, missingVars, nil
}
// buildNotificationTemplateData creates the NotificationTemplateData using
// info from context and the raw alerts.
func (at *alertManagerTemplater) buildNotificationTemplateData(
ctx context.Context,
alerts []*types.Alert,
) *NotificationTemplateData {
// extract the required data from the context
receiver, ok := notify.ReceiverName(ctx)
if !ok {
at.logger.WarnContext(ctx, "missing receiver name in context")
}
groupLabels, ok := notify.GroupLabels(ctx)
if !ok {
at.logger.WarnContext(ctx, "missing group labels in context")
}
// extract the external URL from the template
externalURL := ""
if at.tmpl.ExternalURL != nil {
externalURL = at.tmpl.ExternalURL.String()
}
commonAnnotations := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
commonLabels := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
// aggregate labels and annotations from all alerts
labels := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
annotations := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
// build the alert data slice
alertDataSlice := make([]AlertData, 0, len(alerts))
for _, a := range alerts {
ad := buildAlertData(a, receiver)
alertDataSlice = append(alertDataSlice, ad)
}
// count the number of firing and resolved alerts
var firing, resolved int
for _, ad := range alertDataSlice {
if ad.IsFiring {
firing++
} else if ad.IsResolved {
resolved++
}
}
// extract the rule-level convenience fields from common labels
alertName := commonLabels[ruletypes.LabelAlertName]
ruleID := commonLabels[ruletypes.LabelRuleID]
ruleLink := commonLabels[ruletypes.LabelRuleSource]
// build the group labels
gl := make(template.KV, len(groupLabels))
for k, v := range groupLabels {
gl[string(k)] = string(v)
}
// build the notification template data
return &NotificationTemplateData{
Receiver: receiver,
Status: string(types.Alerts(alerts...).Status()),
AlertName: alertName,
RuleID: ruleID,
RuleLink: ruleLink,
TotalFiring: firing,
TotalResolved: resolved,
Alerts: alertDataSlice,
GroupLabels: gl,
CommonLabels: commonLabels,
CommonAnnotations: commonAnnotations,
ExternalURL: externalURL,
Labels: labels,
Annotations: annotations,
}
}
// buildAlertData converts a single *types.Alert into an AlertData.
func buildAlertData(a *types.Alert, receiver string) AlertData {
labels := make(template.KV, len(a.Labels))
for k, v := range a.Labels {
labels[string(k)] = string(v)
}
annotations := make(template.KV, len(a.Annotations))
for k, v := range a.Annotations {
annotations[string(k)] = string(v)
}
return AlertData{
Receiver: receiver,
Status: string(a.Status()),
Labels: labels,
Annotations: annotations,
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,
GeneratorURL: a.GeneratorURL,
Fingerprint: a.Fingerprint().String(),
AlertName: labels[ruletypes.LabelAlertName],
RuleID: labels[ruletypes.LabelRuleID],
RuleLink: labels[ruletypes.LabelRuleSource],
Severity: labels[ruletypes.LabelSeverityName],
LogLink: annotations[ruletypes.AnnotationRelatedLogs],
TraceLink: annotations[ruletypes.AnnotationRelatedTraces],
Value: annotations[ruletypes.AnnotationValue],
Threshold: annotations[ruletypes.AnnotationThresholdValue],
CompareOp: annotations[ruletypes.AnnotationCompareOp],
MatchType: annotations[ruletypes.AnnotationMatchType],
IsFiring: a.Status() == model.AlertFiring,
IsResolved: a.Status() == model.AlertResolved,
IsMissingData: labels[ruletypes.LabelNoData] == "true",
IsRecovering: labels[ruletypes.LabelIsRecovering] == "true",
}
}

View File

@@ -1,283 +0,0 @@
package alertmanagertemplate
import (
"context"
"log/slog"
"sort"
"testing"
"time"
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
)
// testSetup returns an AlertTemplater and a context pre-populated with group key,
// receiver name, and group labels for use in tests.
func testSetup(t *testing.T) (AlertManagerTemplater, context.Context) {
t.Helper()
tmpl := test.CreateTmpl(t)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group")
ctx = notify.WithReceiverName(ctx, "slack")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": "TestAlert", "severity": "critical"})
logger := slog.New(slog.DiscardHandler)
return New(tmpl, logger), ctx
}
func createAlert(labels, annotations map[string]string, isFiring bool) *types.Alert {
ls := model.LabelSet{}
for k, v := range labels {
ls[model.LabelName(k)] = model.LabelValue(v)
}
ann := model.LabelSet{}
for k, v := range annotations {
ann[model.LabelName(k)] = model.LabelValue(v)
}
startsAt := time.Now()
var endsAt time.Time
if isFiring {
endsAt = startsAt.Add(time.Hour)
} else {
startsAt = startsAt.Add(-2 * time.Hour)
endsAt = startsAt.Add(-time.Hour)
}
return &types.Alert{Alert: model.Alert{Labels: ls, Annotations: ann, StartsAt: startsAt, EndsAt: endsAt}}
}
func TestExpandTemplates(t *testing.T) {
at, ctx := testSetup(t)
tests := []struct {
name string
alerts []*types.Alert
input TemplateInput
wantTitle string
wantBody []string
wantMissingVars []string
errorContains string
wantIsDefaultBody bool
}{
{
// High request throughput on a service — service is a custom label.
// $labels.service extracts the label value; $annotations.description pulls the annotation.
name: "new template: high request throughput for a service",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "HighRequestThroughput",
ruletypes.LabelSeverityName: "warning",
"service": "payment-service",
},
map[string]string{"description": "Request rate exceeded 10k/s"},
true,
),
},
input: TemplateInput{
TitleTemplate: "High request throughput for $service",
BodyTemplate: `The service $service is getting high request. Please investigate.
Severity: $severity
Status: $status
Service: $service
Description: $description`,
},
wantTitle: "High request throughput for payment-service",
wantBody: []string{`The service payment-service is getting high request. Please investigate.
Severity: warning
Status: firing
Service: payment-service
Description: Request rate exceeded 10k/s`},
wantIsDefaultBody: false,
},
{
// Disk usage alert using old Go template syntax throughout.
// No custom templates — both title and body use the default fallback path.
name: "old template: disk usage high on database host",
alerts: []*types.Alert{
createAlert(
map[string]string{ruletypes.LabelAlertName: "DiskUsageHigh",
ruletypes.LabelSeverityName: "critical",
"instance": "db-primary-01",
},
map[string]string{
"summary": "Disk usage high on database host",
"description": "Disk usage is high on the database host",
"related_logs": "https://logs.example.com/search?q=DiskUsageHigh",
"related_traces": "https://traces.example.com/search?q=DiskUsageHigh",
},
true,
),
},
input: TemplateInput{
DefaultTitleTemplate: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
{{- if gt (len .CommonLabels) (len .GroupLabels) -}}
{{" "}}(
{{- with .CommonLabels.Remove .GroupLabels.Names }}
{{- range $index, $label := .SortedPairs -}}
{{ if $index }}, {{ end }}
{{- $label.Name }}="{{ $label.Value -}}"
{{- end }}
{{- end -}}
)
{{- end }}`,
DefaultBodyTemplate: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
*Summary:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
*Details:*
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
{{ end }}
{{ end }}`,
},
wantTitle: "[FIRING:1] DiskUsageHigh for (instance=\"db-primary-01\")",
wantBody: []string{`*Alert:* DiskUsageHigh - critical
*Summary:* Disk usage high on database host
*Description:* Disk usage is high on the database host
*RelatedLogs:* View in <https://logs.example.com/search?q=DiskUsageHigh|logs explorer>
*RelatedTraces:* View in <https://traces.example.com/search?q=DiskUsageHigh|traces explorer>
*Details:*
• *alertname:* DiskUsageHigh
• *instance:* db-primary-01
• *severity:* critical
`},
wantIsDefaultBody: true,
},
{
// Pod crash loop on multiple pods — body is expanded once per alert
// and joined with "\n\n", with the pod name pulled from labels.
name: "new template: pod crash loop on multiple pods, body per-alert",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-1"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-2"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-3"}, nil, true),
},
input: TemplateInput{
TitleTemplate: "$rule_name: $total_firing pods affected",
BodyTemplate: "$labels.pod is crash looping",
},
wantTitle: "PodCrashLoop: 3 pods affected",
wantBody: []string{"api-worker-1 is crash looping", "api-worker-2 is crash looping", "api-worker-3 is crash looping"},
wantIsDefaultBody: false,
},
{
// Incident partially resolved — one service still down, one recovered.
// Title shows the aggregate counts; body shows per-service status.
name: "new template: service degradation with mixed firing and resolved alerts",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "auth-service"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "payment-service"}, nil, false),
},
input: TemplateInput{
TitleTemplate: "$total_firing firing, $total_resolved resolved",
BodyTemplate: "$labels.service ($status)",
},
wantTitle: "1 firing, 1 resolved",
wantBody: []string{"auth-service (firing)", "payment-service (resolved)"},
wantIsDefaultBody: false,
},
{
// $environment is not a known AlertData or NotificationTemplateData field,
// so it lands in MissingVars and renders as "<no value>" in the output.
name: "missing vars: unknown $environment variable in title",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
},
input: TemplateInput{
TitleTemplate: "[$environment] $rule_name",
},
wantTitle: "[<no value>] HighCPU",
wantMissingVars: []string{"environment"},
wantIsDefaultBody: true,
},
{
// $runbook_url is not a known field — someone tried to embed a runbook link
// directly as a variable instead of via annotations.
name: "missing vars: unknown $runbook_url variable in body",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "PodOOMKilled", ruletypes.LabelSeverityName: "warning"}, nil, true),
},
input: TemplateInput{
BodyTemplate: "$rule_name: see runbook at $runbook_url",
},
wantBody: []string{"PodOOMKilled: see runbook at <no value>"},
wantMissingVars: []string{"runbook_url"},
},
{
// Both title and body use unknown variables; MissingVars is the union of both.
name: "missing vars: unknown variables in both title and body",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighMemory", ruletypes.LabelSeverityName: "critical"}, nil, true),
},
input: TemplateInput{
TitleTemplate: "[$environment] $rule_name and [{{ $service }}]",
BodyTemplate: "$rule_name: see runbook at $runbook_url",
},
wantTitle: "[<no value>] HighMemory and [<no value>]",
wantBody: []string{"HighMemory: see runbook at <no value>"},
wantMissingVars: []string{"environment", "runbook_url", "service"},
},
{
// Custom title template that expands to only whitespace triggers the fallback,
// so the default title template is used instead.
name: "fallback: whitespace-only custom title falls back to default",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, false),
},
input: TemplateInput{
TitleTemplate: " ",
DefaultTitleTemplate: "{{ .CommonLabels.alertname }} ({{ .Status | toUpper }})",
DefaultBodyTemplate: "Runbook: https://runbook.example.com",
},
wantTitle: "HighCPU (RESOLVED)",
wantBody: []string{"Runbook: https://runbook.example.com"},
wantIsDefaultBody: true,
},
{
name: "using non-existing function in template",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
},
input: TemplateInput{
TitleTemplate: "$rule_name ({{$severity | toUpperAndTrim}}) for $alertname",
},
errorContains: "function \"toUpperAndTrim\" not defined",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := at.ProcessTemplates(ctx, tc.input, tc.alerts)
if tc.errorContains != "" {
require.ErrorContains(t, err, tc.errorContains)
return
}
require.NoError(t, err)
if tc.wantTitle != "" {
require.Equal(t, tc.wantTitle, got.Title)
}
if tc.wantBody != nil {
require.Equal(t, tc.wantBody, got.Body)
}
require.Equal(t, tc.wantIsDefaultBody, got.IsDefaultTemplatedBody)
if len(tc.wantMissingVars) == 0 {
require.Empty(t, got.MissingVars)
} else {
sort.Strings(tc.wantMissingVars)
require.Equal(t, tc.wantMissingVars, got.MissingVars)
}
})
}
}

View File

@@ -1,271 +0,0 @@
package alertmanagertemplate
import (
"fmt"
"reflect"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/go-viper/mapstructure/v2"
)
// fieldMapping represents a mapping from a JSON tag name to its struct field name.
type fieldMapping struct {
VarName string // JSON tag name (e.g., "receiver", "rule_name")
FieldName string // Struct field name (e.g., "Receiver", "AlertName")
}
// extractFieldMappings uses reflection to extract field mappings from a struct.
func extractFieldMappings(data any) []fieldMapping {
val := reflect.ValueOf(data)
// Handle pointer types
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil
}
val = val.Elem()
}
// return nil if the given data is not a struct
if val.Kind() != reflect.Struct {
return nil
}
typ := val.Type()
var mappings []fieldMapping
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Get JSON tag name
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Extract the name part (before any comma options like omitempty)
varName := strings.Split(jsonTag, ",")[0]
if varName == "" {
continue
}
varFieldName := field.Tag.Get("mapstructure")
if varFieldName == "" {
varFieldName = field.Name
}
// Skip complex types: slices and interfaces
kind := field.Type.Kind()
if kind == reflect.Slice || kind == reflect.Interface {
continue
}
// For struct types, we skip all but with few exceptions like time.Time
if kind == reflect.Struct {
// Allow time.Time which is commonly used
if field.Type.String() != "time.Time" {
continue
}
}
mappings = append(mappings, fieldMapping{
VarName: varName,
FieldName: varFieldName,
})
}
return mappings
}
// prepareVariableName prepares the variable name to be used in go-text-template
// it replaces every unwanted character like dots, spaces, etc. with an underscore
// for example, "http.request.method" becomes "http_request_method".
func prepareVariableName(key string) string {
var b strings.Builder
b.Grow(len(key))
for i, r := range key {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r == '_': // valid variable name characters
b.WriteRune(r)
case r >= '0' && r <= '9':
if b.Len() == 0 {
// leading digit — replace with underscore
b.WriteByte('_')
} else {
b.WriteRune(r)
}
default:
// dots, hyphens, spaces, etc. → underscore
b.WriteByte('_')
}
_ = i
}
return b.String()
}
// extractNestedFieldsDefinitions adds the labels and annotations keys from the data struct to the template variable definitions
// it takes the known data struct and extracts the labels and annotations maps and adds their keys to template variable definitions to be used in the template.
func extractNestedFieldsDefinitions(data any) map[string]string {
variables := make(map[string]string)
addLabelsAndAnnotations := func(labels, annotations map[string]string) {
for k := range annotations {
variables[prepareVariableName(k)] = fmt.Sprintf("index .annotations \"%s\"", k)
}
for k := range labels {
variables[prepareVariableName(k)] = fmt.Sprintf("index .labels \"%s\"", k)
}
}
switch data := data.(type) {
case *NotificationTemplateData:
addLabelsAndAnnotations(data.Labels, data.Annotations)
case *AlertData:
addLabelsAndAnnotations(data.Labels, data.Annotations)
default:
return variables
}
return variables
}
// prepareDataForTemplating prepares the data for templating by adding the labels and annotations values to the resulting map
// so they can be accessed directly from root level, the predefined values take precedence over the labels and annotations values
// for example, if labels have a value called rule_name, which collides with the rule_name field in the data struct, the value from the data struct will take precedence.
func prepareDataForTemplating(data any) (map[string]interface{}, error) {
var result map[string]interface{}
if err := mapstructure.Decode(data, &result); err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to prepare data for templating")
}
addLabelsAndAnnotationsValues := func(labels, annotations map[string]string) {
for k, v := range labels {
k = prepareVariableName(k)
if _, ok := result[k]; !ok {
result[k] = v
}
}
for k, v := range annotations {
k = prepareVariableName(k)
if _, ok := result[k]; !ok {
result[k] = v
}
}
}
switch data := data.(type) {
case *NotificationTemplateData:
addLabelsAndAnnotationsValues(data.Labels, data.Annotations)
case *AlertData:
addLabelsAndAnnotationsValues(data.Labels, data.Annotations)
default:
return result, nil
}
return result, nil
}
// generateVariableDefinitions creates `{{ $varname := "" }}` declarations for each variable name.
func generateVariableDefinitions(varNames map[string]string) string {
if len(varNames) == 0 {
return ""
}
var sb strings.Builder
for name := range varNames {
fmt.Fprintf(&sb, `{{ $%s := %s }}`, name, varNames[name])
}
return sb.String()
}
// buildVariableDefinitions constructs the full variable definition preamble for a template.
// containing all known and unknown variables, the reason to include unknown variables is to
// populate them with "<no value>" in template so go-text-template don't throw errors
// when these variables are used in the template.
func buildVariableDefinitions(tmpl string, data any) (string, map[string]bool, error) {
// Extract the initial fields from the data struct and add to the definitions
mappings := extractFieldMappings(data)
// Add variables from struct root level fields to the definitions
variables := make(map[string]string)
for _, m := range mappings {
variables[m.VarName] = fmt.Sprintf(".%s", m.FieldName)
}
// Extract the nested fields definitions from the data struct, like labels, annotations, etc.
// once extracted we add them to the variables map along with the field address
nestedVariables := extractNestedFieldsDefinitions(data)
for k, v := range nestedVariables {
variables[k] = v
}
// variables that are used throughout the template
usedVars, err := ExtractUsedVariables(tmpl)
if err != nil {
return "", nil, err
}
// Compute unknown variables: used in template but not covered by a field mapping
probableUnknownVars := make(map[string]bool)
for name := range usedVars {
_, ok := variables[name]
if !ok {
probableUnknownVars[name] = true
}
}
// Add missing variables to the definitions with "<no value>"
// missingkey=zero is used to replace the missing value with "<no value>"
// but it only works when getting map values like {{ .keyfrommap }} from map and in struct this breaks
// with missing variable errors, we add missing variables in map so when directly variables
// are accessed directly in template block like {{ $variable }} it's handled and doesn't throw errors.
for name := range probableUnknownVars {
variables[name] = `"<no value>"`
}
return generateVariableDefinitions(variables), probableUnknownVars, nil
}
type ProcessingResult struct {
Template string
Data map[string]interface{}
// UnknownVars is the set of possible unknown variables exptracted using regex
UnknownVars map[string]bool
}
// PreProcessTemplateAndData prepares a template string and struct data for Go template execution.
//
// Input: "$receiver has $rule_name in $status state"
// Output: "{{ $receiver := .Receiver }}...{{ $receiver }} has {{ $rule_name }} in {{ $status }} state"
func PreProcessTemplateAndData(tmpl string, data any) (*ProcessingResult, error) {
// Handle empty template
unknownVars := make(map[string]bool)
if tmpl == "" {
result, err := prepareDataForTemplating(data)
if err != nil {
return nil, err
}
return &ProcessingResult{Data: result, UnknownVars: unknownVars}, nil
}
// Build variable definitions: known struct fields + fallback empty-string declarations
definitions, unknownVars, err := buildVariableDefinitions(tmpl, data)
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to build template definitions")
}
// Attach definitions prefix so WrapDollarVariables can parse the AST without "undefined variable" errors.
finalTmpl := definitions + tmpl
// Call WrapDollarVariables to transform bare $variable references to go-text-template format
// with {{ $variable }} syntax from $variable syntax
wrappedTmpl, err := WrapDollarVariables(finalTmpl)
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to prepare template for templating")
}
// Convert struct to map using mapstructure to be used for template execution
result, err := prepareDataForTemplating(data)
if err != nil {
return nil, err
}
return &ProcessingResult{Template: wrappedTmpl, Data: result, UnknownVars: unknownVars}, nil
}

View File

@@ -1,327 +0,0 @@
package alertmanagertemplate
import (
"testing"
"time"
"github.com/prometheus/alertmanager/template"
"github.com/stretchr/testify/require"
)
func TestExtractFieldMappings(t *testing.T) {
// Struct with various field types to test extraction logic
type TestStruct struct {
Name string `json:"name"`
Status string `json:"status"`
ActiveUserCount int `json:"user_count" mapstructure:"active_user_count"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"` // time.Time allowed
Items []string `json:"items"` // slice skipped
unexported string // unexported skipped (no tag needed)
NoTag string // no json tag skipped
SkippedTag string `json:"-"` // json:"-" skipped
}
testCases := []struct {
name string
data any
expected []fieldMapping
}{
{
name: "struct with mixed field types",
data: TestStruct{Name: "test", ActiveUserCount: 5, unexported: ""},
expected: []fieldMapping{
{VarName: "name", FieldName: "Name"},
{VarName: "status", FieldName: "Status"},
{VarName: "user_count", FieldName: "active_user_count"},
{VarName: "is_active", FieldName: "IsActive"},
{VarName: "created_at", FieldName: "CreatedAt"},
},
},
{
name: "nil data",
data: nil,
expected: nil,
},
{
name: "non-struct type",
data: "string",
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := extractFieldMappings(tc.data)
require.Equal(t, tc.expected, result)
})
}
}
func TestBuildVariableDefinitions(t *testing.T) {
testCases := []struct {
name string
tmpl string
data any
expectedVars []string // substrings that must appear in result
expectError bool
}{
{
name: "empty template still returns struct field definitions",
tmpl: "",
data: &NotificationTemplateData{Receiver: "test"},
expectedVars: []string{
"{{ $receiver := .receiver }}",
"{{ $status := .status }}",
},
},
{
name: "mix of known and unknown vars",
tmpl: "$rule_name: $custom_label",
data: &AlertData{AlertName: "test", Status: "ok", Severity: "critical"},
expectedVars: []string{
"{{ $rule_name := .rule_name }}",
"{{ $status := .status }}",
"{{ $severity := .severity }}",
`{{ $custom_label := "<no value>" }}`,
},
},
{
name: "nested fields definitions coming from NotificationTemplateData",
tmpl: "$severity for $service",
data: &NotificationTemplateData{Labels: template.KV{
"severity": "critical",
"service": "test",
"cloud.region.instance": "ap-south-1",
}},
expectedVars: []string{
"{{ $severity := index .labels \"severity\" }}",
"{{ $service := index .labels \"service\" }}",
"{{ $cloud_region_instance := index .labels \"cloud.region.instance\" }}",
},
},
{
name: "nested fields definitions coming from AlertData",
tmpl: "$severity for $service",
data: &AlertData{Labels: template.KV{
"severity": "critical",
"service": "test",
}},
expectedVars: []string{
"{{ $severity := index .labels \"severity\" }}",
"{{ $service := index .labels \"service\" }}",
},
},
{
name: "invalid template syntax returns error",
tmpl: "{{invalid",
data: &NotificationTemplateData{},
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, _, err := buildVariableDefinitions(tc.tmpl, tc.data)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
if len(tc.expectedVars) == 0 {
require.Empty(t, result)
return
}
for _, expected := range tc.expectedVars {
require.Contains(t, result, expected)
}
})
}
}
func TestPreProcessTemplateAndData(t *testing.T) {
testCases := []struct {
name string
tmpl string
data any
expectedTemplateContains []string
expectedData map[string]any
expectedUnknownVars map[string]bool
expectError bool
}{
{
name: "NotificationTemplateData with dollar variables and labels with dots and hyphens",
tmpl: "[$status] $rule_name (ID: $rule_id) - Firing: $total_firing, Resolved: $total_resolved, Severity: $severity\nHTTP method is: $http_request_method\nRequest path is: $http_request_path",
data: &NotificationTemplateData{
Receiver: "pagerduty",
Status: "firing",
AlertName: "HighMemory",
RuleID: "rule-123",
Labels: template.KV{
"severity": "critical",
"http.request.method": "GET",
"http-request-path": "/api/v1/metrics",
},
TotalFiring: 3,
TotalResolved: 1,
},
expectedTemplateContains: []string{
"{{$status := .status}}",
"{{$rule_name := .rule_name}}",
"{{$rule_id := .rule_id}}",
"{{$total_firing := .total_firing}}",
"{{$total_resolved := .total_resolved}}",
"{{$severity := index .labels \"severity\"}}",
"[{{ .status }}] {{ .rule_name }} (ID: {{ .rule_id }}) - Firing: {{ .total_firing }}, Resolved: {{ .total_resolved }}",
"{{$http_request_method := index .labels \"http.request.method\"}}",
"{{$http_request_path := index .labels \"http-request-path\"}}",
},
expectedData: map[string]any{
"status": "firing",
"rule_name": "HighMemory",
"rule_id": "rule-123",
"total_firing": 3,
"total_resolved": 1,
"severity": "critical",
"http_request_method": "GET",
"http_request_path": "/api/v1/metrics",
},
expectedUnknownVars: map[string]bool{},
},
{
name: "AlertData with dollar variables",
tmpl: "$rule_name: Value $value exceeded $threshold (Status: $status, Severity: $severity, Description: $description)",
data: &AlertData{
Receiver: "webhook",
Status: "resolved",
AlertName: "DiskFull",
RuleID: "disk-001",
Severity: "warning",
Annotations: template.KV{
"description": "Disk full and cannot be written to",
},
Value: "85%",
Threshold: "80%",
IsFiring: false,
IsResolved: true,
},
expectedTemplateContains: []string{
"{{$rule_name := .rule_name}}",
"{{$value := .value}}",
"{{$threshold := .threshold}}",
"{{$status := .status}}",
"{{$severity := .severity}}",
"{{$description := index .annotations \"description\"}}",
"{{ .rule_name }}: Value {{ .value }} exceeded {{ .threshold }} (Status: {{ .status }}, Severity: {{ .severity }}, Description: {{ .description }})",
},
expectedData: map[string]any{
"status": "resolved",
"rule_name": "DiskFull",
"rule_id": "disk-001",
"severity": "warning",
"value": "85%",
"threshold": "80%",
"description": "Disk full and cannot be written to",
},
expectedUnknownVars: map[string]bool{},
},
{
name: "mixed dollar and dot notation with both labels and annotations",
tmpl: "Alert $rule_name has {{.total_firing}} firing alerts",
data: &NotificationTemplateData{
AlertName: "HighCPU",
TotalFiring: 5,
Labels: template.KV{
"value": "<MASKED VALUE>",
"cpu.number": "10",
},
Annotations: template.KV{
"value": "85%",
},
},
expectedTemplateContains: []string{
"{{$rule_name := .rule_name}}",
"{{$value := index .labels \"value\"}}",
"Alert {{ .rule_name }} has {{.total_firing}} firing alerts",
"{{$cpu_number := index .labels \"cpu.number\"}}",
},
expectedData: map[string]any{
"rule_name": "HighCPU",
"total_firing": 5,
"value": "<MASKED VALUE>",
"cpu_number": "10",
},
expectedUnknownVars: map[string]bool{},
},
{
name: "empty template",
tmpl: "",
data: &NotificationTemplateData{Receiver: "slack"},
},
{
name: "invalid template syntax",
tmpl: "{{invalid",
data: &NotificationTemplateData{},
expectError: true,
},
{
name: "unknown dollar var in text renders empty",
tmpl: "alert $custom_note fired",
data: &NotificationTemplateData{AlertName: "HighCPU"},
expectedTemplateContains: []string{
`{{$custom_note := "<no value>"}}`,
"alert {{ .custom_note }} fired",
},
expectedUnknownVars: map[string]bool{"custom_note": true},
},
{
name: "unknown dollar var in action block renders empty",
tmpl: "alert {{ $custom_note }} fired",
data: &NotificationTemplateData{AlertName: "HighCPU"},
expectedTemplateContains: []string{
`{{$custom_note := "<no value>"}}`,
`alert {{$custom_note}} fired`,
},
expectedUnknownVars: map[string]bool{"custom_note": true},
},
{
name: "mix of known and unknown vars",
tmpl: "$rule_name: $custom_label",
data: &NotificationTemplateData{AlertName: "HighCPU"},
expectedTemplateContains: []string{
"{{$rule_name := .rule_name}}",
`{{$custom_label := "<no value>"}}`,
"{{ .rule_name }}: {{ .custom_label }}",
},
expectedData: map[string]any{"rule_name": "HighCPU"},
expectedUnknownVars: map[string]bool{"custom_label": true},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := PreProcessTemplateAndData(tc.tmpl, tc.data)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
if tc.tmpl == "" {
require.Equal(t, "", result.Template)
return
}
for _, substr := range tc.expectedTemplateContains {
require.Contains(t, result.Template, substr)
}
for k, v := range tc.expectedData {
require.Equal(t, v, result.Data[k])
}
if tc.expectedUnknownVars != nil {
require.Equal(t, tc.expectedUnknownVars, result.UnknownVars)
}
})
}
}

View File

@@ -1,90 +0,0 @@
package alertmanagertemplate
import (
"time"
"github.com/prometheus/alertmanager/template"
)
// TemplateInput carries the title/body templates
// and their defaults to apply in case the custom templates
// are result in empty strings.
type TemplateInput struct {
TitleTemplate string
BodyTemplate string
DefaultTitleTemplate string
DefaultBodyTemplate string
}
// ExpandedTemplates is the result of ExpandTemplates.
type ExpandedTemplates struct {
Title string
// Body is notification array of body for each alert
Body []string
// IsDefaultTemplatedBody is true if the body templates are templated using
// default templates, false when custom templates were used for templating.
IsDefaultTemplatedBody bool
MissingVars []string // union of unknown vars from title + body templates
}
// AlertData holds per-alert data used when expanding body templates.
type AlertData struct {
Receiver string `json:"receiver" mapstructure:"receiver"`
Status string `json:"status" mapstructure:"status"`
Labels template.KV `json:"labels" mapstructure:"labels"`
Annotations template.KV `json:"annotations" mapstructure:"annotations"`
StartsAt time.Time `json:"starts_at" mapstructure:"starts_at"`
EndsAt time.Time `json:"ends_at" mapstructure:"ends_at"`
GeneratorURL string `json:"generator_url" mapstructure:"generator_url"`
Fingerprint string `json:"fingerprint" mapstructure:"fingerprint"`
// Convenience fields extracted from well-known labels/annotations.
AlertName string `json:"rule_name" mapstructure:"rule_name"`
RuleID string `json:"rule_id" mapstructure:"rule_id"`
RuleLink string `json:"rule_link" mapstructure:"rule_link"`
Severity string `json:"severity" mapstructure:"severity"`
// Alert internal data fields
Value string `json:"value" mapstructure:"value"`
Threshold string `json:"threshold" mapstructure:"threshold"`
CompareOp string `json:"compare_op" mapstructure:"compare_op"`
MatchType string `json:"match_type" mapstructure:"match_type"`
// Link annotations added by the rule evaluator.
LogLink string `json:"log_link" mapstructure:"log_link"`
TraceLink string `json:"trace_link" mapstructure:"trace_link"`
// Status booleans for easy conditional templating.
IsFiring bool `json:"is_firing" mapstructure:"is_firing"`
IsResolved bool `json:"is_resolved" mapstructure:"is_resolved"`
IsMissingData bool `json:"is_missing_data" mapstructure:"is_missing_data"`
IsRecovering bool `json:"is_recovering" mapstructure:"is_recovering"`
}
// NotificationTemplateData is the top-level data struct provided to custom templates.
type NotificationTemplateData struct {
Receiver string `json:"receiver" mapstructure:"receiver"`
Status string `json:"status" mapstructure:"status"`
// Convenience fields for title templates.
AlertName string `json:"rule_name" mapstructure:"rule_name"`
RuleID string `json:"rule_id" mapstructure:"rule_id"`
RuleLink string `json:"rule_link" mapstructure:"rule_link"`
TotalFiring int `json:"total_firing" mapstructure:"total_firing"`
TotalResolved int `json:"total_resolved" mapstructure:"total_resolved"`
// Per-alert data, also available as filtered sub-slices.
Alerts []AlertData `json:"-" mapstructure:"-"`
// Cross-alert aggregates, computed as intersection across all alerts.
GroupLabels template.KV `json:"group_labels" mapstructure:"group_labels"`
CommonLabels template.KV `json:"common_labels" mapstructure:"common_labels"`
CommonAnnotations template.KV `json:"common_annotations" mapstructure:"common_annotations"`
ExternalURL string `json:"external_url" mapstructure:"external_url"`
// Labels and Annotations that are collection of labels
// and annotations from all alerts, it includes only the common labels and annotations
// and for non-common labels and annotations, it picks some first few labels/annotations
// and joins them with ", " to avoid blank values in the template
Labels template.KV `json:"labels" mapstructure:"labels"`
Annotations template.KV `json:"annotations" mapstructure:"annotations"`
}

View File

@@ -1,230 +0,0 @@
package alertmanagertemplate
import (
"fmt"
"reflect"
"regexp"
"strings"
"text/template/parse"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)
// maxAggregatedValues is the maximum number of unique values to include
// when aggregating non-common label/annotation values across alerts.
const maxAggregatedValues = 5
// bareVariableRegex matches bare $variable references including dotted paths like $service.name.
var bareVariableRegex = regexp.MustCompile(`\$(\w+(?:\.\w+)*)`)
// bareVariableRegexFirstSeg matches only the base $variable name, stopping before any dotted path.
// e.g. "$labels.severity" matches "$labels", "$name" matches "$name".
var bareVariableRegexFirstSeg = regexp.MustCompile(`\$\w+`)
// ExtractTemplatesFromAnnotations computes the common annotations across all alerts
// and returns the values for the title_template and body_template annotation keys as title and body templates.
func ExtractTemplatesFromAnnotations(alerts []*types.Alert) (titleTemplate, bodyTemplate string) {
if len(alerts) == 0 {
return "", ""
}
commonAnnotations := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
return commonAnnotations[ruletypes.AnnotationTitleTemplate], commonAnnotations[ruletypes.AnnotationBodyTemplate]
}
// WrapDollarVariables wraps bare $variable references in Go template syntax.
// Example transformations:
// - "$name is $status" -> "{{ $name }} is {{ $status }}"
// - "$labels.severity" -> "{{ index .labels \"severity\" }}"
// - "$labels.http.status" -> "{{ index .labels \"http.status\" }}"
// - "$annotations.summary" -> "{{ index .annotations \"summary\" }}"
// - "$service.name" -> "{{ index . \"service.name\" }}"
// - "$name is {{ .Status }}" -> "{{ $name }} is {{ .Status }}"
func WrapDollarVariables(src string) (string, error) {
if src == "" {
return src, nil
}
// Create a new parse.Tree directly
tree := parse.New("template")
tree.Mode = parse.SkipFuncCheck
// Parse the template
_, err := tree.Parse(src, "{{", "}}", make(map[string]*parse.Tree), nil)
if err != nil {
return "", err
}
// Walk the AST and transform TextNodes
walkAndWrapTextNodes(tree.Root)
// Return the reassembled template
return tree.Root.String(), nil
}
// walkAndWrapTextNodes recursively walks the parse tree trying to find a text node.
// Once a text node is found, it wraps the bare $variable and changes it to index-based
// element access from the data map, like '.key' or '.key.subkey'.
func walkAndWrapTextNodes(node parse.Node) {
if reflect.ValueOf(node).IsNil() {
return
}
switch n := node.(type) {
// `$name is {{.Status}}` is a list node with one text and one action node
case *parse.ListNode:
// Recurse into all child nodes
if n.Nodes != nil {
for _, child := range n.Nodes {
walkAndWrapTextNodes(child)
}
}
// `$name is ` is a text node with plain text in root
// we try to find the $name variable and wrap it with template block
// like `{{ .name }}`, for labels and annotations we use the index to access the value
// so `$labels.service` becomes `{{ index .labels "service" }}`
case *parse.TextNode:
// Transform $variable based on its pattern
n.Text = bareVariableRegex.ReplaceAllFunc(n.Text, func(match []byte) []byte {
// Extract variable name without the $
varName := string(match[1:])
// Check if variable contains dots
if strings.Contains(varName, ".") {
// Check for reserved prefixes: labels.* or annotations.*
if strings.HasPrefix(varName, "labels.") {
key := strings.TrimPrefix(varName, "labels.")
return []byte(fmt.Sprintf(`{{ index .labels "%s" }}`, key))
}
if strings.HasPrefix(varName, "annotations.") {
key := strings.TrimPrefix(varName, "annotations.")
return []byte(fmt.Sprintf(`{{ index .annotations "%s" }}`, key))
}
// Other dotted variables: index into root context
return []byte(fmt.Sprintf(`{{ index . "%s" }}`, varName))
}
// Simple variables: use dot notation to directly access the field
// without raising any error due to missing variables
return []byte(fmt.Sprintf("{{ .%s }}", varName))
})
// `{{if pipeline}} T1 {{else}} T0 {{end}}` is a if node with T1 part of List and T0 part of ElseList
case *parse.IfNode:
// Recurse into both branches
walkAndWrapTextNodes(n.List)
walkAndWrapTextNodes(n.ElseList)
// `{{range pipeline}} T1 {{else}} T0 {{end}}` is a range node with T1 part of List and T0 part of ElseList
case *parse.RangeNode:
// Recurse into both branches
walkAndWrapTextNodes(n.List)
walkAndWrapTextNodes(n.ElseList)
// All other node types (ActionNode, PipeNode, VariableNode, etc.) are already
// inside {{ }} action blocks and don't need transformation
// Support for `with` can be added later when we start supporting it in editor block
}
}
// ExtractUsedVariables returns the set of all $variable referenced in template
// — text nodes, action blocks, branch conditions, and loop declarations — regardless of scope.
// After finding all variables, we find those that are not part of our alert data and handle them so the
// text/template parser does not reject undefined $variables.
func ExtractUsedVariables(src string) (map[string]bool, error) {
if src == "" {
return map[string]bool{}, nil
}
// Regex-scan raw template string to collect all $var base names.
// bareVariableRegexFirstSeg stops before dots, so "$labels.severity" yields "$labels".
used := make(map[string]bool)
for _, m := range bareVariableRegexFirstSeg.FindAll([]byte(src), -1) {
used[string(m[1:])] = true // strip leading "$"
}
// Build a preamble that pre-declares every found variable.
// This prevents "undefined variable" parse errors for $vars used in action
// blocks while still letting genuine syntax errors propagate.
var preamble strings.Builder
for name := range used {
fmt.Fprintf(&preamble, `{{$%s := ""}}`, name)
}
// Validate template syntax.
tree := parse.New("template")
tree.Mode = parse.SkipFuncCheck
if _, err := tree.Parse(preamble.String()+src, "{{", "}}", make(map[string]*parse.Tree), nil); err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInternal, "failed to extract used variables")
}
return used, nil
}
// aggregateKV aggregates key-value pairs (labels or annotations) from all alerts into a single template.KV.
// The result is used to populate the labels and annotations in the notification template data.
func aggregateKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
// track unique values per key in order of first appearance
valuesPerKey := make(map[string][]string)
// track which values have been seen for deduplication
seenValues := make(map[string]map[string]bool)
for _, alert := range alerts {
kvPairs := extractFn(alert)
for k, v := range kvPairs {
key := string(k)
value := string(v)
if seenValues[key] == nil {
seenValues[key] = make(map[string]bool)
}
// only add if not already seen and under the limit of maxAggregatedValues
if !seenValues[key][value] && len(valuesPerKey[key]) < maxAggregatedValues {
seenValues[key][value] = true
valuesPerKey[key] = append(valuesPerKey[key], value)
}
}
}
// build the result by joining values
result := make(template.KV, len(valuesPerKey))
for key, values := range valuesPerKey {
result[key] = strings.Join(values, ", ")
}
return result
}
// extractCommonKV returns the intersection of key-value pairs across all alerts.
// A key/value pair is included only if it appears identically on every alert.
func extractCommonKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
if len(alerts) == 0 {
return template.KV{}
}
common := make(template.KV, len(extractFn(alerts[0])))
for k, v := range extractFn(alerts[0]) {
common[string(k)] = string(v)
}
for _, a := range alerts[1:] {
kv := extractFn(a)
for k := range common {
if string(kv[model.LabelName(k)]) != common[k] {
delete(common, k)
}
}
if len(common) == 0 {
break
}
}
return common
}

View File

@@ -1,348 +0,0 @@
package alertmanagertemplate
import (
"testing"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
)
func TestWrapBareVars(t *testing.T) {
testCases := []struct {
name string
input string
expected string
expectError bool
}{
{
name: "mixed variables with actions",
input: "$name is {{.Status}}",
expected: "{{ .name }} is {{.Status}}",
},
{
name: "nested variables in range",
input: `{{range .items}}
$title
{{end}}`,
expected: `{{range .items}}
{{ .title }}
{{end}}`,
},
{
name: "nested variables in if else",
input: "{{if .ok}}$a{{else}}$b{{end}}",
expected: "{{if .ok}}{{ .a }}{{else}}{{ .b }}{{end}}",
},
// Labels prefix: index into .labels map
{
name: "labels variables prefix simple",
input: "$labels.service",
expected: `{{ index .labels "service" }}`,
},
{
name: "labels variables prefix nested with multiple dots",
input: "$labels.http.status",
expected: `{{ index .labels "http.status" }}`,
},
{
name: "multiple labels variables simple and nested",
input: "$labels.service and $labels.instance.id",
expected: `{{ index .labels "service" }} and {{ index .labels "instance.id" }}`,
},
// Annotations prefix: index into .annotations map
{
name: "annotations variables prefix simple",
input: "$annotations.summary",
expected: `{{ index .annotations "summary" }}`,
},
{
name: "annotations variables prefix nested with multiple dots",
input: "$annotations.alert.url",
expected: `{{ index .annotations "alert.url" }}`,
},
// Other dotted paths: index into root context
{
name: "other variables with multiple dots",
input: "$service.name",
expected: `{{ index . "service.name" }}`,
},
{
name: "other variables with multiple dots nested",
input: "$http.status.code",
expected: `{{ index . "http.status.code" }}`,
},
// Hybrid: all types combined
{
name: "hybrid - all variables types",
input: "Alert: $alert_name Labels: $labels.severity Annotations: $annotations.desc Service: $service.name Count: $error_count",
expected: `Alert: {{ .alert_name }} Labels: {{ index .labels "severity" }} Annotations: {{ index .annotations "desc" }} Service: {{ index . "service.name" }} Count: {{ .error_count }}`,
},
{
name: "already wrapped should not be changed",
input: "{{$status := .status}}{{.name}} is {{$status | toUpper}}",
expected: "{{$status := .status}}{{.name}} is {{$status | toUpper}}",
},
{
name: "no variables should not be changed",
input: "Hello world",
expected: "Hello world",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "deeply nested",
input: "{{range .items}}{{if .ok}}$deep{{end}}{{end}}",
expected: "{{range .items}}{{if .ok}}{{ .deep }}{{end}}{{end}}",
},
{
name: "complex example",
input: `Hello $name, your score is $score.
{{if .isAdmin}}
Welcome back $name, you have {{.unreadCount}} messages.
{{end}}`,
expected: `Hello {{ .name }}, your score is {{ .score }}.
{{if .isAdmin}}
Welcome back {{ .name }}, you have {{.unreadCount}} messages.
{{end}}`,
},
{
name: "with custom function",
input: "$name triggered at {{urlescape .url}}",
expected: "{{ .name }} triggered at {{urlescape .url}}",
},
{
name: "invalid template",
input: "{{invalid",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := WrapDollarVariables(tc.input)
if tc.expectError {
require.Error(t, err, "should error on invalid template syntax")
} else {
require.NoError(t, err)
require.Equal(t, tc.expected, result)
}
})
}
}
func TestExtractUsedVariables(t *testing.T) {
testCases := []struct {
name string
input string
expected map[string]bool
expectError bool
}{
{
name: "simple usage in text",
input: "$name is $status",
expected: map[string]bool{"name": true, "status": true},
},
{
name: "declared in action block",
input: "{{ $name := .name }}",
expected: map[string]bool{"name": true},
},
{
name: "range loop vars",
input: "{{ range $i, $v := .items }}{{ end }}",
expected: map[string]bool{"i": true, "v": true},
},
{
name: "mixed text and action",
input: "$x and {{ $y }}",
expected: map[string]bool{"x": true, "y": true},
},
{
name: "dotted path in text extracts base only",
input: "$labels.severity",
expected: map[string]bool{"labels": true},
},
{
name: "nested if else",
input: "{{ if .ok }}{{ $a }}{{ else }}{{ $b }}{{ end }}",
expected: map[string]bool{"a": true, "b": true},
},
{
name: "empty string",
input: "",
expected: map[string]bool{},
},
{
name: "no variables",
input: "Hello world",
expected: map[string]bool{},
},
{
name: "invalid template returns error",
input: "{{invalid",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ExtractUsedVariables(tc.input)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expected, result)
}
})
}
}
func TestAggregateKV(t *testing.T) {
extractLabels := func(a *types.Alert) model.LabelSet { return a.Labels }
testCases := []struct {
name string
alerts []*types.Alert
extractFn func(*types.Alert) model.LabelSet
expected template.KV
}{
{
name: "empty alerts slice",
alerts: []*types.Alert{},
extractFn: extractLabels,
expected: template.KV{},
},
{
name: "single alert",
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{
"env": "production",
"service": "backend",
},
},
},
},
extractFn: extractLabels,
expected: template.KV{
"env": "production",
"service": "backend",
},
},
{
name: "varying values with duplicates deduped",
alerts: []*types.Alert{
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "backend"}}},
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "api"}}},
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "frontend"}}},
{Alert: model.Alert{Labels: model.LabelSet{"env": "production", "service": "api"}}},
},
extractFn: extractLabels,
expected: template.KV{
"env": "production",
"service": "backend, api, frontend",
},
},
{
name: "more than 5 unique values truncates to 5",
alerts: []*types.Alert{
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc1"}}},
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc2"}}},
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc3"}}},
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc4"}}},
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc5"}}},
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc6"}}},
{Alert: model.Alert{Labels: model.LabelSet{"service": "svc7"}}},
},
extractFn: extractLabels,
expected: template.KV{
"service": "svc1, svc2, svc3, svc4, svc5",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := aggregateKV(tc.alerts, tc.extractFn)
require.Equal(t, tc.expected, result)
})
}
}
func TestExtractCommonKV(t *testing.T) {
extractLabels := func(a *types.Alert) model.LabelSet { return a.Labels }
extractAnnotations := func(a *types.Alert) model.LabelSet { return a.Annotations }
testCases := []struct {
name string
alerts []*types.Alert
extractFn func(*types.Alert) model.LabelSet
expected template.KV
}{
{
name: "empty alerts slice",
alerts: []*types.Alert{},
extractFn: extractLabels,
expected: template.KV{},
},
{
name: "single alert returns all labels",
alerts: []*types.Alert{
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "api"}}},
},
extractFn: extractLabels,
expected: template.KV{"env": "prod", "service": "api"},
},
{
name: "multiple alerts with fully common labels",
alerts: []*types.Alert{
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "region": "us-east"}}},
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "region": "us-east"}}},
},
extractFn: extractLabels,
expected: template.KV{"env": "prod", "region": "us-east"},
},
{
name: "multiple alerts with partially common labels",
alerts: []*types.Alert{
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "api"}}},
{Alert: model.Alert{Labels: model.LabelSet{"env": "prod", "service": "worker"}}},
},
extractFn: extractLabels,
expected: template.KV{"env": "prod"},
},
{
name: "multiple alerts with no common labels",
alerts: []*types.Alert{
{Alert: model.Alert{Labels: model.LabelSet{"service": "api"}}},
{Alert: model.Alert{Labels: model.LabelSet{"service": "worker"}}},
},
extractFn: extractLabels,
expected: template.KV{},
},
{
name: "annotations extract common annotations",
alerts: []*types.Alert{
{Alert: model.Alert{Annotations: model.LabelSet{"summary": "high cpu", "runbook": "http://x"}}},
{Alert: model.Alert{Annotations: model.LabelSet{"summary": "high cpu", "runbook": "http://y"}}},
},
extractFn: extractAnnotations,
expected: template.KV{"summary": "high cpu"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := extractCommonKV(tc.alerts, tc.extractFn)
require.Equal(t, tc.expected, result)
})
}
}

View File

@@ -1,106 +0,0 @@
package alertnotificationprocessor
import (
"bytes"
"context"
htmltemplate "html/template"
"log/slog"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/prometheus/alertmanager/types"
)
type alertNotificationProcessor struct {
templater alertmanagertemplate.AlertManagerTemplater
renderer markdownrenderer.MarkdownRenderer
logger *slog.Logger
templateStore emailtypes.TemplateStore
}
func New(templater alertmanagertemplate.AlertManagerTemplater, renderer markdownrenderer.MarkdownRenderer, templateStore emailtypes.TemplateStore, logger *slog.Logger) alertmanagertypes.NotificationProcessor {
return &alertNotificationProcessor{
templater: templater,
renderer: renderer,
logger: logger,
templateStore: templateStore,
}
}
// emailNotificationTemplateData is the data passed to the email HTML layout template.
// It embeds NotificationTemplateData so all its fields are directly accessible in the template.
type emailNotificationTemplateData struct {
alertmanagertemplate.NotificationTemplateData
Title string
Bodies []htmltemplate.HTML
}
func (p *alertNotificationProcessor) ProcessAlertNotification(ctx context.Context, input alertmanagertypes.NotificationProcessorInput, alerts []*types.Alert, rendererFormat markdownrenderer.OutputFormat) (*alertmanagertypes.NotificationProcessorResult, error) {
// delegate to templater
expanded, err := p.templater.ProcessTemplates(ctx, alertmanagertemplate.TemplateInput{
TitleTemplate: input.TitleTemplate,
BodyTemplate: input.BodyTemplate,
DefaultTitleTemplate: input.DefaultTitleTemplate,
DefaultBodyTemplate: input.DefaultBodyTemplate,
}, alerts)
if err != nil {
return nil, err
}
// apply rendering to body based on the format
var renderedBodies []string
if expanded.IsDefaultTemplatedBody {
// default templates already produce format-appropriate output
renderedBodies = expanded.Body
} else {
// render each body string using the renderer
for _, body := range expanded.Body {
rendered, err := p.renderer.Render(ctx, body, rendererFormat)
if err != nil {
return nil, err
}
renderedBodies = append(renderedBodies, rendered)
}
}
return &alertmanagertypes.NotificationProcessorResult{
Title: expanded.Title,
Body: renderedBodies,
IsDefaultTemplatedBody: expanded.IsDefaultTemplatedBody,
MissingVars: expanded.MissingVars,
}, nil
}
func (p *alertNotificationProcessor) RenderEmailNotification(
ctx context.Context,
templateName emailtypes.TemplateName,
result *alertmanagertypes.NotificationProcessorResult,
alerts []*types.Alert,
) (string, error) {
layoutTmpl, err := p.templateStore.Get(ctx, templateName)
if err != nil {
return "", err
}
ntd := p.templater.BuildNotificationTemplateData(ctx, alerts)
bodies := make([]htmltemplate.HTML, 0, len(result.Body))
for _, b := range result.Body {
bodies = append(bodies, htmltemplate.HTML(b))
}
data := emailNotificationTemplateData{
NotificationTemplateData: *ntd,
Title: result.Title,
Bodies: bodies,
}
var buf bytes.Buffer
if err := layoutTmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}

View File

@@ -1,343 +0,0 @@
package alertnotificationprocessor
import (
"context"
"log/slog"
"os"
"path/filepath"
"sort"
"testing"
"time"
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
)
func testSetup(t *testing.T) (alertmanagertypes.NotificationProcessor, context.Context) {
t.Helper()
tmpl := test.CreateTmpl(t)
logger := slog.New(slog.DiscardHandler)
templater := alertmanagertemplate.New(tmpl, logger)
renderer := markdownrenderer.NewMarkdownRenderer(logger)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group")
ctx = notify.WithReceiverName(ctx, "slack")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{
"alertname": "TestAlert",
"severity": "critical",
})
return New(templater, renderer, filetemplatestore.NewEmptyStore(), logger), ctx
}
func createAlert(labels, annotations map[string]string, isFiring bool) *types.Alert {
ls := model.LabelSet{}
for k, v := range labels {
ls[model.LabelName(k)] = model.LabelValue(v)
}
ann := model.LabelSet{}
for k, v := range annotations {
ann[model.LabelName(k)] = model.LabelValue(v)
}
startsAt := time.Now()
var endsAt time.Time
if isFiring {
endsAt = startsAt.Add(time.Hour)
} else {
startsAt = startsAt.Add(-2 * time.Hour)
endsAt = startsAt.Add(-time.Hour)
}
return &types.Alert{Alert: model.Alert{Labels: ls, Annotations: ann, StartsAt: startsAt, EndsAt: endsAt}}
}
func TestProcessAlertNotification(t *testing.T) {
processor, ctx := testSetup(t)
tests := []struct {
name string
alerts []*types.Alert
input alertmanagertypes.NotificationProcessorInput
wantTitle string
wantBody []string
wantIsDefaultBody bool
wantMissingVars []string
RendererFormat markdownrenderer.OutputFormat
}{
{
name: "custom title and body rendered as HTML",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "HighCPU",
ruletypes.LabelSeverityName: "critical",
"service": "api-server",
},
map[string]string{"description": "CPU usage exceeded 95%"},
true,
),
},
input: alertmanagertypes.NotificationProcessorInput{
TitleTemplate: "Alert: $rule_name on $service",
BodyTemplate: "**Service:** $service\n\n**Description:** $description",
},
RendererFormat: markdownrenderer.MarkdownFormatHTML,
wantTitle: "Alert: HighCPU on api-server",
wantBody: []string{"<p><strong>Service:</strong> api-server</p><p></p><p><strong>Description:</strong> CPU usage exceeded 95%</p><p></p>"},
wantIsDefaultBody: false,
},
{
name: "custom title and body rendered as SlackBlockKit",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "HighMemory",
ruletypes.LabelSeverityName: "warning",
},
map[string]string{"description": "Memory usage high"},
true,
),
},
input: alertmanagertypes.NotificationProcessorInput{
TitleTemplate: "$rule_name - $severity",
BodyTemplate: "Memory alert: $description",
},
RendererFormat: markdownrenderer.MarkdownFormatSlackBlockKit,
wantTitle: "HighMemory - warning",
wantBody: []string{`[{"type":"section","text":{"type":"mrkdwn","text":"Memory alert: Memory usage high"}}]`},
wantIsDefaultBody: false,
},
{
name: "custom title and body with Noop format passes through as-is",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "DiskFull",
ruletypes.LabelSeverityName: "critical",
"host": "db-01",
},
nil,
true,
),
},
input: alertmanagertypes.NotificationProcessorInput{
TitleTemplate: "$rule_name on $host",
BodyTemplate: "**Host:** $labels.host is full",
},
RendererFormat: markdownrenderer.MarkdownFormatNoop,
wantTitle: "DiskFull on db-01",
wantBody: []string{"**Host:** db-01 is full"},
wantIsDefaultBody: false,
},
{
name: "default fallback when custom templates are empty",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "TestAlert",
ruletypes.LabelSeverityName: "critical",
},
map[string]string{"description": "Something broke"},
true,
),
},
input: alertmanagertypes.NotificationProcessorInput{
DefaultTitleTemplate: `{{ .CommonLabels.alertname }} ({{ .Status | toUpper }})`,
DefaultBodyTemplate: `{{ range .Alerts }}{{ .Annotations.description }}{{ end }}`,
},
RendererFormat: markdownrenderer.MarkdownFormatHTML,
wantTitle: "TestAlert (FIRING)",
wantBody: []string{"Something broke"},
wantIsDefaultBody: true,
},
{
name: "missing vars pass through to result",
alerts: []*types.Alert{
createAlert(
map[string]string{ruletypes.LabelAlertName: "TestAlert"},
nil,
true,
),
},
input: alertmanagertypes.NotificationProcessorInput{
TitleTemplate: "[$environment] $rule_name",
BodyTemplate: "See runbook: $runbook_url",
},
RendererFormat: markdownrenderer.MarkdownFormatNoop,
wantTitle: "[<no value>] TestAlert",
wantBody: []string{"See runbook: <no value>"},
wantIsDefaultBody: false,
wantMissingVars: []string{"environment", "runbook_url"},
},
{
name: "slack mrkdwn renders bold and italic correctly along with missing variables",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "HighCPU",
ruletypes.LabelSeverityName: "critical",
"service": "api-server",
},
map[string]string{"description": "CPU usage exceeded 95%"},
true,
),
},
input: alertmanagertypes.NotificationProcessorInput{
TitleTemplate: "Alert: $rule_name",
BodyTemplate: "**Service:** $service\n\n*Description:* $description $http_request_method",
},
RendererFormat: markdownrenderer.MarkdownFormatSlackMrkdwn,
wantTitle: "Alert: HighCPU",
wantBody: []string{"*Service:* api-server\n\n_Description:_ CPU usage exceeded 95% <no value>\n\n"},
wantMissingVars: []string{"http_request_method"},
wantIsDefaultBody: false,
},
{
name: "slack mrkdwn with multiple alerts produces per-alert bodies",
alerts: []*types.Alert{
createAlert(
map[string]string{ruletypes.LabelAlertName: "SvcDown", "service": "auth"},
map[string]string{"description": "Auth service **down**"},
true,
),
createAlert(
map[string]string{ruletypes.LabelAlertName: "SvcDown", "service": "payments"},
map[string]string{"description": "Payments service **degraded**"},
false,
),
},
input: alertmanagertypes.NotificationProcessorInput{
TitleTemplate: "$rule_name: $total_firing firing, $total_resolved resolved",
BodyTemplate: "**$service** ($status): $description",
},
RendererFormat: markdownrenderer.MarkdownFormatSlackMrkdwn,
wantTitle: "SvcDown: 1 firing, 1 resolved",
wantBody: []string{"*auth* (firing): Auth service *down*\n\n", "*payments* (resolved): Payments service *degraded*\n\n"},
wantIsDefaultBody: false,
},
{
name: "slack mrkdwn skips rendering for default templates",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "TestAlert",
ruletypes.LabelSeverityName: "critical",
},
map[string]string{"description": "Something broke"},
true,
),
},
input: alertmanagertypes.NotificationProcessorInput{
DefaultTitleTemplate: `{{ .CommonLabels.alertname }} ({{ .Status | toUpper }})`,
DefaultBodyTemplate: `{{ range .Alerts }}**Bold** *italic* ~~strike~~ {{ .Annotations.description }}{{ end }}`,
},
RendererFormat: markdownrenderer.MarkdownFormatSlackMrkdwn,
wantTitle: "TestAlert (FIRING)",
wantBody: []string{"**Bold** *italic* ~~strike~~ Something broke"},
wantIsDefaultBody: true,
},
{
name: "multiple alerts produce one body entry per alert",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrash", "pod": "worker-1"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrash", "pod": "worker-2"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrash", "pod": "worker-3"}, nil, false),
},
input: alertmanagertypes.NotificationProcessorInput{
TitleTemplate: "$rule_name: $total_firing firing",
BodyTemplate: "$labels.pod ($status)",
},
RendererFormat: markdownrenderer.MarkdownFormatNoop,
wantTitle: "PodCrash: 2 firing",
wantBody: []string{"worker-1 (firing)", "worker-2 (firing)", "worker-3 (resolved)"},
wantIsDefaultBody: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := processor.ProcessAlertNotification(ctx, tc.input, tc.alerts, tc.RendererFormat)
require.NoError(t, err)
require.Equal(t, tc.wantTitle, result.Title)
require.Equal(t, tc.wantBody, result.Body)
require.Equal(t, tc.wantIsDefaultBody, result.IsDefaultTemplatedBody)
if len(tc.wantMissingVars) == 0 {
require.Empty(t, result.MissingVars)
} else {
sort.Strings(tc.wantMissingVars)
require.Equal(t, tc.wantMissingVars, result.MissingVars)
}
})
}
}
func TestRenderEmailNotification_TemplateNotFound(t *testing.T) {
processor, ctx := testSetup(t)
result := &alertmanagertypes.NotificationProcessorResult{
Title: "Test Alert",
Body: []string{"alert body"},
}
alerts := []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "TestAlert"}, nil, true),
}
_, err := processor.RenderEmailNotification(ctx, emailtypes.TemplateNameAlertEmailNotification, result, alerts)
require.Error(t, err)
require.True(t, errors.Ast(err, errors.TypeNotFound))
}
func TestRenderEmailNotification_RendersTemplate(t *testing.T) {
// Create a temp dir with a test template
tmpDir := t.TempDir()
tmplContent := `<!DOCTYPE html><html><body><h1>{{.Title}}</h1><p>Status: {{.Status}}</p><p>Firing: {{.TotalFiring}}</p>{{range .Bodies}}<div>{{.}}</div>{{end}}{{range .Alerts}}<p>{{.AlertName}}</p>{{end}}</body></html>`
err := os.WriteFile(filepath.Join(tmpDir, "alert_email_notification.gotmpl"), []byte(tmplContent), 0644)
require.NoError(t, err)
tmpl := test.CreateTmpl(t)
logger := slog.New(slog.DiscardHandler)
templater := alertmanagertemplate.New(tmpl, logger)
renderer := markdownrenderer.NewMarkdownRenderer(logger)
store, err := filetemplatestore.NewStore(context.Background(), tmpDir, emailtypes.Templates, logger)
require.NoError(t, err)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group")
ctx = notify.WithReceiverName(ctx, "email")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{
"alertname": "HighCPU",
"severity": "critical",
})
processor := New(templater, renderer, store, logger)
result := &alertmanagertypes.NotificationProcessorResult{
Title: "HighCPU Alert",
Body: []string{"<strong>CPU is high</strong>", "<strong>CPU is low</strong>"},
IsDefaultTemplatedBody: false,
}
alerts := []*types.Alert{
createAlert(
map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"},
nil,
true,
),
}
html, err := processor.RenderEmailNotification(ctx, emailtypes.TemplateNameAlertEmailNotification, result, alerts)
require.NoError(t, err)
require.NotEmpty(t, html)
// the html template should be filled with go text templating
require.Equal(t, "<!DOCTYPE html><html><body><h1>HighCPU Alert</h1><p>Status: firing</p><p>Firing: 1</p><div><strong>CPU is high</strong></div><div><strong>CPU is low</strong></div><p>HighCPU</p></body></html>", html)
}

View File

@@ -0,0 +1,31 @@
package alertmanager
import "net/http"
type Handler interface {
GetAlerts(http.ResponseWriter, *http.Request)
TestReceiver(http.ResponseWriter, *http.Request)
ListChannels(http.ResponseWriter, *http.Request)
ListAllChannels(http.ResponseWriter, *http.Request)
GetChannelByID(http.ResponseWriter, *http.Request)
CreateChannel(http.ResponseWriter, *http.Request)
UpdateChannelByID(http.ResponseWriter, *http.Request)
DeleteChannelByID(http.ResponseWriter, *http.Request)
GetAllRoutePolicies(http.ResponseWriter, *http.Request)
GetRoutePolicyByID(http.ResponseWriter, *http.Request)
CreateRoutePolicy(http.ResponseWriter, *http.Request)
UpdateRoutePolicy(http.ResponseWriter, *http.Request)
DeleteRoutePolicyByID(http.ResponseWriter, *http.Request)
}

View File

@@ -223,6 +223,8 @@ func (r *provider) Match(ctx context.Context, orgID string, ruleID string, set m
for _, route := range expressionRoutes {
evaluateExpr, err := r.evaluateExpr(ctx, route.Expression, set)
if err != nil {
//nolint:sloglint
r.settings.Logger().WarnContext(ctx, "failed to evaluate route policy expression", errors.Attr(err), slog.String("rule.id", ruleID))
continue
}
if evaluateExpr {
@@ -298,7 +300,7 @@ func (r *provider) convertLabelSetToEnv(ctx context.Context, labelSet model.Labe
func (r *provider) evaluateExpr(ctx context.Context, expression string, labelSet model.LabelSet) (bool, error) {
env := r.convertLabelSetToEnv(ctx, labelSet)
program, err := expr.Compile(expression, expr.Env(env))
program, err := expr.Compile(expression, expr.Env(env), expr.AllowUndefinedVariables())
if err != nil {
return false, errors.NewInternalf(errors.CodeInternal, "error compiling route policy %s: %v", expression, err)
}

View File

@@ -644,6 +644,22 @@ func TestProvider_EvaluateExpression(t *testing.T) {
},
expected: true,
},
{
name: "nonexistent key OR check",
expression: `threshold.name = 'warning' OR ruleId = 'rule1'`,
labelSet: model.LabelSet{
"threshold.name": "warning",
},
expected: true,
},
{
name: "nonexistent key && check",
expression: `threshold.name = 'warning' && nonexistent = 'auth'`,
labelSet: model.LabelSet{
"threshold.name": "warning",
},
expected: false,
},
}
for _, tt := range tests {

View File

@@ -1,4 +1,4 @@
package alertmanager
package signozalertmanager
import (
"context"
@@ -7,6 +7,7 @@ import (
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
@@ -15,17 +16,15 @@ import (
"github.com/gorilla/mux"
)
type API struct {
alertmanager Alertmanager
type handler struct {
alertmanager alertmanager.Alertmanager
}
func NewAPI(alertmanager Alertmanager) *API {
return &API{
alertmanager: alertmanager,
}
func NewHandler(alertmanager alertmanager.Alertmanager) alertmanager.Handler {
return &handler{alertmanager: alertmanager}
}
func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetAlerts(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -41,7 +40,7 @@ func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
return
}
alerts, err := api.alertmanager.GetAlerts(ctx, claims.OrgID, params)
alerts, err := handler.alertmanager.GetAlerts(ctx, claims.OrgID, params)
if err != nil {
render.Error(rw, err)
return
@@ -50,7 +49,7 @@ func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, alerts)
}
func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) TestReceiver(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -73,7 +72,7 @@ func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
return
}
err = api.alertmanager.TestReceiver(ctx, claims.OrgID, receiver)
err = handler.alertmanager.TestReceiver(ctx, claims.OrgID, receiver)
if err != nil {
render.Error(rw, err)
return
@@ -82,7 +81,7 @@ func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) ListChannels(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -92,7 +91,7 @@ func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
return
}
channels, err := api.alertmanager.ListChannels(ctx, claims.OrgID)
channels, err := handler.alertmanager.ListChannels(ctx, claims.OrgID)
if err != nil {
render.Error(rw, err)
return
@@ -106,11 +105,11 @@ func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, channels)
}
func (api *API) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
channels, err := api.alertmanager.ListAllChannels(ctx)
channels, err := handler.alertmanager.ListAllChannels(ctx)
if err != nil {
render.Error(rw, err)
return
@@ -119,7 +118,7 @@ func (api *API) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, channels)
}
func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -147,7 +146,7 @@ func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
return
}
channel, err := api.alertmanager.GetChannelByID(ctx, claims.OrgID, id)
channel, err := handler.alertmanager.GetChannelByID(ctx, claims.OrgID, id)
if err != nil {
render.Error(rw, err)
return
@@ -156,7 +155,7 @@ func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, channel)
}
func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -197,7 +196,7 @@ func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
return
}
err = api.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
err = handler.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
if err != nil {
render.Error(rw, err)
return
@@ -206,7 +205,7 @@ func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -234,7 +233,7 @@ func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
return
}
err = api.alertmanager.DeleteChannelByID(ctx, claims.OrgID, id)
err = handler.alertmanager.DeleteChannelByID(ctx, claims.OrgID, id)
if err != nil {
render.Error(rw, err)
return
@@ -243,7 +242,7 @@ func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) CreateChannel(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -266,7 +265,7 @@ func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
return
}
channel, err := api.alertmanager.CreateChannel(ctx, claims.OrgID, receiver)
channel, err := handler.alertmanager.CreateChannel(ctx, claims.OrgID, receiver)
if err != nil {
render.Error(rw, err)
return
@@ -275,7 +274,7 @@ func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusCreated, channel)
}
func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -300,7 +299,7 @@ func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
return
}
result, err := api.alertmanager.CreateRoutePolicy(ctx, &policy)
result, err := handler.alertmanager.CreateRoutePolicy(ctx, &policy)
if err != nil {
render.Error(rw, err)
return
@@ -309,11 +308,11 @@ func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusCreated, result)
}
func (api *API) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
policies, err := api.alertmanager.GetAllRoutePolicies(ctx)
policies, err := handler.alertmanager.GetAllRoutePolicies(ctx)
if err != nil {
render.Error(rw, err)
return
@@ -322,7 +321,7 @@ func (api *API) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, policies)
}
func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -333,7 +332,7 @@ func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
return
}
policy, err := api.alertmanager.GetRoutePolicyByID(ctx, policyID)
policy, err := handler.alertmanager.GetRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
@@ -342,7 +341,7 @@ func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, policy)
}
func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -353,7 +352,7 @@ func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request)
return
}
err := api.alertmanager.DeleteRoutePolicyByID(ctx, policyID)
err := handler.alertmanager.DeleteRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
@@ -362,7 +361,7 @@ func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request)
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
func (handler *handler) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -392,7 +391,7 @@ func (api *API) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
return
}
result, err := api.alertmanager.UpdateRoutePolicyByID(ctx, policyID, &policy)
result, err := handler.alertmanager.UpdateRoutePolicyByID(ctx, policyID, &policy)
if err != nil {
render.Error(rw, err)
return

View File

@@ -309,8 +309,8 @@ func (provider *provider) CreateRoutePolicy(ctx context.Context, routeRequest *a
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
@@ -365,8 +365,8 @@ func (provider *provider) CreateRoutePolicies(ctx context.Context, routeRequests
results = append(results, &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
@@ -405,8 +405,8 @@ func (provider *provider) GetRoutePolicyByID(ctx context.Context, routeID string
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
@@ -439,8 +439,8 @@ func (provider *provider) GetAllRoutePolicies(ctx context.Context) ([]*alertmana
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
@@ -508,8 +508,8 @@ func (provider *provider) UpdateRoutePolicyByID(ctx context.Context, routeID str
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *route,
ID: updatedRoute.ID.StringValue(),
CreatedAt: &updatedRoute.CreatedAt,
UpdatedAt: &updatedRoute.UpdatedAt,
CreatedAt: updatedRoute.CreatedAt,
UpdatedAt: updatedRoute.UpdatedAt,
CreatedBy: &updatedRoute.CreatedBy,
UpdatedBy: &updatedRoute.UpdatedBy,
}, nil

View File

@@ -0,0 +1,235 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/gorilla/mux"
)
func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.ListChannels), handler.OpenAPIDef{
ID: "ListChannels",
Tags: []string{"channels"},
Summary: "List notification channels",
Description: "This endpoint lists all notification channels for the organization",
Request: nil,
RequestContentType: "",
Response: make([]*alertmanagertypes.Channel, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetChannelByID), handler.OpenAPIDef{
ID: "GetChannelByID",
Tags: []string{"channels"},
Summary: "Get notification channel by ID",
Description: "This endpoint returns a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.CreateChannel), handler.OpenAPIDef{
ID: "CreateChannel",
Tags: []string{"channels"},
Summary: "Create notification channel",
Description: "This endpoint creates a notification channel",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID), handler.OpenAPIDef{
ID: "UpdateChannelByID",
Tags: []string{"channels"},
Summary: "Update notification channel",
Description: "This endpoint updates a notification channel by ID",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID), handler.OpenAPIDef{
ID: "DeleteChannelByID",
Tags: []string{"channels"},
Summary: "Delete notification channel",
Description: "This endpoint deletes a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/test", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannel",
Tags: []string{"channels"},
Summary: "Test notification channel",
Description: "This endpoint tests a notification channel by sending a test notification",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/testChannel", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannelDeprecated",
Tags: []string{"channels"},
Summary: "Test notification channel (deprecated)",
Description: "Deprecated: use /api/v1/channels/test instead",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAllRoutePolicies), handler.OpenAPIDef{
ID: "GetAllRoutePolicies",
Tags: []string{"routepolicies"},
Summary: "List route policies",
Description: "This endpoint lists all route policies for the organization",
Request: nil,
RequestContentType: "",
Response: make([]*alertmanagertypes.GettableRoutePolicy, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetRoutePolicyByID), handler.OpenAPIDef{
ID: "GetRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Get route policy by ID",
Description: "This endpoint returns a route policy by ID",
Request: nil,
RequestContentType: "",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy), handler.OpenAPIDef{
ID: "CreateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Create route policy",
Description: "This endpoint creates a route policy",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy), handler.OpenAPIDef{
ID: "UpdateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Update route policy",
Description: "This endpoint updates a route policy by ID",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID), handler.OpenAPIDef{
ID: "DeleteRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Delete route policy",
Description: "This endpoint deletes a route policy by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/alerts", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAlerts), handler.OpenAPIDef{
ID: "GetAlerts",
Tags: []string{"alerts"},
Summary: "Get alerts",
Description: "This endpoint returns alerts for the organization",
Request: nil,
RequestContentType: "",
Response: make(alertmanagertypes.DeprecatedGettableAlerts, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -3,6 +3,7 @@ package signozapiserver
import (
"context"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/factory"
@@ -57,6 +58,7 @@ type provider struct {
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
alertmanagerHandler alertmanager.Handler
}
func NewFactory(
@@ -83,6 +85,7 @@ func NewFactory(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -112,6 +115,7 @@ func NewFactory(
factoryHandler,
cloudIntegrationHandler,
ruleStateHistoryHandler,
alertmanagerHandler,
)
})
}
@@ -143,6 +147,7 @@ func newProvider(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -172,6 +177,7 @@ func newProvider(
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
alertmanagerHandler: alertmanagerHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -272,6 +278,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addAlertmanagerRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -172,7 +172,7 @@ func Ast(cause error, typ typ) bool {
return t == typ
}
// Asc checks if the provided error matches the specified custom error code.
// Ast checks if the provided error matches the specified custom error code.
func Asc(cause error, code Code) bool {
_, c, _, _, _, _ := Unwrapb(cause)

View File

@@ -2,6 +2,8 @@ package global
import (
"net/url"
"path"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
@@ -37,5 +39,34 @@ func newConfig() factory.Config {
}
func (c Config) Validate() error {
if c.ExternalURL != nil {
if c.ExternalURL.Path != "" && c.ExternalURL.Path != "/" {
if !strings.HasPrefix(c.ExternalURL.Path, "/") {
return errors.NewInvalidInputf(ErrCodeInvalidGlobalConfig, "global::external_url path must start with '/', got %q", c.ExternalURL.Path)
}
}
}
return nil
}
func (c Config) ExternalPath() string {
if c.ExternalURL == nil || c.ExternalURL.Path == "" || c.ExternalURL.Path == "/" {
return ""
}
p := path.Clean("/" + c.ExternalURL.Path)
if p == "/" {
return ""
}
return p
}
func (c Config) ExternalPathTrailing() string {
if p := c.ExternalPath(); p != "" {
return p + "/"
}
return "/"
}

139
pkg/global/config_test.go Normal file
View File

@@ -0,0 +1,139 @@
package global
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExternalPath(t *testing.T) {
testCases := []struct {
name string
config Config
expected string
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
expected: "",
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: ""}},
expected: "",
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/"}},
expected: "",
},
{
name: "SingleSegment",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: "/signoz",
},
{
name: "TrailingSlash",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz/"}},
expected: "/signoz",
},
{
name: "MultiSegment",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/a/b/c"}},
expected: "/a/b/c",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.config.ExternalPath())
})
}
}
func TestExternalPathTrailing(t *testing.T) {
testCases := []struct {
name string
config Config
expected string
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
expected: "/",
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Path: ""}},
expected: "/",
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Path: "/"}},
expected: "/",
},
{
name: "SingleSegment",
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
expected: "/signoz/",
},
{
name: "MultiSegment",
config: Config{ExternalURL: &url.URL{Path: "/a/b/c"}},
expected: "/a/b/c/",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.config.ExternalPathTrailing())
})
}
}
func TestValidate(t *testing.T) {
testCases := []struct {
name string
config Config
fail bool
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
fail: false,
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Path: ""}},
fail: false,
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Path: "/"}},
fail: false,
},
{
name: "ValidPath",
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
fail: false,
},
{
name: "NoLeadingSlash",
config: Config{ExternalURL: &url.URL{Path: "signoz"}},
fail: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.config.Validate()
if tc.fail {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

View File

@@ -22,7 +22,7 @@ func newConfig() factory.Config {
Agent: AgentConfig{
// we will maintain the latest version of cloud integration agent from here,
// till we automate it externally or figure out a way to validate it.
Version: "v0.0.8",
Version: "v0.0.9",
},
}
}

View File

@@ -99,7 +99,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\nGROUP BY cloud_account_id",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\nGROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -115,7 +115,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_region') as cloud_region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY cloud_region\n",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`\n",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -158,10 +158,10 @@
"id": "b282d9f1",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -171,10 +171,10 @@
"id": "71837c70",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -221,18 +221,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
@@ -364,10 +364,10 @@
"id": "448b551a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -377,10 +377,10 @@
"id": "a8821216",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -427,18 +427,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
@@ -570,10 +570,10 @@
"id": "702a8765",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -583,10 +583,10 @@
"id": "32985f2d",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -625,18 +625,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -776,10 +776,10 @@
"id": "5807a1e3",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -789,10 +789,10 @@
"id": "0dd63d0c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -826,18 +826,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
@@ -969,10 +969,10 @@
"id": "72c256c0",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -982,10 +982,10 @@
"id": "b433c2a1",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1019,18 +1019,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
@@ -1162,10 +1162,10 @@
"id": "9226a37c",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1175,10 +1175,10 @@
"id": "c3ff0c8f",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1221,18 +1221,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
@@ -1364,10 +1364,10 @@
"id": "20627274",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1377,10 +1377,10 @@
"id": "cd861e27",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1401,18 +1401,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
@@ -1544,10 +1544,10 @@
"id": "7d4a3494",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1557,10 +1557,10 @@
"id": "3c307858",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1594,18 +1594,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
@@ -1737,10 +1737,10 @@
"id": "a416e862",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1750,10 +1750,10 @@
"id": "ed7d0a39",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1774,18 +1774,18 @@
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],

View File

@@ -393,7 +393,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as `cloud_account_id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\nGROUP BY `cloud_account_id`",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\nGROUP BY `cloud.account.id`\n\n",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -409,7 +409,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_region') as `cloud_region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY `cloud_region`",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`\n",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -471,10 +471,10 @@
"id": "81918ce2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -484,10 +484,10 @@
"id": "114c7ff4",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -522,6 +522,7 @@
"id": "rest-requests-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -577,10 +578,10 @@
"id": "3c67d1fc",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -590,10 +591,10 @@
"id": "e2a96f23",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -628,6 +629,7 @@
"id": "rest-latency-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -683,10 +685,10 @@
"id": "b3ebaf28",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -696,10 +698,10 @@
"id": "f2030d94",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -734,6 +736,7 @@
"id": "rest-5xx-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -789,10 +792,10 @@
"id": "5f2f2892",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -802,10 +805,10 @@
"id": "960ee6d2",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -840,6 +843,7 @@
"id": "rest-integ-latency-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -895,10 +899,10 @@
"id": "8ec09e30",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -908,10 +912,10 @@
"id": "d1622884",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -946,6 +950,7 @@
"id": "rest-cache-hit-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1001,10 +1006,10 @@
"id": "f02d484d",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1014,10 +1019,10 @@
"id": "4d8ddc75",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1052,6 +1057,7 @@
"id": "rest-cache-miss-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1113,10 +1119,10 @@
"id": "http2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1126,10 +1132,10 @@
"id": "http3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1164,6 +1170,7 @@
"id": "http-count-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1219,10 +1226,10 @@
"id": "http4xx2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1232,10 +1239,10 @@
"id": "http4xx3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1270,6 +1277,7 @@
"id": "http-4xx-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1325,10 +1333,10 @@
"id": "http5xx2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1338,10 +1346,10 @@
"id": "http5xx3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1376,6 +1384,7 @@
"id": "http-5xx-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1431,10 +1440,10 @@
"id": "httplat2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1444,10 +1453,10 @@
"id": "httplat3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1482,6 +1491,7 @@
"id": "http-latency-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1537,10 +1547,10 @@
"id": "httpdata2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1550,10 +1560,10 @@
"id": "httpdata3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1588,6 +1598,7 @@
"id": "http-data-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1649,10 +1660,10 @@
"id": "wscon2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1662,10 +1673,10 @@
"id": "wscon3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1700,6 +1711,7 @@
"id": "ws-connect-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1755,10 +1767,10 @@
"id": "wsmsg2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1768,10 +1780,10 @@
"id": "wsmsg3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1806,6 +1818,7 @@
"id": "ws-message-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1861,10 +1874,10 @@
"id": "wscli2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1874,10 +1887,10 @@
"id": "wscli3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1912,6 +1925,7 @@
"id": "ws-client-error-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -1967,10 +1981,10 @@
"id": "wsexec2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1980,10 +1994,10 @@
"id": "wsexec3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2018,6 +2032,7 @@
"id": "ws-exec-error-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -2073,10 +2088,10 @@
"id": "wsint2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2086,10 +2101,10 @@
"id": "wsint3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2124,6 +2139,7 @@
"id": "ws-integ-error-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
@@ -2185,10 +2201,10 @@
"id": "commonintlat2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2198,10 +2214,10 @@
"id": "commonintlat3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2257,6 +2273,7 @@
"id": "common-integ-latency-query",
"queryType": "builder"
},
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,

View File

@@ -153,7 +153,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -169,7 +169,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -184,7 +184,7 @@
"multiSelect": true,
"name": "Table",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TableName') AS table FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}} and table != '' GROUP BY table\n",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TableName') AS table FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}} and table != '' GROUP BY table\n",
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
@@ -228,10 +228,10 @@
"id": "fc55895c",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -241,10 +241,10 @@
"id": "8b3f3e0b",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -384,10 +384,10 @@
"id": "f7b176f8",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -397,10 +397,10 @@
"id": "9a023ab7",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -540,10 +540,10 @@
"id": "ec5ebf95",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -553,10 +553,10 @@
"id": "5b2fb00e",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -696,10 +696,10 @@
"id": "3815cf09",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -709,10 +709,10 @@
"id": "a783bd91",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -852,10 +852,10 @@
"id": "edcbcb83",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -865,10 +865,10 @@
"id": "224766cb",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1008,10 +1008,10 @@
"id": "c237482a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1021,10 +1021,10 @@
"id": "e3a117d5",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1164,10 +1164,10 @@
"id": "b867513b",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1177,10 +1177,10 @@
"id": "9c10cbaa",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1344,10 +1344,10 @@
"id": "7e2aa806",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1357,10 +1357,10 @@
"id": "dd49e062",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1524,10 +1524,10 @@
"id": "b3e029fa",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1537,10 +1537,10 @@
"id": "e6764d50",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1680,10 +1680,10 @@
"id": "80ba9142",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1693,10 +1693,10 @@
"id": "9c802cf0",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1836,10 +1836,10 @@
"id": "db6edb77",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1849,10 +1849,10 @@
"id": "8b86de4a",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2016,10 +2016,10 @@
"id": "93bef7f0",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2029,10 +2029,10 @@
"id": "4a293ec8",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2196,10 +2196,10 @@
"id": "28fcd3cd",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2209,10 +2209,10 @@
"id": "619578e5",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2376,10 +2376,10 @@
"id": "5a060b5e",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2389,10 +2389,10 @@
"id": "3a1cb5ff",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2532,10 +2532,10 @@
"id": "58bc06b3",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2545,10 +2545,10 @@
"id": "d6d7a8fb",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2654,4 +2654,4 @@
"yAxisUnit": "none"
}
]
}
}

View File

@@ -72,7 +72,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\nGROUP BY cloud_account_id",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\nGROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -87,7 +87,7 @@
"multiSelect": false,
"name": "Region",
"order": 0,
"queryValue": "\nSELECT JSONExtractString(labels, 'cloud_region') as cloud_region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY cloud_region",
"queryValue": "\nSELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -130,10 +130,10 @@
"id": "d302d50d",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -143,10 +143,10 @@
"id": "e6c54e87",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -156,10 +156,10 @@
"id": "7907211a",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -172,31 +172,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}}",
"legend": "{{service.instance.id}}",
"limit": null,
"orderBy": [],
"queryName": "A",
@@ -323,10 +323,10 @@
"id": "30ded0dc",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -336,10 +336,10 @@
"id": "c935f6ec",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -349,10 +349,10 @@
"id": "d092fef8",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -365,31 +365,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}}",
"legend": "{{service.instance.id}}",
"limit": null,
"orderBy": [],
"queryName": "A",
@@ -516,10 +516,10 @@
"id": "a5fbfa4a",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -529,10 +529,10 @@
"id": "87071f13",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -542,10 +542,10 @@
"id": "c84a88c4",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -558,31 +558,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}} - Reads",
"legend": "{{service.instance.id}} - Reads",
"limit": null,
"orderBy": [],
"queryName": "A",
@@ -610,10 +610,10 @@
"id": "4d10ca4b",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -623,10 +623,10 @@
"id": "fc2db932",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -636,10 +636,10 @@
"id": "a3fd74c0",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -652,31 +652,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}} - Writes",
"legend": "{{service.instance.id}} - Writes",
"limit": null,
"orderBy": [],
"queryName": "B",
@@ -803,10 +803,10 @@
"id": "85d84806",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -816,10 +816,10 @@
"id": "f2074606",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -829,10 +829,10 @@
"id": "134c7ca9",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -845,31 +845,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}} - Reads",
"legend": "{{service.instance.id}} - Reads",
"limit": null,
"orderBy": [],
"queryName": "A",
@@ -897,10 +897,10 @@
"id": "47e0c00f",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -910,10 +910,10 @@
"id": "0a157dfe",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -923,10 +923,10 @@
"id": "a7d1e8df",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -939,31 +939,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}} - Writes",
"legend": "{{service.instance.id}} - Writes",
"limit": null,
"orderBy": [],
"queryName": "B",
@@ -1090,10 +1090,10 @@
"id": "12d6748d",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -1103,10 +1103,10 @@
"id": "df3a8da1",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1116,10 +1116,10 @@
"id": "81ec53f4",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1132,31 +1132,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}}",
"legend": "{{service.instance.id}}",
"limit": null,
"orderBy": [],
"queryName": "A",
@@ -1283,10 +1283,10 @@
"id": "d301aaa7",
"key": {
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
"op": "!=",
@@ -1296,10 +1296,10 @@
"id": "e8afaa3b",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1309,10 +1309,10 @@
"id": "d67487ab",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1325,31 +1325,31 @@
"groupBy": [
{
"dataType": "string",
"id": "service_instance_id--string--tag--false",
"id": "service.instance.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "service_instance_id",
"key": "service.instance.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
}
],
"having": [],
"legend": "{{service_instance_id}}",
"legend": "{{service.instance.id}}",
"limit": null,
"orderBy": [],
"queryName": "A",
@@ -1443,4 +1443,4 @@
"yAxisUnit": "binBps"
}
]
}
}

View File

@@ -100,7 +100,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -116,7 +116,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -132,7 +132,7 @@
"multiSelect": false,
"name": "Cluster",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_ECS%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}}\nGROUP BY cluster",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_ECS%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY cluster",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -178,10 +178,10 @@
"id": "c002d3ea",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -191,10 +191,10 @@
"id": "d95dc93f",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -358,10 +358,10 @@
"id": "8ae50256",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -371,10 +371,10 @@
"id": "dada2be4",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -538,10 +538,10 @@
"id": "840f6a82",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -551,10 +551,10 @@
"id": "e494eace",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -711,10 +711,10 @@
"id": "98cf55a2",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -724,10 +724,10 @@
"id": "dc2591e8",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -880,10 +880,10 @@
"id": "6d3fb70d",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -893,10 +893,10 @@
"id": "763ec68f",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1049,10 +1049,10 @@
"id": "4cabe614",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1062,10 +1062,10 @@
"id": "077e09db",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1218,10 +1218,10 @@
"id": "8e15b10c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1231,10 +1231,10 @@
"id": "92d56544",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1411,10 +1411,10 @@
"id": "6a1059e5",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1424,10 +1424,10 @@
"id": "fe0d40de",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1604,10 +1604,10 @@
"id": "89f0e499",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1617,10 +1617,10 @@
"id": "91ce3091",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1805,10 +1805,10 @@
"id": "edef4331",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1818,10 +1818,10 @@
"id": "d6081c36",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1962,4 +1962,4 @@
"yAxisUnit": "Bps"
}
]
}
}

View File

@@ -121,7 +121,7 @@
"customValue": "",
"defaultValue": "",
"description": "",
"dynamicVariablesAttribute": "cloud_region",
"dynamicVariablesAttribute": "cloud.region",
"dynamicVariablesSource": "Metrics",
"id": "3e37f808-da29-4f42-b9fd-ed0c5a9f55af",
"key": "3e37f808-da29-4f42-b9fd-ed0c5a9f55af",
@@ -159,7 +159,7 @@
"customValue": "",
"defaultValue": "",
"description": "",
"dynamicVariablesAttribute": "cloud_account_id",
"dynamicVariablesAttribute": "cloud.account.id",
"dynamicVariablesSource": "Metrics",
"id": "ae760815-3966-421c-97ac-f7793e524779",
"key": "ae760815-3966-421c-97ac-f7793e524779",
@@ -232,7 +232,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [],
@@ -364,7 +364,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service"
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service"
},
"functions": [],
"groupBy": [],
@@ -496,7 +496,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [],
@@ -628,7 +628,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service"
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service"
},
"functions": [],
"groupBy": [],
@@ -760,7 +760,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [
@@ -901,7 +901,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [
@@ -1042,7 +1042,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [
@@ -1183,7 +1183,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [
@@ -1324,7 +1324,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [
@@ -1465,7 +1465,7 @@
"disabled": false,
"expression": "A",
"filter": {
"expression": "cloud_account_id = $Account AND cloud_region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
"expression": "cloud.account.id = $Account AND cloud.region = $Region AND ClusterName = $Cluster AND ServiceName = $Service AND TaskId in $TaskId AND TaskId != \"\""
},
"functions": [],
"groupBy": [
@@ -1572,5 +1572,5 @@
"yAxisUnit": "Bps"
}
],
"uuid": "019ada51-9b8a-75b8-aa67-8fd35114855c"
"uuid": "019add91-55bd-778b-8459-60e19b691b6e"
}

View File

@@ -54,7 +54,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -70,7 +70,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -86,7 +86,7 @@
"multiSelect": true,
"name": "Cluster",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}}\nGROUP BY cluster",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY cluster",
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
@@ -130,10 +130,10 @@
"id": "26ac617d",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -143,10 +143,10 @@
"id": "57172ed9",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -318,10 +318,10 @@
"id": "cd4b8848",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -331,10 +331,10 @@
"id": "aa5115c6",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -506,10 +506,10 @@
"id": "2c13c8ee",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -519,10 +519,10 @@
"id": "f489f6a8",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -694,10 +694,10 @@
"id": "758ba906",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -707,10 +707,10 @@
"id": "4ffe6bf7",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -848,4 +848,4 @@
"yAxisUnit": "none"
}
]
}
}

View File

@@ -1,851 +0,0 @@
{
"description": "View key AWS ECS metrics with an out of the box dashboard.\n",
"image":"data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%2280px%22%20height%3D%2280px%22%20viewBox%3D%220%200%2080%2080%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3C!--%20Generator%3A%20Sketch%2064%20(93537)%20-%20https%3A%2F%2Fsketch.com%20--%3E%3Ctitle%3EIcon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%3C%2Ftitle%3E%3Cdesc%3ECreated%20with%20Sketch.%3C%2Fdesc%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%23C8511B%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23FF9900%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20id%3D%22Icon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20id%3D%22Icon-Architecture-BG%2F64%2FContainers%22%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2280%22%20height%3D%2280%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M64%2C48.2340095%20L56%2C43.4330117%20L56%2C32.0000169%20C56%2C31.6440171%2055.812%2C31.3150172%2055.504%2C31.1360173%20L44%2C24.4260204%20L44%2C14.7520248%20L64%2C26.5710194%20L64%2C48.2340095%20Z%20M65.509%2C25.13902%20L43.509%2C12.139026%20C43.199%2C11.9560261%2042.818%2C11.9540261%2042.504%2C12.131026%20C42.193%2C12.3090259%2042%2C12.6410257%2042%2C13.0000256%20L42%2C25.0000201%20C42%2C25.3550199%2042.189%2C25.6840198%2042.496%2C25.8640197%20L54%2C32.5740166%20L54%2C44.0000114%20C54%2C44.3510113%2054.185%2C44.6770111%2054.486%2C44.857011%20L64.486%2C50.8570083%20C64.644%2C50.9520082%2064.822%2C51%2065%2C51%20C65.17%2C51%2065.34%2C50.9570082%2065.493%2C50.8700083%20C65.807%2C50.6930084%2066%2C50.3600085%2066%2C50%20L66%2C26.0000196%20C66%2C25.6460198%2065.814%2C25.31902%2065.509%2C25.13902%20L65.509%2C25.13902%20Z%20M40.445%2C66.863001%20L17%2C54.3990067%20L17%2C26.5710194%20L37%2C14.7520248%20L37%2C24.4510204%20L26.463%2C31.1560173%20C26.175%2C31.3400172%2026%2C31.6580171%2026%2C32.0000169%20L26%2C49.0000091%20C26%2C49.373009%2026.208%2C49.7150088%2026.538%2C49.8870087%20L39.991%2C56.8870055%20C40.28%2C57.0370055%2040.624%2C57.0380055%2040.912%2C56.8880055%20L53.964%2C50.1440086%20L61.996%2C54.9640064%20L40.445%2C66.863001%20Z%20M64.515%2C54.1420068%20L54.515%2C48.1420095%20C54.217%2C47.9640096%2053.849%2C47.9520096%2053.541%2C48.1120095%20L40.455%2C54.8730065%20L28%2C48.3930094%20L28%2C32.5490167%20L38.537%2C25.8440197%20C38.825%2C25.6600198%2039%2C25.3420199%2039%2C25.0000201%20L39%2C13.0000256%20C39%2C12.6410257%2038.808%2C12.3090259%2038.496%2C12.131026%20C38.184%2C11.9540261%2037.802%2C11.9560261%2037.491%2C12.139026%20L15.491%2C25.13902%20C15.187%2C25.31902%2015%2C25.6460198%2015%2C26.0000196%20L15%2C55%20C15%2C55.3690062%2015.204%2C55.7090061%2015.53%2C55.883006%20L39.984%2C68.8830001%20C40.131%2C68.961%2040.292%2C69%2040.453%2C69%20C40.62%2C69%2040.786%2C68.958%2040.937%2C68.8750001%20L64.484%2C55.875006%20C64.797%2C55.7020061%2064.993%2C55.3750062%2065.0001416%2C55.0180064%20C65.006%2C54.6600066%2064.821%2C54.3260067%2064.515%2C54.1420068%20L64.515%2C54.1420068%20Z%22%20id%3D%22Amazon-Elastic-Container-Service_Icon_64_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E",
"layout": [
{
"h": 6,
"i": "f78becf8-0328-48b4-84b6-ff4dac325940",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 0
},
{
"h": 6,
"i": "2b4eac06-b426-4f78-b874-2e1734c4104b",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 0
},
{
"h": 6,
"i": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 6
},
{
"h": 6,
"i": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 6
}
],
"panelMap": {},
"tags": [],
"title": "AWS ECS Overview",
"uploadedGrafana": false,
"variables": {
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
"allSelected": false,
"customValue": "",
"description": "AWS Account ID",
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
"modificationUUID": "7b814d17-8fff-4ed6-a4ea-90e3b1a97584",
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
"type": "QUERY"
},
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
"allSelected": false,
"customValue": "",
"description": "Account Region",
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
"modificationUUID": "3b5f499b-22a3-4c8a-847c-8d3811c9e6b2",
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
"type": "QUERY"
},
"bfbdbcbe-a168-4d81-b108-36339e249116": {
"allSelected": true,
"customValue": "",
"description": "ECS Cluster Name",
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
"key": "bfbdbcbe-a168-4d81-b108-36339e249116",
"modificationUUID": "9fb0d63c-ac6c-497d-82b3-17d95944e245",
"multiSelect": true,
"name": "Cluster",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY cluster",
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
"type": "QUERY"
}
},
"version": "v4",
"widgets": [
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "f78becf8-0328-48b4-84b6-ff4dac325940",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_ECS_MemoryUtilization_max--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_ECS_MemoryUtilization_max",
"type": "Gauge"
},
"aggregateOperator": "max",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "26ac617d",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "57172ed9",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
},
{
"id": "49b9f85e",
"key": {
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
},
"op": "in",
"value": [
"$Cluster"
]
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "ServiceName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ServiceName",
"type": "tag"
},
{
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
}
],
"having": [],
"legend": "{{ServiceName}} ({{ClusterName}})",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "max",
"stepInterval": 60,
"timeAggregation": "max"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "56068fdd-d523-4117-92fa-87c6518ad07c",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Maximum Memory Utilization",
"yAxisUnit": "none"
},
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "2b4eac06-b426-4f78-b874-2e1734c4104b",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_ECS_MemoryUtilization_min--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_ECS_MemoryUtilization_min",
"type": "Gauge"
},
"aggregateOperator": "min",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "cd4b8848",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "aa5115c6",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
},
{
"id": "f60677b6",
"key": {
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
},
"op": "in",
"value": [
"$Cluster"
]
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "ServiceName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ServiceName",
"type": "tag"
},
{
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
}
],
"having": [],
"legend": "{{ServiceName}} ({{ClusterName}})",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "min",
"stepInterval": 60,
"timeAggregation": "min"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "fb19342e-cbde-40d8-b12f-ad108698356b",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Minimum Memory Utilization",
"yAxisUnit": "none"
},
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_ECS_CPUUtilization_max--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_ECS_CPUUtilization_max",
"type": "Gauge"
},
"aggregateOperator": "max",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "2c13c8ee",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "f489f6a8",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
},
{
"id": "94012320",
"key": {
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
},
"op": "in",
"value": [
"$Cluster"
]
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "ServiceName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ServiceName",
"type": "tag"
},
{
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
}
],
"having": [],
"legend": "{{ServiceName}} ({{ClusterName}})",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "max",
"stepInterval": 60,
"timeAggregation": "max"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "273e0a76-c780-4b9a-9b03-2649d4227173",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Maximum CPU Utilization",
"yAxisUnit": "none"
},
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_ECS_CPUUtilization_min--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_ECS_CPUUtilization_min",
"type": "Gauge"
},
"aggregateOperator": "min",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "758ba906",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "4ffe6bf7",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
},
{
"id": "53d98059",
"key": {
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
},
"op": "in",
"value": [
"$Cluster"
]
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "ServiceName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ServiceName",
"type": "tag"
},
{
"dataType": "string",
"id": "ClusterName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "ClusterName",
"type": "tag"
}
],
"having": [],
"legend": "{{ServiceName}} ({{ClusterName}})",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "min",
"stepInterval": 60,
"timeAggregation": "min"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "c89482b3-5a98-4e2c-be0d-ef036d7dac05",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Minimum CPU Utilization",
"yAxisUnit": "none"
}
]
}

View File

@@ -99,7 +99,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_ContainerInsights%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_ContainerInsights%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -115,7 +115,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_ContainerInsights%' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_ContainerInsights%' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -130,7 +130,7 @@
"multiSelect": true,
"name": "Namespace",
"order": 3,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'Namespace') AS namespace FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_ContainerInsights%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}} AND JSONExtractString(labels, 'ClusterName') IN {{.Cluster}} and namespace != '' GROUP BY namespace\n",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'Namespace') AS namespace FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_ContainerInsights%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}} AND JSONExtractString(labels, 'ClusterName') IN {{.Cluster}} and namespace != '' GROUP BY namespace\n",
"showALLOption": true,
"sort": "DISABLED",
"textboxValue": "",
@@ -146,7 +146,7 @@
"multiSelect": false,
"name": "Cluster",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS table FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_ContainerInsights%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}} and table != '' GROUP BY table\n",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS table FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_ContainerInsights%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}} and table != '' GROUP BY table\n",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -190,10 +190,10 @@
"id": "3c9d3220",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -203,10 +203,10 @@
"id": "c7ec3ea4",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -406,10 +406,10 @@
"id": "84a456ab",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -419,10 +419,10 @@
"id": "4317ebfb",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -622,10 +622,10 @@
"id": "a93d165c",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -635,10 +635,10 @@
"id": "99c79a76",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -815,10 +815,10 @@
"id": "afed4306",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -828,10 +828,10 @@
"id": "92ab51e8",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -880,7 +880,7 @@
}
],
"having": [],
"legend": " {{NodeName}} ",
"legend": "{{NodeName}}",
"limit": null,
"orderBy": [],
"queryName": "A",
@@ -1008,10 +1008,10 @@
"id": "1020db2d",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1021,10 +1021,10 @@
"id": "c74e1d9c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1101,10 +1101,10 @@
"id": "6e9e1a4a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1114,10 +1114,10 @@
"id": "4f6ba883",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1317,10 +1317,10 @@
"id": "09854d1f",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1330,10 +1330,10 @@
"id": "2d14e3fa",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1410,10 +1410,10 @@
"id": "02e9e011",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1423,10 +1423,10 @@
"id": "690694fd",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1626,10 +1626,10 @@
"id": "a8e7b55b",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1639,10 +1639,10 @@
"id": "6e34744c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1817,10 +1817,10 @@
"id": "76fe4793",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1830,10 +1830,10 @@
"id": "b39cec30",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2033,10 +2033,10 @@
"id": "b00a5918",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2046,10 +2046,10 @@
"id": "e2ad047a",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2179,4 +2179,4 @@
"yAxisUnit": "none"
}
]
}
}

View File

@@ -64,7 +64,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_EKS%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_EKS%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -80,7 +80,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_EKS%' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_EKS%' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -94,7 +94,7 @@
"multiSelect": false,
"name": "Cluster",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS table FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_EKS%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}} and table != '' GROUP BY table\n",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster_name FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_EKS%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}} and cluster_name != '' GROUP BY cluster_name\n",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -139,10 +139,10 @@
"id": "cd48d097",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -152,10 +152,10 @@
"id": "e15d2193",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -319,10 +319,10 @@
"id": "ceb37a7e",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -332,10 +332,10 @@
"id": "43d6f274",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -399,10 +399,10 @@
"id": "ba6a48c6",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -412,10 +412,10 @@
"id": "fe1a0cd2",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -579,10 +579,10 @@
"id": "fd1d1922",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -592,10 +592,10 @@
"id": "d7eb2ac6",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -659,10 +659,10 @@
"id": "f1d20f31",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -672,10 +672,10 @@
"id": "ce660d67",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -739,10 +739,10 @@
"id": "cd6566ff",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -752,10 +752,10 @@
"id": "e582ca67",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -919,10 +919,10 @@
"id": "cdd3d571",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -932,10 +932,10 @@
"id": "badba5d2",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -999,10 +999,10 @@
"id": "07af73bd",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1012,10 +1012,10 @@
"id": "a5792c41",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1179,10 +1179,10 @@
"id": "9272ad56",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1192,10 +1192,10 @@
"id": "1b6292ad",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1259,10 +1259,10 @@
"id": "37b6b2c2",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1272,10 +1272,10 @@
"id": "904415fa",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1339,10 +1339,10 @@
"id": "3c7ff964",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1352,10 +1352,10 @@
"id": "945a29cd",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1519,10 +1519,10 @@
"id": "506f5d0b",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1532,10 +1532,10 @@
"id": "1ea4e5ea",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1599,10 +1599,10 @@
"id": "42761bd5",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1612,10 +1612,10 @@
"id": "80d9d3ff",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1745,4 +1745,4 @@
"yAxisUnit": "none"
}
]
}
}

View File

@@ -143,7 +143,7 @@
"multiSelect": true,
"name": "Cluster",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'CacheClusterId') AS cluster FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}} AND cluster != '' GROUP BY cluster",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'CacheClusterId') AS cluster FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}} AND cluster != '' GROUP BY cluster",
"showALLOption": true,
"sort": "DISABLED",
"textboxValue": "",
@@ -159,7 +159,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -175,7 +175,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id` FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -234,10 +234,10 @@
"id": "a2e662bc",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -247,10 +247,10 @@
"id": "b7391c4e",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -414,10 +414,10 @@
"id": "0c3a5e7a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -427,10 +427,10 @@
"id": "de00d7b3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -594,10 +594,10 @@
"id": "7087d8df",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -607,10 +607,10 @@
"id": "26bd4c51",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -774,10 +774,10 @@
"id": "63918715",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -787,10 +787,10 @@
"id": "647e4dbe",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -954,10 +954,10 @@
"id": "72e562a1",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -967,10 +967,10 @@
"id": "26891e8d",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1134,10 +1134,10 @@
"id": "c6612aef",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1147,10 +1147,10 @@
"id": "b863daa8",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1314,10 +1314,10 @@
"id": "05806d0e",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1327,10 +1327,10 @@
"id": "edfe2e2c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1494,10 +1494,10 @@
"id": "088c9b4a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1507,10 +1507,10 @@
"id": "1a86c392",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1672,10 +1672,10 @@
"id": "10a254a8",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1685,10 +1685,10 @@
"id": "310ce219",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1852,10 +1852,10 @@
"id": "a3ab9c6c",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1865,10 +1865,10 @@
"id": "cfd6d4ee",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2032,10 +2032,10 @@
"id": "0e7a0149",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2045,10 +2045,10 @@
"id": "1440550c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2197,10 +2197,10 @@
"id": "43ef42eb",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2225,10 +2225,10 @@
"id": "4cc11182",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2377,10 +2377,10 @@
"id": "87b3e146",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2405,10 +2405,10 @@
"id": "07a5a0d1",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2557,10 +2557,10 @@
"id": "6764e117",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -2585,10 +2585,10 @@
"id": "a1c90d55",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -2703,4 +2703,4 @@
"yAxisUnit": "none"
}
]
}
}

View File

@@ -81,7 +81,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_Lambda_Invocations_sum'\nGROUP BY cloud_account_id\n\n",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_Lambda_Invocations_sum'\nGROUP BY `cloud.account.id`\n\n",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -96,7 +96,7 @@
"multiSelect": false,
"name": "Region",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_region') as cloud_region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_Lambda_Invocations_sum'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY cloud_region\n",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_Lambda_Invocations_sum'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`\n",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -139,10 +139,10 @@
"id": "49d33567",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -152,10 +152,10 @@
"id": "b9dfa1c9",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -190,18 +190,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -240,7 +240,7 @@
"disabled": false,
"legend": "",
"name": "A",
"query": "aws_Lambda_Invocations_sum{cloud_region=\"us-east-2\"}"
"query": "{\"aws_Lambda_Invocations_sum\",\"cloud.region\"=\"us-east-2\"}"
}
],
"queryType": "builder"
@@ -341,10 +341,10 @@
"id": "af05252d",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -354,10 +354,10 @@
"id": "983efea5",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -396,18 +396,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -446,7 +446,7 @@
"disabled": false,
"legend": "",
"name": "A",
"query": "aws_Lambda_Invocations_sum{cloud_region=\"us-east-2\"}"
"query": "{\"aws_Lambda_Invocations_sum\", \"cloud.region\"=\"us-east-2\"}"
}
],
"queryType": "builder"
@@ -547,10 +547,10 @@
"id": "c67262c9",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -560,10 +560,10 @@
"id": "c5ccbbf4",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -602,18 +602,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -652,7 +652,7 @@
"disabled": false,
"legend": "",
"name": "A",
"query": "aws_Lambda_Invocations_sum{cloud_region=\"us-east-2\"}"
"query": "{\"aws_Lambda_Invocations_sum\", \"cloud.region\"=\"us-east-2\"}"
}
],
"queryType": "builder"
@@ -753,10 +753,10 @@
"id": "6c956b7d",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -766,10 +766,10 @@
"id": "5fef840b",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -808,18 +808,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -858,7 +858,7 @@
"disabled": false,
"legend": "",
"name": "A",
"query": "aws_Lambda_Invocations_sum{cloud_region=\"us-east-2\"}"
"query": "{\"aws_Lambda_Invocations_sum\",\"cloud.region\"=\"us-east-2\"}"
}
],
"queryType": "builder"
@@ -959,10 +959,10 @@
"id": "f4c6246b",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -972,10 +972,10 @@
"id": "5b7a75a1",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1014,18 +1014,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1064,7 +1064,7 @@
"disabled": false,
"legend": "",
"name": "A",
"query": "aws_Lambda_Invocations_sum{cloud_region=\"us-east-2\"}"
"query": "{\"aws_Lambda_Invocations_sum\",\"cloud.region=\"us-east-2\"}"
}
],
"queryType": "builder"
@@ -1165,10 +1165,10 @@
"id": "1aee3626",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1178,10 +1178,10 @@
"id": "11631fda",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1220,18 +1220,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1270,7 +1270,7 @@
"disabled": false,
"legend": "",
"name": "A",
"query": "aws_Lambda_Invocations_sum{cloud_region=\"us-east-2\"}"
"query": "{\"aws_Lambda_Invocations_sum\",\"cloud.region=\"us-east-2\"}"
}
],
"queryType": "builder"
@@ -1371,10 +1371,10 @@
"id": "a8c65389",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1384,10 +1384,10 @@
"id": "2ab205c8",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1426,18 +1426,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1476,7 +1476,7 @@
"disabled": false,
"legend": "",
"name": "A",
"query": "aws_Lambda_Invocations_sum{cloud_region=\"us-east-2\"}"
"query": "{\"aws_Lambda_Invocations_sum\",\"cloud.region\"=\"us-east-2\"}"
}
],
"queryType": "builder"

View File

@@ -72,7 +72,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_Kafka_KafkaDataLogsDiskUsed_max' GROUP BY cloud_account_id\n",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_Kafka_KafkaDataLogsDiskUsed_max' GROUP BY `cloud.account.id`\n",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -87,7 +87,7 @@
"multiSelect": false,
"name": "Region",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_Kafka_KafkaDataLogsDiskUsed_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region\n",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_Kafka_KafkaDataLogsDiskUsed_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region\n",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -143,10 +143,10 @@
"id": "8b658843",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -156,10 +156,10 @@
"id": "9cbc21ee",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -328,10 +328,10 @@
"id": "754c3c99",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -341,10 +341,10 @@
"id": "09ad3a79",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -513,10 +513,10 @@
"id": "3e5db1d7",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -526,10 +526,10 @@
"id": "9e9bf94c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -698,10 +698,10 @@
"id": "2233f9a5",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -711,10 +711,10 @@
"id": "52bd69d4",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -870,10 +870,10 @@
"id": "a00425be",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -896,10 +896,10 @@
"id": "02adea69",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1055,10 +1055,10 @@
"id": "0626eebd",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1068,10 +1068,10 @@
"id": "b633d867",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1186,4 +1186,4 @@
"yAxisUnit": "none"
}
]
}
}

View File

@@ -90,7 +90,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_RDS_CPUUtilization_sum'\nGROUP BY cloud_account_id",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_RDS_CPUUtilization_sum'\nGROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -106,7 +106,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT JSONExtractString(labels, 'cloud_region') as cloud_region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_RDS_CPUUtilization_sum'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY cloud_region",
"queryValue": "SELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_RDS_CPUUtilization_sum'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -149,10 +149,10 @@
"id": "f8e72efc",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -162,10 +162,10 @@
"id": "4e68256a",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -191,18 +191,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -342,10 +342,10 @@
"id": "723ba84a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -355,10 +355,10 @@
"id": "f8227b55",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -384,18 +384,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -535,10 +535,10 @@
"id": "31191f74",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -548,10 +548,10 @@
"id": "aa644bbf",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -577,18 +577,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -728,10 +728,10 @@
"id": "83f232af",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -741,10 +741,10 @@
"id": "2677873f",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -770,18 +770,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -822,10 +822,10 @@
"id": "e2df7981",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -835,10 +835,10 @@
"id": "6daad748",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -864,18 +864,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1015,10 +1015,10 @@
"id": "17142c3d",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1028,10 +1028,10 @@
"id": "27fcc87d",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1057,18 +1057,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1109,10 +1109,10 @@
"id": "a050e23a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1122,10 +1122,10 @@
"id": "6df80990",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1151,18 +1151,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1302,10 +1302,10 @@
"id": "a298d4bd",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1315,10 +1315,10 @@
"id": "810c0586",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1344,18 +1344,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1396,10 +1396,10 @@
"id": "d46f3f53",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1409,10 +1409,10 @@
"id": "4ec47a19",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1438,18 +1438,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1589,10 +1589,10 @@
"id": "f64f0096",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1602,10 +1602,10 @@
"id": "18ad7c1e",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1631,18 +1631,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1782,10 +1782,10 @@
"id": "7786e529",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1795,10 +1795,10 @@
"id": "f27b4616",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1824,18 +1824,18 @@
"groupBy": [
{
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
{
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
{
@@ -1942,4 +1942,4 @@
"yAxisUnit": "bytes"
}
]
}
}

View File

@@ -54,7 +54,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -70,7 +70,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -85,7 +85,7 @@
"multiSelect": true,
"name": "Topic",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TopicName') AS topic\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}}\nGROUP BY topic",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TopicName') AS topic\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY topic",
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
@@ -144,10 +144,10 @@
"id": "b18187c3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -157,10 +157,10 @@
"id": "eebe4578",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -324,10 +324,10 @@
"id": "62255cff",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -337,10 +337,10 @@
"id": "17c7153e",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -504,10 +504,10 @@
"id": "8ca86829",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -517,10 +517,10 @@
"id": "8a444f66",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -684,10 +684,10 @@
"id": "e2084931",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -697,10 +697,10 @@
"id": "0b05209a",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -815,4 +815,4 @@
"yAxisUnit": "none"
}
]
}
}

View File

@@ -1,818 +0,0 @@
{
"description": "View key AWS SNS metrics with an out of the box dashboard.",
"image": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDY0ICg5MzUzNykgLSBodHRwczovL3NrZXRjaC5jb20gLS0+CiAgICA8dGl0bGU+SWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BV1MtU2ltcGxlLU5vdGlmaWNhdGlvbi1TZXJ2aWNlXzY0PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+CiAgICAgICAgPGxpbmVhckdyYWRpZW50IHgxPSIwJSIgeTE9IjEwMCUiIHgyPSIxMDAlIiB5Mj0iMCUiIGlkPSJsaW5lYXJHcmFkaWVudC0xIj4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0IwMDg0RCIgb2Zmc2V0PSIwJSI+PC9zdG9wPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjRkY0RjhCIiBvZmZzZXQ9IjEwMCUiPjwvc3RvcD4KICAgICAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPC9kZWZzPgogICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLzY0L0FyY2hfQVdTLVNpbXBsZS1Ob3RpZmljYXRpb24tU2VydmljZV82NCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLUJHLzY0L0FwcGxpY2F0aW9uLUludGVncmF0aW9uIiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50LTEpIj4KICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIj48L3JlY3Q+CiAgICAgICAgPC9nPgogICAgICAgIDxwYXRoIGQ9Ik0xNywzOCBDMTguMTAzLDM4IDE5LDM4Ljg5NyAxOSw0MCBDMTksNDEuMTAzIDE4LjEwMyw0MiAxNyw0MiBDMTUuODk3LDQyIDE1LDQxLjEwMyAxNSw0MCBDMTUsMzguODk3IDE1Ljg5NywzOCAxNywzOCBMMTcsMzggWiBNNDEsNjQgQzI5LjMxNCw2NCAxOS4yODksNTUuNDY2IDE3LjE5NCw0My45OCBDMTguOTY1LDQzLjg5NCAyMC40MjcsNDIuNjU5IDIwLjg1Nyw0MSBMMjcsNDEgTDI3LDM5IEwyMC44NTcsMzkgQzIwLjQyNywzNy4zNDIgMTguOTY2LDM2LjEwNyAxNy4xOTUsMzYuMDIgQzE5LjI4NSwyNC43MSAyOS41MTEsMTYgNDEsMTYgQzQ1LjMxMywxNiA0OS44MzIsMTcuNjIyIDU0LjQyOSwyMC44MjEgTDU1LjU3MSwxOS4xNzkgQzUwLjYzMywxNS43NDMgNDUuNzMsMTQgNDEsMTQgQzI4LjI3LDE0IDE2Ljk0OSwyMy44NjUgMTUuMDYzLDM2LjUyMSBDMTMuODM5LDM3LjIwNyAxMywzOC41IDEzLDQwIEMxMyw0MS41IDEzLjgzOSw0Mi43OTMgMTUuMDYzLDQzLjQ3OCBDMTYuOTcsNTYuMzQxIDI4LjA1Niw2NiA0MSw2NiBDNDYuNDA3LDY2IDUxLjk0Miw2NC4xNTcgNTYuNTg1LDYwLjgxMSBMNTUuNDE1LDU5LjE4OSBDNTEuMTEsNjIuMjkyIDQ1Ljk5MSw2NCA0MSw2NCBMNDEsNjQgWiBNMzAuMTAxLDM2LjQ0MiBDMzEuOTU1LDM2Ljg5NSAzNC4yNzUsMzcgMzYsMzcgQzM3LjY0MiwzNyAzOS44MjMsMzYuOTA1IDQxLjYyOSwzNi41MDYgTDM3LjEwNSw0NS41NTMgQzM3LjAzNiw0NS42OTEgMzcsNDUuODQ1IDM3LDQ2IEwzNyw1MC40NTMgQzM2LjE5OSw1MC45NjQgMzQuODMzLDUxLjgxMiAzNCw1MS45ODYgTDM0LDQ2IEMzNCw0NS44NjggMzMuOTc0LDQ1LjczNyAzMy45MjMsNDUuNjE1IEwzMC4xMDEsMzYuNDQyIFogTTM2LDMzIEM0MC4wMjUsMzMgNDIuMTc0LDMzLjYwNCA0Mi44NDEsMzQgQzQyLjE3NCwzNC4zOTYgNDAuMDI1LDM1IDM2LDM1IEMzMS45NzUsMzUgMjkuODI2LDM0LjM5NiAyOS4xNTksMzQgQzI5LjgyNiwzMy42MDQgMzEuOTc1LDMzIDM2LDMzIEwzNiwzMyBaIE0zMyw1NCBMMzQsNTQgQzM0LjA0Myw1NCAzNC4wODYsNTMuOTk3IDM0LjEyOCw1My45OTIgQzM1LjM1Miw1My44MzMgMzYuOTA5LDUyLjg4NyAzOC4yNzIsNTIuMDEzIEwzOC41MzUsNTEuODQ1IEMzOC44MjQsNTEuNjYxIDM5LDUxLjM0MiAzOSw1MSBMMzksNDYuMjM2IEw0NC41NTksMzUuMTIgQzQ0LjgzMywzNC44MDEgNDUsMzQuNDM0IDQ1LDM0IEM0NSwzMS4zOSAzOS4zNjEsMzEgMzYsMzEgQzMyLjYzOSwzMSAyNywzMS4zOSAyNywzNCBDMjcsMzQuMzY2IDI3LjEyLDM0LjY4NCAyNy4zMiwzNC45NjcgTDMyLDQ2LjIgTDMyLDUzIEMzMiw1My41NTIgMzIuNDQ3LDU0IDMzLDU0IEwzMyw1NCBaIE02Miw1MyBDNjMuMTAzLDUzIDY0LDUzLjg5NyA2NCw1NSBDNjQsNTYuMTAzIDYzLjEwMyw1NyA2Miw1NyBDNjAuODk3LDU3IDYwLDU2LjEwMyA2MCw1NSBDNjAsNTMuODk3IDYwLjg5Nyw1MyA2Miw1MyBMNjIsNTMgWiBNNjIsMjMgQzYzLjEwMywyMyA2NCwyMy44OTcgNjQsMjUgQzY0LDI2LjEwMyA2My4xMDMsMjcgNjIsMjcgQzYwLjg5NywyNyA2MCwyNi4xMDMgNjAsMjUgQzYwLDIzLjg5NyA2MC44OTcsMjMgNjIsMjMgTDYyLDIzIFogTTY0LDM4IEM2NS4xMDMsMzggNjYsMzguODk3IDY2LDQwIEM2Niw0MS4xMDMgNjUuMTAzLDQyIDY0LDQyIEM2Mi44OTcsNDIgNjIsNDEuMTAzIDYyLDQwIEM2MiwzOC44OTcgNjIuODk3LDM4IDY0LDM4IEw2NCwzOCBaIE01NCw0MSBMNjAuMTQzLDQxIEM2MC41ODksNDIuNzIgNjIuMTQyLDQ0IDY0LDQ0IEM2Ni4yMDYsNDQgNjgsNDIuMjA2IDY4LDQwIEM2OCwzNy43OTQgNjYuMjA2LDM2IDY0LDM2IEM2Mi4xNDIsMzYgNjAuNTg5LDM3LjI4IDYwLjE0MywzOSBMNTQsMzkgTDU0LDI2IEw1OC4xNDMsMjYgQzU4LjU4OSwyNy43MiA2MC4xNDIsMjkgNjIsMjkgQzY0LjIwNiwyOSA2NiwyNy4yMDYgNjYsMjUgQzY2LDIyLjc5NCA2NC4yMDYsMjEgNjIsMjEgQzYwLjE0MiwyMSA1OC41ODksMjIuMjggNTguMTQzLDI0IEw1MywyNCBDNTIuNDQ3LDI0IDUyLDI0LjQ0OCA1MiwyNSBMNTIsMzkgTDQ1LDM5IEw0NSw0MSBMNTIsNDEgTDUyLDU1IEM1Miw1NS41NTIgNTIuNDQ3LDU2IDUzLDU2IEw1OC4xNDMsNTYgQzU4LjU4OSw1Ny43MiA2MC4xNDIsNTkgNjIsNTkgQzY0LjIwNiw1OSA2Niw1Ny4yMDYgNjYsNTUgQzY2LDUyLjc5NCA2NC4yMDYsNTEgNjIsNTEgQzYwLjE0Miw1MSA1OC41ODksNTIuMjggNTguMTQzLDU0IEw1NCw1NCBMNTQsNDEgWiIgaWQ9IkFXUy1TaW1wbGUtTm90aWZpY2F0aW9uLVNlcnZpY2VfSWNvbl82NF9TcXVpZCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgPC9nPgo8L3N2Zz4=",
"layout": [
{
"h": 6,
"i": "4eb87f89-0213-4773-9b06-6aecc6701898",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 0
},
{
"h": 6,
"i": "7a010b4e-ea7c-4a45-a9eb-93af650c45b4",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 0
},
{
"h": 6,
"i": "2299d4e3-6c40-4bf2-a550-c7bb8a7acd38",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 6
},
{
"h": 6,
"i": "16eec8b7-de1a-4039-b180-24c7a6704b6e",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 6
}
],
"panelMap": {},
"tags": [],
"title": "SNS Overview",
"uploadedGrafana": false,
"variables": {
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
"allSelected": false,
"customValue": "",
"description": "AWS Account ID",
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
"modificationUUID": "b7a6b06b-fa1f-4fb8-b70e-6bd9b350f29e",
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
"type": "QUERY"
},
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
"allSelected": false,
"customValue": "",
"description": "Account Region",
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
"modificationUUID": "8428a5de-bfd1-4a69-9601-63e3041cd556",
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
"type": "QUERY"
},
"bfbdbcbe-a168-4d81-b108-36339e249116": {
"allSelected": true,
"customValue": "",
"description": "SNS Topic Name",
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
"modificationUUID": "dfed7272-16dc-4eb6-99bf-7c82fc8e04f0",
"multiSelect": true,
"name": "Topic",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TopicName') AS topic\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY topic",
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
"type": "QUERY"
}
},
"version": "v4",
"widgets": [
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "4eb87f89-0213-4773-9b06-6aecc6701898",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_SNS_NumberOfMessagesPublished_max--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_SNS_NumberOfMessagesPublished_max",
"type": "Gauge"
},
"aggregateOperator": "max",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "8fd51b53",
"key": {
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
},
"op": "in",
"value": [
"$Topic"
]
},
{
"id": "b18187c3",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "eebe4578",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
}
],
"having": [],
"legend": "{{TopicName}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "max",
"stepInterval": 60,
"timeAggregation": "max"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "9c67615a-55f7-42da-835c-86922f2ff8bb",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Number of Messages Published",
"yAxisUnit": "none"
},
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "7a010b4e-ea7c-4a45-a9eb-93af650c45b4",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_SNS_PublishSize_max--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_SNS_PublishSize_max",
"type": "Gauge"
},
"aggregateOperator": "max",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "1aa0d1a9",
"key": {
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
},
"op": "in",
"value": [
"$Topic"
]
},
{
"id": "62255cff",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "17c7153e",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
}
],
"having": [],
"legend": "{{TopicName}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "max",
"stepInterval": 60,
"timeAggregation": "max"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "a635a15b-dfe6-4617-a82e-29d93e27deaf",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Published Message Size",
"yAxisUnit": "decbytes"
},
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "2299d4e3-6c40-4bf2-a550-c7bb8a7acd38",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_SNS_NumberOfNotificationsDelivered_max--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_SNS_NumberOfNotificationsDelivered_max",
"type": "Gauge"
},
"aggregateOperator": "max",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "c96a4ac0",
"key": {
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
},
"op": "in",
"value": [
"$Topic"
]
},
{
"id": "8ca86829",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "8a444f66",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
}
],
"having": [],
"legend": "{{TopicName}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "max",
"stepInterval": 60,
"timeAggregation": "max"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "0d2fc26c-9b21-4dfc-b631-64b7c8d3bd71",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Number of Notifications Delivered",
"yAxisUnit": "none"
},
{
"bucketCount": 30,
"bucketWidth": 0,
"columnUnits": {},
"description": "",
"fillSpans": false,
"id": "16eec8b7-de1a-4039-b180-24c7a6704b6e",
"isLogScale": false,
"isStacked": false,
"mergeAllActiveQueries": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregateAttribute": {
"dataType": "float64",
"id": "aws_SNS_NumberOfNotificationsFailed_max--float64--Gauge--true",
"isColumn": true,
"isJSON": false,
"key": "aws_SNS_NumberOfNotificationsFailed_max",
"type": "Gauge"
},
"aggregateOperator": "max",
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filters": {
"items": [
{
"id": "6175f3d5",
"key": {
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
},
"op": "in",
"value": [
"$Topic"
]
},
{
"id": "e2084931",
"key": {
"dataType": "string",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.region",
"type": "tag"
},
"op": "=",
"value": "$Region"
},
{
"id": "0b05209a",
"key": {
"dataType": "string",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
"value": "$Account"
}
],
"op": "AND"
},
"functions": [],
"groupBy": [
{
"dataType": "string",
"id": "TopicName--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "TopicName",
"type": "tag"
}
],
"having": [],
"legend": "{{TopicName}}",
"limit": null,
"orderBy": [],
"queryName": "A",
"reduceTo": "avg",
"spaceAggregation": "max",
"stepInterval": 60,
"timeAggregation": "max"
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "526247af-6ac9-42ff-83e9-cce0e32a9e63",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"selectedLogFields": [
{
"dataType": "string",
"name": "body",
"type": ""
},
{
"dataType": "string",
"name": "timestamp",
"type": ""
}
],
"selectedTracesFields": [
{
"dataType": "string",
"id": "serviceName--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "serviceName",
"type": "tag"
},
{
"dataType": "string",
"id": "name--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "name",
"type": "tag"
},
{
"dataType": "float64",
"id": "durationNano--float64--tag--true",
"isColumn": true,
"isJSON": false,
"key": "durationNano",
"type": "tag"
},
{
"dataType": "string",
"id": "httpMethod--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "httpMethod",
"type": "tag"
},
{
"dataType": "string",
"id": "responseStatusCode--string--tag--true",
"isColumn": true,
"isJSON": false,
"key": "responseStatusCode",
"type": "tag"
}
],
"softMax": 0,
"softMin": 0,
"stackedBarChart": false,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Number of Notifications Failed",
"yAxisUnit": "none"
}
]
}

View File

@@ -96,7 +96,7 @@
"multiSelect": true,
"name": "Queue",
"order": 2,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'QueueName') AS queue\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SQS_ApproximateAgeOfOldestMessage_max' \nAND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} \nAND JSONExtractString(labels, 'cloud_region') IN {{.Region}} \nGROUP BY queue",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'QueueName') AS queue\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SQS_ApproximateAgeOfOldestMessage_max' \nAND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} \nAND JSONExtractString(labels, 'cloud.region') IN {{.Region}} \nGROUP BY queue",
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
@@ -112,7 +112,7 @@
"multiSelect": false,
"name": "Region",
"order": 1,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SQS_ApproximateAgeOfOldestMessage_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SQS_ApproximateAgeOfOldestMessage_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",
@@ -128,7 +128,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SQS_ApproximateAgeOfOldestMessage_max' GROUP BY cloud_account_id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SQS_ApproximateAgeOfOldestMessage_max' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "ASC",
"textboxValue": "",
@@ -172,10 +172,10 @@
"id": "f3faf3d7",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -185,10 +185,10 @@
"id": "e9f94e6c",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -352,10 +352,10 @@
"id": "bcad72b1",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -365,10 +365,10 @@
"id": "7e9fbca3",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -532,10 +532,10 @@
"id": "d20d64d4",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -545,10 +545,10 @@
"id": "072f1e3f",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -712,10 +712,10 @@
"id": "20a35c55",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -725,10 +725,10 @@
"id": "4d702aca",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -892,10 +892,10 @@
"id": "d6acfbea",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -905,10 +905,10 @@
"id": "c8f19331",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1072,10 +1072,10 @@
"id": "0fe5e8bd",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1085,10 +1085,10 @@
"id": "6702e7e6",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1252,10 +1252,10 @@
"id": "52cf9dd8",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1265,10 +1265,10 @@
"id": "35ebf39a",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1432,10 +1432,10 @@
"id": "b8f487f1",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1445,10 +1445,10 @@
"id": "e89032c8",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1612,10 +1612,10 @@
"id": "0711b803",
"key": {
"dataType": "string",
"id": "cloud_account_id--string--tag--false",
"id": "cloud.account.id--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_account_id",
"key": "cloud.account.id",
"type": "tag"
},
"op": "=",
@@ -1625,10 +1625,10 @@
"id": "15f30a54",
"key": {
"dataType": "string",
"id": "cloud_region--string--tag--false",
"id": "cloud.region--string--tag--false",
"isColumn": false,
"isJSON": false,
"key": "cloud_region",
"key": "cloud.region",
"type": "tag"
},
"op": "=",
@@ -1758,4 +1758,4 @@
"yAxisUnit": "decbytes"
}
]
}
}

View File

@@ -27,7 +27,6 @@ import (
"github.com/prometheus/prometheus/promql"
"github.com/SigNoz/signoz/pkg/alertmanager"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/http/render"
@@ -140,8 +139,6 @@ type APIHandler struct {
pvcsRepo *inframetrics.PvcsRepo
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
QueryParserAPI *queryparser.API
@@ -168,8 +165,6 @@ type APIHandlerOpts struct {
// Flux Interval
FluxInterval time.Duration
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
QueryParserAPI *queryparser.API
@@ -230,7 +225,6 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
statefulsetsRepo: statefulsetsRepo,
jobsRepo: jobsRepo,
pvcsRepo: pvcsRepo,
AlertmanagerAPI: opts.AlertmanagerAPI,
LicensingAPI: opts.LicensingAPI,
Signoz: opts.Signoz,
QueryParserAPI: opts.QueryParserAPI,
@@ -502,21 +496,6 @@ func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_range", am.ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/query", am.ViewAccess(aH.queryMetrics)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels", am.ViewAccess(aH.AlertmanagerAPI.ListChannels)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels/{id}", am.ViewAccess(aH.AlertmanagerAPI.GetChannelByID)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels/{id}", am.AdminAccess(aH.AlertmanagerAPI.UpdateChannelByID)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/channels/{id}", am.AdminAccess(aH.AlertmanagerAPI.DeleteChannelByID)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/channels", am.EditAccess(aH.AlertmanagerAPI.CreateChannel)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/testChannel", am.EditAccess(aH.AlertmanagerAPI.TestReceiver)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies", am.ViewAccess(aH.AlertmanagerAPI.GetAllRoutePolicies)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies/{id}", am.ViewAccess(aH.AlertmanagerAPI.GetRoutePolicyByID)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies", am.AdminAccess(aH.AlertmanagerAPI.CreateRoutePolicy)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.DeleteRoutePolicyByID)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.UpdateRoutePolicy)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/alerts", am.ViewAccess(aH.AlertmanagerAPI.GetAlerts)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)
@@ -608,7 +587,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_filter/analyze", am.ViewAccess(aH.QueryParserAPI.AnalyzeQueryFilter)).Methods(http.MethodPost)
}
func Intersection(a, b []int) (c []int) {
m := make(map[int]bool)

View File

@@ -136,7 +136,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: nooplicensing.NewLicenseAPI(),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
@@ -245,6 +244,20 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
return nil, err
}
routePrefix := s.config.Global.ExternalPath()
if routePrefix != "" {
prefixed := http.StripPrefix(routePrefix, handler)
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
r.ServeHTTP(w, req)
return
}
prefixed.ServeHTTP(w, req)
})
}
return &http.Server{
Handler: handler,
}, nil

View File

@@ -211,23 +211,8 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (int, error) {
annotations := make(ruletypes.Labels, 0, len(r.annotations.Map()))
for name, value := range r.annotations.Map() {
// no need to expand custom templating annotations — they get expanded in the notifier layer
if ruletypes.IsCustomTemplatingAnnotation(name) {
annotations = append(annotations, ruletypes.Label{Name: name, Value: value})
continue
}
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
}
// Add values to be used in notifier layer for notification templates
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationValue, Value: valueFormatter.Format(result.V, r.Unit())})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationThresholdValue, Value: threshold})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationCompareOp, Value: result.CompareOperator.Literal()})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationMatchType, Value: result.MatchType.Literal()})
if result.IsRecovering {
lb.Set(ruletypes.LabelIsRecovering, "true")
}
if result.IsMissing {
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
lb.Set(ruletypes.NoDataLabel, "true")

View File

@@ -337,23 +337,8 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
annotations := make(ruletypes.Labels, 0, len(r.annotations.Map()))
for name, value := range r.annotations.Map() {
// no need to expand custom templating annotations — they get expanded in the notifier layer
if ruletypes.IsCustomTemplatingAnnotation(name) {
annotations = append(annotations, ruletypes.Label{Name: name, Value: value})
continue
}
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
}
// Add values to be used in notifier layer for notification templates
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationValue, Value: value})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationThresholdValue, Value: threshold})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationCompareOp, Value: smpl.CompareOperator.Literal()})
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationMatchType, Value: smpl.MatchType.Literal()})
if smpl.IsRecovering {
lb.Set(ruletypes.LabelIsRecovering, "true")
}
if smpl.IsMissing {
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
lb.Set(ruletypes.NoDataLabel, "true")
@@ -367,13 +352,13 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
link := r.prepareLinksToTraces(ctx, ts, smpl.Metric)
if link != "" && r.hostFromSource() != "" {
r.logger.InfoContext(ctx, "adding traces link to annotations", slog.String("annotation.link", fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)))
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationRelatedTraces, Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)})
annotations = append(annotations, ruletypes.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)})
}
case ruletypes.AlertTypeLogs:
link := r.prepareLinksToLogs(ctx, ts, smpl.Metric)
if link != "" && r.hostFromSource() != "" {
r.logger.InfoContext(ctx, "adding logs link to annotations", slog.String("annotation.link", fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)))
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationRelatedLogs, Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)})
annotations = append(annotations, ruletypes.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)})
}
}

View File

@@ -1,6 +1,8 @@
package signoz
import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/signozauthzapi"
@@ -62,6 +64,7 @@ type Handlers struct {
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
AlertmanagerHandler alertmanager.Handler
}
func NewHandlers(
@@ -77,6 +80,7 @@ func NewHandlers(
authz authz.AuthZ,
zeusService zeus.Zeus,
registryHandler factory.Handler,
alertmanagerService alertmanager.Alertmanager,
) Handlers {
return Handlers{
SavedView: implsavedview.NewHandler(modules.SavedView),
@@ -99,5 +103,6 @@ func NewHandlers(
RegistryHandler: registryHandler,
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
}
}

View File

@@ -56,7 +56,7 @@ func TestNewHandlers(t *testing.T) {
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager)
reflectVal := reflect.ValueOf(handlers)
for i := 0; i < reflectVal.NumField(); i++ {
f := reflectVal.Field(i)

View File

@@ -7,6 +7,7 @@ import (
"os"
"reflect"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
"github.com/SigNoz/signoz/pkg/authz"
@@ -69,6 +70,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ alertmanager.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -88,9 +88,9 @@ func NewCacheProviderFactories() factory.NamedMap[factory.ProviderFactory[cache.
)
}
func NewWebProviderFactories() factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
func NewWebProviderFactories(globalConfig global.Config) factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
return factory.MustNewNamedMap(
routerweb.NewFactory(),
routerweb.NewFactory(globalConfig),
noopweb.NewFactory(),
)
}
@@ -288,6 +288,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RegistryHandler,
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.AlertmanagerHandler,
),
)
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
@@ -34,7 +35,7 @@ func TestNewProviderFactories(t *testing.T) {
})
assert.NotPanics(t, func() {
NewWebProviderFactories()
NewWebProviderFactories(global.Config{})
})
assert.NotPanics(t, func() {

View File

@@ -500,7 +500,7 @@ func New(
// Initialize all handlers for the modules
registryHandler := factory.NewHandler(registry)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager)
// Initialize the API server (after registry so it can access service health)
apiserverInstance, err := factory.NewProviderFromNamedMap(

View File

@@ -889,7 +889,12 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
// }
if fieldKeySelector.MetricContext != nil {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
}
}
conds = append(conds, sb.And(fieldConds...))
@@ -977,7 +982,12 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
fieldConds = append(fieldConds, sb.NotLike("attr_name", "\\_\\_%"))
if fieldKeySelector.MetricContext != nil {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
}
}
conds = append(conds, sb.And(fieldConds...))
@@ -1071,8 +1081,8 @@ func enrichWithIntrinsicMetricKeys(keys map[string][]*telemetrytypes.TelemetryFi
if selector.Signal != telemetrytypes.SignalMetrics && selector.Signal != telemetrytypes.SignalUnspecified {
continue
}
// If a metricName is provided, dont surface intrinsic metric keys
if selector.MetricContext != nil && selector.MetricContext.MetricName != "" {
// If metric filters are provided, do not surface intrinsic metric keys.
if selector.MetricContext != nil && (selector.MetricContext.MetricName != "" || selector.MetricContext.MetricNamespace != "") {
continue
}
@@ -1728,9 +1738,12 @@ func (t *telemetryMetaStore) getMetricFieldValues(ctx context.Context, fieldValu
sb.Where(sb.E("attr_datatype", fieldValueSelector.FieldDataType.TagDataType()))
}
if fieldValueSelector.MetricContext != nil {
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.StartUnixMilli > 0 {
sb.Where(sb.GE("last_reported_unix_milli", fieldValueSelector.StartUnixMilli))
@@ -1812,6 +1825,9 @@ func (t *telemetryMetaStore) getIntrinsicMetricFieldValues(ctx context.Context,
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.StartUnixMilli > 0 {
sb.Where(sb.GE("unix_milli", fieldValueSelector.StartUnixMilli))
@@ -1869,6 +1885,13 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
}
sb.Where(sb.NotLike("attr.1", "\\_\\_%"))
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.Value != "" {
if fieldValueSelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
sb.Where(sb.E("attr.2", fieldValueSelector.Value))

View File

@@ -320,6 +320,20 @@ func TestEnrichWithIntrinsicMetricKeys(t *testing.T) {
},
)
assert.NotContains(t, result, "metric_name")
result = enrichWithIntrinsicMetricKeys(
map[string][]*telemetrytypes.TelemetryFieldKey{},
[]*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
)
assert.NotContains(t, result, "metric_name")
}
func TestGetMetricFieldValuesIntrinsicMetricName(t *testing.T) {
@@ -392,3 +406,174 @@ func TestGetMetricFieldValuesIntrinsicBoolReturnsEmpty(t *testing.T) {
assert.Empty(t, values.BoolValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricFieldValuesAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
valueRows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_string_value", Type: "String"},
}, [][]any{{"value.a"}})
mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT attr_string_value FROM signoz_metrics.distributed_metadata WHERE attr_name = ? AND metric_name LIKE ? LIMIT ?")).
WithArgs("custom_key", "system.cpu%", 11).
WillReturnRows(valueRows)
values, complete, err := metadata.(*telemetryMetaStore).getMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Name: "custom_key",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 10,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"value.a"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricFieldValuesIntrinsicMetricNameAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
valueRows := cmock.NewRows([]cmock.ColumnType{
{Name: "metric_name", Type: "String"},
}, [][]any{{"system.cpu.utilization"}})
mock.ExpectQuery(regexp.QuoteMeta("SELECT metric_name FROM signoz_metrics.distributed_time_series_v4_1week WHERE metric_name LIKE ? GROUP BY metric_name LIMIT ?")).
WithArgs("system.cpu%", 51).
WillReturnRows(valueRows)
metadataRows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_string_value", Type: "String"},
}, [][]any{})
mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT attr_string_value FROM signoz_metrics.distributed_metadata WHERE attr_name = ? AND metric_name LIKE ? LIMIT ?")).
WithArgs("metric_name", "system.cpu%", 50).
WillReturnRows(metadataRows)
values, complete, err := metadata.(*telemetryMetaStore).getMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Name: "metric_name",
Limit: 50,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 50,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"system.cpu.utilization"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMeterSourceMetricFieldValuesAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr", Type: "Array(String)"},
}, [][]any{{[]string{"service.name", "frontend"}}})
mock.ExpectQuery(`SELECT .*distributed_samples_agg_1d.*metric_name LIKE .*`).
WithArgs("service.name", "\\_\\_%", "system.cpu%", "", 11).
WillReturnRows(rows)
values, complete, err := metadata.(*telemetryMetaStore).getMeterSourceMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Source: telemetrytypes.SourceMeter,
Name: "service.name",
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 10,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"frontend"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricsKeysAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "name", Type: "String"},
{Name: "field_context", Type: "String"},
{Name: "field_data_type", Type: "String"},
{Name: "priority", Type: "UInt8"},
}, [][]any{{"service.name", "resource", "String", 1}})
mock.ExpectQuery(`(?s)SELECT.*distributed_metadata.*metric_name LIKE.*`).
WithArgs("%service%", "\\_\\_%", "system.cpu%", 11).
WillReturnRows(rows)
keys, complete, err := metadata.(*telemetryMetaStore).getMetricsKeys(context.Background(), []*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
Name: "service",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
})
require.NoError(t, err)
assert.True(t, complete)
assert.Len(t, keys, 1)
assert.Equal(t, "service.name", keys[0].Name)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMeterSourceMetricKeysAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_name", Type: "String"},
}, [][]any{{"service.name"}})
mock.ExpectQuery(`SELECT.*distributed_samples_agg_1d.*metric_name LIKE.*`).
WithArgs("%service%", "\\_\\_%", "system.cpu%", 10).
WillReturnRows(rows)
keys, complete, err := metadata.(*telemetryMetaStore).getMeterSourceMetricKeys(context.Background(), []*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
Source: telemetrytypes.SourceMeter,
Name: "service",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
})
require.NoError(t, err)
assert.True(t, complete)
assert.Len(t, keys, 1)
assert.Equal(t, "service.name", keys[0].Name)
require.NoError(t, mock.ExpectationsWereMet())
}

View File

@@ -1,28 +0,0 @@
package markdownrenderer
import (
"bytes"
"context"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
)
// SoftLineBreakHTML is a HTML tag that is used to represent a soft line break.
const SoftLineBreakHTML = `<p></p>`
func (r *markdownRenderer) renderHTML(_ context.Context, markdown string) (string, error) {
var buf bytes.Buffer
if err := r.htmlRenderer.Convert([]byte(markdown), &buf); err != nil {
return "", errors.WrapInternalf(err, errors.CodeInternal, "failed to convert markdown to HTML")
}
// return buf.String(), nil
// TODO: check if there is another way to handle soft line breaks in HTML
// the idea with paragraph tags is that it will start the content in new
// line without using a line break tag, this works well in variety of cases
// but not all, for example, in case of code block, the paragraph tags will be added
// to the code block where newline is present.
return strings.ReplaceAll(buf.String(), "\n", SoftLineBreakHTML), nil
}

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