Compare commits

..

8 Commits

Author SHA1 Message Date
srikanthccv
2bf6f10ebe chore: temp commit 2026-05-27 00:28:51 +05:30
Srikanth Chekuri
2a7e7afc83 Merge branch 'main' into google-chat-alert-channel#5095 2026-05-21 20:36:37 +05:30
Niladri Adhikary
cab18fb844 Merge branch 'main' into google-chat-alert-channel#5095 2026-05-12 19:50:55 +05:30
Niladri Adhikary
3ed0e4f28b fix: ci lint & template test fixes
Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>
2026-05-12 19:50:29 +05:30
Niladri Adhikary
ef57a95f6c chore: remove unused import
Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>
2026-05-12 16:54:24 +05:30
Niladri Adhikary
4e33586f28 Merge branch 'main' into google-chat-alert-channel#5095 2026-05-12 13:31:57 +05:30
Niladri Adhikary
1ea76147e5 Merge branch 'main' into google-chat-alert-channel#5095 2026-05-12 13:30:48 +05:30
Niladri Adhikary
7653793e22 feat(alertmanager): add support for google chat alert channel
Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>
2026-05-12 13:01:14 +05:30
216 changed files with 68273 additions and 23292 deletions

View File

@@ -1544,6 +1544,17 @@ components:
webhook_url_file:
type: string
type: object
ConfigGoogleChatConfig:
properties:
send_resolved:
type: boolean
text:
type: string
title:
type: string
webhook_url:
$ref: '#/components/schemas/ConfigSecretURL'
type: object
ConfigMattermostAttachment:
properties:
author_icon:
@@ -1847,6 +1858,10 @@ components:
items:
$ref: '#/components/schemas/ConfigEmailConfig'
type: array
googlechat_configs:
items:
$ref: '#/components/schemas/ConfigGoogleChatConfig'
type: array
incidentio_configs:
items:
$ref: '#/components/schemas/ConfigIncidentioConfig'
@@ -18948,77 +18963,6 @@ paths:
summary: Get waterfall view for a trace
tags:
- tracedetail
/api/v4/traces/{traceID}/waterfall:
post:
deprecated: false
description: Returns the waterfall view of spans including all spans if total
spans are under a limit, a max count otherwise. Aggregations are dropped compared
to v3
operationId: GetWaterfallV4
parameters:
- in: path
name: traceID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SpantypesPostableWaterfall'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableWaterfallTrace'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get waterfall view for a trace
tags:
- tracedetail
/api/v5/query_range:
post:
deprecated: false

View File

@@ -535,7 +535,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, provider cloudintegrationtypes.CloudProviderType, service *cloudintegrationtypes.CloudIntegrationService, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
// TODO: DB calls are in for loop, can be optimized later.
for _, dashboard := range serviceDefinition.Assets.Dashboards {
slug := cloudintegrationtypes.CloudIntegrationDashboardSlug(provider, service.Type, dashboard.ID)
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, service.Type, dashboard.ID)
existing, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
@@ -562,7 +562,7 @@ func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID
// deprovisionDashboards deletes all dashboard and integration_dashboard rows for the given service.
// make sure to call this within a transaction.
func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID) error {
slugPrefix := cloudintegrationtypes.CloudIntegrationDashboardSlugPrefix(provider, serviceID)
slugPrefix := cloudintegrationtypes.IntegrationDashboardSlugPrefix(provider, serviceID)
rows, err := module.store.ListIntegrationDashboardsBySlugPrefix(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slugPrefix)
if err != nil {
return err
@@ -588,7 +588,7 @@ func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UU
// TODO: remove this hack and send idiomatic response to client.
func (module *module) enrichDashboardIDs(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
for i, d := range serviceDefinition.Assets.Dashboards {
slug := cloudintegrationtypes.CloudIntegrationDashboardSlug(provider, serviceID, d.ID)
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, serviceID, d.ID)
row, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
@@ -23,6 +24,7 @@ type APIHandlerOptions struct {
DataConnector interfaces.Reader
UsageManager *usage.Manager
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
GatewayUrl string
// Querier Influx Interval
@@ -40,6 +42,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
@@ -88,6 +91,17 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
}
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
router.HandleFunc(
"/api/v1/cloud-integrations/{cloudProvider}/accounts/generate-connection-params",
am.EditAccess(ah.CloudIntegrationsGenerateConnectionParams),
).Methods(http.MethodGet)
}
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
versionResponse := basemodel.GetVersionResponse{
Version: version.Info.Version(),

View File

@@ -30,6 +30,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
@@ -85,13 +86,20 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
// initiate opamp
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
integrationsController, err := integrations.NewController(signoz.SQLStore, signoz.Modules.Dashboard)
integrationsController, err := integrations.NewController(signoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
)
}
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
signoz.SQLStore,
@@ -126,6 +134,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
DataConnector: reader,
UsageManager: usageManager,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
GatewayUrl: config.Gateway.URL.String(),
@@ -191,6 +200,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)

View File

@@ -24,9 +24,12 @@
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
"tooltip_email_to": "Enter email addresses separated by commas.",
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
"tooltip_googlechat_url": "The URL of the Google Chat [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",
"field_googlechat_title": "Title",
"field_googlechat_description": "Description",
"field_opsgenie_api_key": "API Key",
"field_opsgenie_description": "Description",
"placeholder_opsgenie_description": "Description",
@@ -77,6 +80,7 @@
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
"webhook_url_required": "Webhook URL is mandatory",
"googlechat_webhook_url_required": "Google Chat webhook URL is mandatory",
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
"api_key_required": "API Key is mandatory",
"to_required": "To field is mandatory",

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createGoogleChat';
const create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/channels', {
name: props.name,
googlechat_configs: [
{
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,
},
],
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
throw error;
}
};
export default create;

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editGoogleChat';
const editGoogleChat = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.put<PayloadProps>(`/channels/${props.id}`, {
name: props.name,
googlechat_configs: [
{
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,
},
],
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
throw error;
}
};
export default editGoogleChat;

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createGoogleChat';
const testGoogleChat = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/testChannel', {
name: props.name,
googlechat_configs: [
{
send_resolved: true,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,
},
],
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
throw error;
}
};
export default testGoogleChat;

View File

@@ -9232,17 +9232,6 @@ export type GetWaterfall200 = {
status: string;
};
export type GetWaterfallV4PathParameters = {
traceID: string;
};
export type GetWaterfallV4200 = {
data: SpantypesGettableWaterfallTraceDTO;
/**
* @type string
*/
status: string;
};
export type QueryRangeV5200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**

View File

@@ -14,8 +14,6 @@ import type {
import type {
GetWaterfall200,
GetWaterfallPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
SpantypesPostableWaterfallDTO,
} from '../sigNoz.schemas';
@@ -122,102 +120,3 @@ export const useGetWaterfall = <
> => {
return useMutation(getGetWaterfallMutationOptions(options));
};
/**
* Returns the waterfall view of spans including all spans if total spans are under a limit, a max count otherwise. Aggregations are dropped compared to v3
* @summary Get waterfall view for a trace
*/
export const getWaterfallV4 = (
{ traceID }: GetWaterfallV4PathParameters,
spantypesPostableWaterfallDTO?: BodyType<SpantypesPostableWaterfallDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetWaterfallV4200>({
url: `/api/v4/traces/${traceID}/waterfall`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableWaterfallDTO,
signal,
});
};
export const getGetWaterfallV4MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
> => {
const mutationKey = ['getWaterfallV4'];
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 getWaterfallV4>>,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getWaterfallV4(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetWaterfallV4MutationResult = NonNullable<
Awaited<ReturnType<typeof getWaterfallV4>>
>;
export type GetWaterfallV4MutationBody =
| BodyType<SpantypesPostableWaterfallDTO>
| undefined;
export type GetWaterfallV4MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get waterfall view for a trace
*/
export const useGetWaterfallV4 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
> => {
return useMutation(getGetWaterfallV4MutationOptions(options));
};

View File

@@ -1,6 +1,8 @@
import CreateAlertChannels from 'container/CreateAlertChannels';
import { ChannelType } from 'container/CreateAlertChannels/config';
import {
googleChatTextDefaultValue,
googleChatTitleDefaultValue,
opsGenieDescriptionDefaultValue,
opsGenieMessageDefaultValue,
opsGeniePriorityDefaultValue,
@@ -419,5 +421,47 @@ describe('Create Alert Channel', () => {
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
});
});
describe('Google Chat', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
});
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
expect(screen.getByText('Google Chat')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Title label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_googlechat_title',
testId: 'title-textarea',
});
});
it('Should check if Title contains template', () => {
const titleTextArea = screen.getByTestId('title-textarea');
expect(titleTextArea).toHaveTextContent(googleChatTitleDefaultValue);
});
it('Should check if Description label and text area are displayed properly ', () => {
testLabelInputAndHelpValue({
labelText: 'field_googlechat_description',
testId: 'description-textarea',
});
});
it('Should check if Description contains template', () => {
const descriptionTextArea = screen.getByTestId('description-textarea');
expect(descriptionTextArea).toHaveTextContent(googleChatTextDefaultValue);
});
});
});
});

View File

@@ -1,6 +1,8 @@
import CreateAlertChannels from 'container/CreateAlertChannels';
import { ChannelType } from 'container/CreateAlertChannels/config';
import {
googleChatTextDefaultValue,
googleChatTitleDefaultValue,
opsGenieDescriptionDefaultValue,
opsGenieMessageDefaultValue,
opsGeniePriorityDefaultValue,
@@ -332,5 +334,42 @@ describe('Create Alert Channel (Normal User)', () => {
).toBeDisabled();
});
});
describe('Google Chat', () => {
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
});
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
expect(screen.getByText('Google Chat')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Title label and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_googlechat_title',
testId: 'title-textarea',
});
});
it('Should check if Title contains template', () => {
const titleTextArea = screen.getByTestId('title-textarea');
expect(titleTextArea).toHaveTextContent(googleChatTitleDefaultValue);
});
it('Should check if Description label and text area are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_googlechat_description',
testId: 'description-textarea',
});
});
it('Should check if Description contains template', () => {
const descriptionTextArea = screen.getByTestId('description-textarea');
expect(descriptionTextArea).toHaveTextContent(googleChatTextDefaultValue);
});
});
});
});

View File

@@ -104,6 +104,7 @@ export enum ChannelType {
Pagerduty = 'pagerduty',
Opsgenie = 'opsgenie',
MsTeams = 'msteams',
GoogleChat = 'googlechat',
}
// LabelFilterStatement will be used for preparing filter conditions / matchers
@@ -125,3 +126,9 @@ export interface MsTeamsChannel extends Channel {
title?: string;
text?: string;
}
export interface GoogleChatChannel extends Channel {
webhook_url?: string;
title?: string;
text?: string;
}

View File

@@ -1,16 +1,32 @@
import { EmailChannel, OpsgenieChannel, PagerChannel } from './config';
import {
EmailChannel,
GoogleChatChannel,
OpsgenieChannel,
PagerChannel,
} from './config';
export const PagerInitialConfig: Partial<PagerChannel> = {
description: `[{{ .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 -}}
)
description: `{{ if gt (len .Alerts.Firing) 0 -}}
Alerts Firing:
{{ range .Alerts.Firing }}
- Message: {{ .Annotations.description }}
Labels:
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }} Annotations:
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }} Source: {{ .GeneratorURL }}
{{ end }}
{{- end }}
{{ if gt (len .Alerts.Resolved) 0 -}}
Alerts Resolved:
{{ range .Alerts.Resolved }}
- Message: {{ .Annotations.description }}
Labels:
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }} Annotations:
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }} Source: {{ .GeneratorURL }}
{{ end }}
{{- end }}`,
severity: '{{ (index .Alerts 0).Labels.severity }}',
client: 'SigNoz Alert Manager',
@@ -446,3 +462,20 @@ export const EmailInitialConfig: Partial<EmailChannel> = {
</body>
</html>`,
};
export const GoogleChatInitialConfig: Partial<GoogleChatChannel> = {
title: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`,
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }}
*Severity:* {{ .Labels.severity }}{{ end }}{{ if .Annotations.summary }}
*Summary:* {{ .Annotations.summary }}{{ end }}{{ if .Annotations.description }}
*Description:* {{ .Annotations.description }}{{ end }}{{ if .Annotations.related_logs }}
*Related Logs:* {{ .Annotations.related_logs }}{{ end }}{{ if .Annotations.related_traces }}
*Related Traces:* {{ .Annotations.related_traces }}{{ end }}
*Labels:*
{{ range .Labels.SortedPairs -}}
\`{{ .Name }}\`: {{ .Value }}
{{ end }}
{{ end }}`,
};

View File

@@ -2,12 +2,14 @@ import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Form } from 'antd';
import createEmail from 'api/channels/createEmail';
import createGoogleChat from 'api/channels/createGoogleChat';
import createMsTeamsApi from 'api/channels/createMsTeams';
import createOpsgenie from 'api/channels/createOpsgenie';
import createPagerApi from 'api/channels/createPager';
import createSlackApi from 'api/channels/createSlack';
import createWebhookApi from 'api/channels/createWebhook';
import testEmail from 'api/channels/testEmail';
import testGoogleChat from 'api/channels/testGoogleChat';
import testMsTeamsApi from 'api/channels/testMsTeams';
import testOpsGenie from 'api/channels/testOpsgenie';
import testPagerApi from 'api/channels/testPager';
@@ -24,6 +26,7 @@ import APIError from 'types/api/error';
import {
ChannelType,
EmailChannel,
GoogleChatChannel,
MsTeamsChannel,
OpsgenieChannel,
PagerChannel,
@@ -33,6 +36,7 @@ import {
} from './config';
import {
EmailInitialConfig,
GoogleChatInitialConfig,
OpsgenieInitialConfig,
PagerInitialConfig,
} from './defaults';
@@ -59,6 +63,7 @@ function CreateAlertChannels({
WebhookChannel &
PagerChannel &
MsTeamsChannel &
GoogleChatChannel &
OpsgenieChannel &
EmailChannel
>
@@ -121,6 +126,14 @@ function CreateAlertChannels({
...EmailInitialConfig,
}));
}
// reset config to Google Chat defaults
if (value === ChannelType.GoogleChat && currentType !== value) {
setSelectedConfig((selectedConfig) => ({
...selectedConfig,
...GoogleChatInitialConfig,
}));
}
},
[type, selectedConfig],
);
@@ -406,7 +419,49 @@ function CreateAlertChannels({
prepareMsTeamsRequest,
showErrorModal,
]);
const prepareGoogleChatRequest = useCallback(
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),
[selectedConfig],
);
const onGoogleChatHandler = useCallback(async () => {
if (!selectedConfig.webhook_url) {
notifications.error({
message: 'Error',
description: t('googlechat_webhook_url_required'),
});
return;
}
setSavingState(true);
try {
await createGoogleChat(prepareGoogleChatRequest());
notifications.success({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(error as APIError);
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [
selectedConfig.webhook_url,
notifications,
t,
prepareGoogleChatRequest,
showErrorModal,
]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
if (!selectedConfig.name) {
@@ -424,6 +479,7 @@ function CreateAlertChannels({
[ChannelType.Opsgenie]: onOpsgenieHandler,
[ChannelType.MsTeams]: onMsTeamsHandler,
[ChannelType.Email]: onEmailHandler,
[ChannelType.GoogleChat]: onGoogleChatHandler,
};
if (isChannelType(value)) {
@@ -455,6 +511,7 @@ function CreateAlertChannels({
onOpsgenieHandler,
onMsTeamsHandler,
onEmailHandler,
onGoogleChatHandler,
notifications,
t,
],
@@ -492,6 +549,10 @@ function CreateAlertChannels({
request = prepareEmailRequest();
await testEmail(request);
break;
case ChannelType.GoogleChat:
request = prepareGoogleChatRequest();
await testGoogleChat(request);
break;
default:
notifications.error({
message: 'Error',
@@ -534,6 +595,7 @@ function CreateAlertChannels({
prepareOpsgenieRequest,
prepareSlackRequest,
prepareMsTeamsRequest,
prepareGoogleChatRequest,
prepareEmailRequest,
notifications,
],
@@ -546,6 +608,23 @@ function CreateAlertChannels({
[performChannelTest],
);
const getInitialConfigForType = (): Partial<
PagerChannel & OpsgenieChannel & EmailChannel & GoogleChatChannel
> => {
switch (type) {
case ChannelType.Pagerduty:
return PagerInitialConfig;
case ChannelType.Opsgenie:
return OpsgenieInitialConfig;
case ChannelType.Email:
return EmailInitialConfig;
case ChannelType.GoogleChat:
return GoogleChatInitialConfig;
default:
return {};
}
};
return (
<div className="create-alert-channels-container">
<FormAlertChannels
@@ -562,9 +641,7 @@ function CreateAlertChannels({
initialValue: {
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
...getInitialConfigForType(),
},
}}
/>

View File

@@ -1,282 +0,0 @@
import { Widgets } from 'types/api/dashboard/getAll';
import {
MetricRangePayloadProps,
MetricRangePayloadV3,
} from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { PanelMode } from '../../types';
import { prepareBarPanelConfig } from '../utils';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
jest.mock(
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
() => ({
getStoredSeriesVisibility: jest.fn(),
}),
);
jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
__esModule: true,
default: jest.fn().mockReturnValue({ name: 'onClickPlugin' }),
}));
jest.mock('lib/dashboard/getQueryResults', () => ({
getLegend: jest.fn(
(_queryData: unknown, _query: unknown, labelName: string) =>
`legend-${labelName}`,
),
}));
jest.mock('lib/getLabelName', () => ({
__esModule: true,
default: jest.fn(
(_metric: unknown, _queryName: string, _legend: string) => 'baseLabel',
),
}));
jest.mock(
'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils',
() => ({
getInitialStackedBands: jest.fn().mockReturnValue([]),
}),
);
const getLegendMock = jest.requireMock('lib/dashboard/getQueryResults')
.getLegend as jest.Mock;
const getLabelNameMock = jest.requireMock('lib/getLabelName')
.default as jest.Mock;
const getInitialStackedBandsMock = jest.requireMock(
'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils',
).getInitialStackedBands as jest.Mock;
const createApiResponse = (
result: MetricRangePayloadProps['data']['result'] = [],
): MetricRangePayloadProps => ({
data: {
result,
resultType: 'matrix',
newResult: null as unknown as MetricRangePayloadV3,
},
});
const createWidget = (overrides: Partial<Widgets> = {}): Widgets =>
({
id: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
thresholds: [],
customLegendColors: {},
...overrides,
}) as Widgets;
const defaultTimezone = {
name: 'UTC',
value: 'UTC',
offset: 'UTC',
searchIndex: 'UTC',
};
describe('BarPanel utils', () => {
beforeEach(() => {
jest.clearAllMocks();
getLabelNameMock.mockReturnValue('baseLabel');
getLegendMock.mockImplementation(
(_queryData: unknown, _query: unknown, labelName: string) =>
`legend-${labelName}`,
);
});
describe('prepareBarPanelData', () => {
it('returns aligned data with timestamps and empty series when result is empty', () => {
const data = prepareChartData(createApiResponse([]));
expect(data).toHaveLength(1);
expect(data[0]).toStrictEqual([]);
});
it('returns timestamps and one series of y values for single series', () => {
const data = prepareChartData(
createApiResponse([
{
metric: {},
queryName: 'Q',
legend: 'Series A',
values: [
[1000, '10'],
[2000, '20'],
],
} as MetricRangePayloadProps['data']['result'][0],
]),
);
expect(data).toHaveLength(2);
expect(data[0]).toStrictEqual([1000, 2000]);
expect(data[1]).toStrictEqual([10, 20]);
});
it('merges timestamps and fills missing values with null for multiple series', () => {
const data = prepareChartData(
createApiResponse([
{
metric: {},
queryName: 'Q1',
values: [
[1000, '1'],
[3000, '3'],
],
} as MetricRangePayloadProps['data']['result'][0],
{
metric: {},
queryName: 'Q2',
values: [
[1000, '10'],
[2000, '20'],
],
} as MetricRangePayloadProps['data']['result'][0],
]),
);
expect(data[0]).toStrictEqual([1000, 2000, 3000]);
expect(data[1]).toStrictEqual([1, null, 3]);
expect(data[2]).toStrictEqual([10, 20, null]);
});
});
describe('prepareBarPanelConfig', () => {
const baseParams = {
widget: createWidget(),
isDarkMode: true,
currentQuery: {} as Query,
onClick: jest.fn(),
onDragSelect: jest.fn(),
apiResponse: createApiResponse(),
timezone: defaultTimezone,
panelMode: PanelMode.DASHBOARD_VIEW,
};
it('adds no series when apiResponse has empty result', () => {
const config = prepareBarPanelConfig(baseParams).getConfig();
expect(config.series).toHaveLength(1);
});
it('adds one series per result item', () => {
const apiResponse = createApiResponse([
{
metric: {},
queryName: 'Q1',
values: [[1000, '1']],
} as MetricRangePayloadProps['data']['result'][0],
{
metric: {},
queryName: 'Q2',
values: [[1000, '2']],
} as MetricRangePayloadProps['data']['result'][0],
]);
const config = prepareBarPanelConfig({
...baseParams,
apiResponse,
}).getConfig();
expect(config.series).toHaveLength(3);
});
it('uses getLegend for label when currentQuery is provided', () => {
const apiResponse = createApiResponse([
{
metric: {},
queryName: 'Q1',
legend: 'L1',
values: [[1000, '1']],
} as MetricRangePayloadProps['data']['result'][0],
]);
const config = prepareBarPanelConfig({
...baseParams,
apiResponse,
currentQuery: {} as Query,
}).getConfig();
expect(getLegendMock).toHaveBeenCalled();
expect(config.series?.[1]).toMatchObject({ label: 'legend-baseLabel' });
});
it('uses getLabelName for label when currentQuery is null', () => {
getLegendMock.mockReset();
const apiResponse = createApiResponse([
{
metric: { __name__: 'requests' },
queryName: 'Q1',
values: [[1000, '1']],
} as MetricRangePayloadProps['data']['result'][0],
]);
prepareBarPanelConfig({
...baseParams,
apiResponse,
currentQuery: null as unknown as Query,
});
expect(getLabelNameMock).toHaveBeenCalled();
expect(getLegendMock).not.toHaveBeenCalled();
});
it('passes result metric to each series for cross-panel sync', () => {
const metric = { host: 'server1', __name__: 'http_requests' };
const apiResponse = createApiResponse([
{
metric,
queryName: 'Q1',
values: [[1000, '1']],
} as MetricRangePayloadProps['data']['result'][0],
]);
const config = prepareBarPanelConfig({
...baseParams,
apiResponse,
}).getConfig();
expect(config.series?.[1]).toMatchObject({ metric });
});
it('uses widget customLegendColors for series stroke', () => {
const widget = createWidget({
customLegendColors: { 'legend-baseLabel': '#ff0000' },
});
const apiResponse = createApiResponse([
{
metric: {},
queryName: 'Q',
values: [[1000, '1']],
} as MetricRangePayloadProps['data']['result'][0],
]);
const config = prepareBarPanelConfig({
...baseParams,
widget,
apiResponse,
}).getConfig();
expect(config.series?.[1]).toMatchObject({ stroke: '#ff0000' });
});
it('calls getInitialStackedBands when widget is stackedBarChart', () => {
const widget = createWidget({ stackedBarChart: true });
const apiResponse = createApiResponse([
{
metric: {},
queryName: 'Q1',
values: [[1000, '1']],
} as MetricRangePayloadProps['data']['result'][0],
{
metric: {},
queryName: 'Q2',
values: [[1000, '2']],
} as MetricRangePayloadProps['data']['result'][0],
]);
prepareBarPanelConfig({ ...baseParams, widget, apiResponse });
// seriesCount = result.length + 1 = 3
expect(getInitialStackedBandsMock).toHaveBeenCalledWith(3);
});
it('does not call getInitialStackedBands for non-stacked chart', () => {
const apiResponse = createApiResponse([
{
metric: {},
queryName: 'Q1',
values: [[1000, '1']],
} as MetricRangePayloadProps['data']['result'][0],
]);
prepareBarPanelConfig({ ...baseParams, apiResponse });
expect(getInitialStackedBandsMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -303,27 +303,6 @@ describe('TimeSeriesPanel utils', () => {
expect(seriesConfig!.stroke).toBe('#ff0000');
});
it('passes result metric to each series for cross-panel sync', () => {
const metric = { host: 'server1', __name__: 'cpu' };
const apiResponse = createApiResponse([
{
metric,
queryName: 'Q',
values: [
[1000, '1'],
[2000, '2'],
],
} as MetricRangePayloadProps['data']['result'][0],
]);
const config = prepareUPlotConfig({
...baseParams,
apiResponse,
}).getConfig();
expect(config.series?.[1]).toMatchObject({ metric });
});
it('adds multiple series when result has multiple items', () => {
const apiResponse = createApiResponse([
{

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Form } from 'antd';
import editGoogleChat from 'api/channels/editGoogleChat';
import editEmail from 'api/channels/editEmail';
import editMsTeamsApi from 'api/channels/editMsTeams';
import editOpsgenie from 'api/channels/editOpsgenie';
@@ -8,6 +9,7 @@ import editPagerApi from 'api/channels/editPager';
import editSlackApi from 'api/channels/editSlack';
import editWebhookApi from 'api/channels/editWebhook';
import testEmail from 'api/channels/testEmail';
import testGoogleChat from 'api/channels/testGoogleChat';
import testMsTeamsApi from 'api/channels/testMsTeams';
import testOpsgenie from 'api/channels/testOpsgenie';
import testPagerApi from 'api/channels/testPager';
@@ -18,6 +20,7 @@ import ROUTES from 'constants/routes';
import {
ChannelType,
EmailChannel,
GoogleChatChannel,
MsTeamsChannel,
OpsgenieChannel,
PagerChannel,
@@ -43,6 +46,7 @@ function EditAlertChannels({
WebhookChannel &
PagerChannel &
MsTeamsChannel &
GoogleChatChannel &
OpsgenieChannel &
EmailChannel
>
@@ -333,6 +337,56 @@ function EditAlertChannels({
[id, selectedConfig],
);
const prepareGoogleChatRequest = useCallback(
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
}),
[id, selectedConfig],
);
const onGoogleChatEditHandler = useCallback(async () => {
setSavingState(true);
if (selectedConfig?.webhook_url === '') {
notifications.error({
message: 'Error',
description: t('googlechat_webhook_url_required'),
});
setSavingState(false);
return {
status: 'failed',
statusMessage: t('googlechat_webhook_url_required'),
};
}
try {
await editGoogleChat(prepareGoogleChatRequest());
notifications.success({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
return {
status: 'failed',
statusMessage:
(error as APIError).getErrorMessage() || t('channel_edit_failed'),
};
} finally {
setSavingState(false);
}
}, [prepareGoogleChatRequest, notifications, selectedConfig, t]);
const onMsTeamsEditHandler = useCallback(async () => {
setSavingState(true);
@@ -383,6 +437,8 @@ function EditAlertChannels({
result = await onOpsgenieEditHandler();
} else if (value === ChannelType.Email) {
result = await onEmailEditHandler();
} else if (value === ChannelType.GoogleChat) {
result = await onGoogleChatEditHandler();
}
logEvent('Alert Channel: Save channel', {
type: value,
@@ -401,6 +457,7 @@ function EditAlertChannels({
onMsTeamsEditHandler,
onOpsgenieEditHandler,
onEmailEditHandler,
onGoogleChatEditHandler,
],
);
@@ -442,6 +499,12 @@ function EditAlertChannels({
await testEmail(request);
}
break;
case ChannelType.GoogleChat:
request = prepareGoogleChatRequest();
if (request) {
await testGoogleChat(request);
}
break;
default:
notifications.error({
message: 'Error',
@@ -484,6 +547,7 @@ function EditAlertChannels({
preparePagerRequest,
prepareSlackRequest,
prepareMsTeamsRequest,
prepareGoogleChatRequest,
prepareOpsgenieRequest,
prepareEmailRequest,
notifications,

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { GoogleChatChannel } from '../../CreateAlertChannels/config';
function GoogleChat({ setSelectedConfig }: GoogleChatProps): JSX.Element {
const { t } = useTranslation('channels');
return (
<>
<Form.Item
name="webhook_url"
label={t('field_webhook_url')}
tooltip={{
title: (
<MarkdownRenderer
markdownContent={t('tooltip_googlechat_url')}
variables={{}}
/>
),
overlayInnerStyle: { maxWidth: 400 },
placement: 'right',
}}
>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
...value,
webhook_url: event.target.value,
}));
}}
data-testid="webhook-url-textbox"
placeholder="https://chat.googleapis.com/v1/spaces/..."
/>
</Form.Item>
<Form.Item name="title" label={t('field_googlechat_title')}>
<Input.TextArea
rows={2}
onChange={(event): void =>
setSelectedConfig((value) => ({
...value,
title: event.target.value,
}))
}
data-testid="title-textarea"
placeholder={`[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`}
/>
</Form.Item>
<Form.Item name="text" label={t('field_googlechat_description')}>
<Input.TextArea
rows={4}
onChange={(event): void =>
setSelectedConfig((value) => ({
...value,
text: event.target.value,
}))
}
data-testid="description-textarea"
placeholder={t('placeholder_slack_description')}
/>
</Form.Item>
</>
);
}
interface GoogleChatProps {
setSelectedConfig: React.Dispatch<
React.SetStateAction<Partial<GoogleChatChannel>>
>;
}
export default GoogleChat;

View File

@@ -8,6 +8,7 @@ import ROUTES from 'constants/routes';
import {
ChannelType,
EmailChannel,
GoogleChatChannel,
OpsgenieChannel,
PagerChannel,
SlackChannel,
@@ -16,6 +17,7 @@ import {
import history from 'lib/history';
import EmailSettings from './Settings/Email';
import GoogleChatSettings from './Settings/GoogleChat';
import MsTeamsSettings from './Settings/MsTeams';
import OpsgenieSettings from './Settings/Opsgenie';
import PagerSettings from './Settings/Pager';
@@ -52,6 +54,8 @@ function FormAlertChannels({
return <OpsgenieSettings setSelectedConfig={setSelectedConfig} />;
case ChannelType.Email:
return <EmailSettings setSelectedConfig={setSelectedConfig} />;
case ChannelType.GoogleChat:
return <GoogleChatSettings setSelectedConfig={setSelectedConfig} />;
default:
return null;
}
@@ -128,6 +132,13 @@ function FormAlertChannels({
<Select.Option value="msteams" key="msteams" data-testid="select-option">
Microsoft Teams
</Select.Option>
<Select.Option
value="googlechat"
key="googlechat"
data-testid="select-option"
>
Google Chat
</Select.Option>
</Select>
</Form.Item>
@@ -172,6 +183,7 @@ interface FormAlertChannelsProps {
WebhookChannel &
PagerChannel &
OpsgenieChannel &
GoogleChatChannel &
EmailChannel
>
>

View File

@@ -1,296 +0,0 @@
import { renderHook } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { getViewQuery } from '../drilldownUtils';
import { AggregateData } from '../useAggregateDrilldown';
import useBaseDrilldownNavigate, {
buildDrilldownUrl,
getRoute,
} from '../useBaseDrilldownNavigate';
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: typeof mockSafeNavigate } => ({
safeNavigate: mockSafeNavigate,
}),
}));
jest.mock('../drilldownUtils', () => ({
...jest.requireActual('../drilldownUtils'),
getViewQuery: jest.fn(),
}));
const mockGetViewQuery = getViewQuery as jest.Mock;
// ─── Fixtures ────────────────────────────────────────────────────────────────
const MOCK_QUERY: Query = {
id: 'q1',
queryType: 'builder' as any,
builder: {
queryData: [
{
queryName: 'A',
dataSource: 'metrics' as any,
groupBy: [],
expression: '',
disabled: false,
functions: [],
legend: '',
having: [],
limit: null,
stepInterval: undefined,
orderBy: [],
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
};
const MOCK_VIEW_QUERY: Query = {
...MOCK_QUERY,
builder: {
...MOCK_QUERY.builder,
queryData: [
{
...MOCK_QUERY.builder.queryData[0],
filters: { items: [], op: 'AND' },
},
],
},
};
const MOCK_AGGREGATE_DATA: AggregateData = {
queryName: 'A',
filters: [{ filterKey: 'service_name', filterValue: 'auth', operator: '=' }],
timeRange: { startTime: 1000000, endTime: 2000000 },
};
// ─── getRoute ─────────────────────────────────────────────────────────────────
describe('getRoute', () => {
it.each([
['view_logs', ROUTES.LOGS_EXPLORER],
['view_metrics', ROUTES.METRICS_EXPLORER],
['view_traces', ROUTES.TRACES_EXPLORER],
])('maps %s to the correct explorer route', (key, expected) => {
expect(getRoute(key)).toBe(expected);
});
it('returns empty string for an unknown key', () => {
expect(getRoute('view_dashboard')).toBe('');
});
});
// ─── buildDrilldownUrl ────────────────────────────────────────────────────────
describe('buildDrilldownUrl', () => {
beforeEach(() => {
mockGetViewQuery.mockReturnValue(MOCK_VIEW_QUERY);
});
afterEach(() => {
jest.clearAllMocks();
});
it('returns null for an unknown drilldown key', () => {
const url = buildDrilldownUrl(
MOCK_QUERY,
MOCK_AGGREGATE_DATA,
'view_dashboard',
);
expect(url).toBeNull();
});
it('returns null when getViewQuery returns null', () => {
mockGetViewQuery.mockReturnValue(null);
const url = buildDrilldownUrl(MOCK_QUERY, MOCK_AGGREGATE_DATA, 'view_logs');
expect(url).toBeNull();
});
it('returns a URL starting with the logs explorer route for view_logs', () => {
const url = buildDrilldownUrl(MOCK_QUERY, MOCK_AGGREGATE_DATA, 'view_logs');
expect(url).not.toBeNull();
expect(url).toContain(ROUTES.LOGS_EXPLORER);
});
it('returns a URL starting with the traces explorer route for view_traces', () => {
const url = buildDrilldownUrl(MOCK_QUERY, MOCK_AGGREGATE_DATA, 'view_traces');
expect(url).toContain(ROUTES.TRACES_EXPLORER);
});
it('includes compositeQuery param in the URL', () => {
const url = buildDrilldownUrl(MOCK_QUERY, MOCK_AGGREGATE_DATA, 'view_logs');
expect(url).toContain('compositeQuery=');
});
it('includes startTime and endTime when aggregateData has a timeRange', () => {
const url = buildDrilldownUrl(MOCK_QUERY, MOCK_AGGREGATE_DATA, 'view_logs');
expect(url).toContain('startTime=1000000');
expect(url).toContain('endTime=2000000');
});
it('omits startTime and endTime when aggregateData has no timeRange', () => {
const { timeRange: _, ...withoutTimeRange } = MOCK_AGGREGATE_DATA;
const url = buildDrilldownUrl(MOCK_QUERY, withoutTimeRange, 'view_logs');
expect(url).not.toContain('startTime=');
expect(url).not.toContain('endTime=');
});
it('includes summaryFilters param for view_metrics', () => {
const url = buildDrilldownUrl(
MOCK_QUERY,
MOCK_AGGREGATE_DATA,
'view_metrics',
);
expect(url).toContain(ROUTES.METRICS_EXPLORER);
expect(url).toContain('summaryFilters=');
});
it('does not include summaryFilters param for non-metrics routes', () => {
const url = buildDrilldownUrl(MOCK_QUERY, MOCK_AGGREGATE_DATA, 'view_logs');
expect(url).not.toContain('summaryFilters=');
});
it('handles null aggregateData by passing empty filters and empty queryName', () => {
const url = buildDrilldownUrl(MOCK_QUERY, null, 'view_logs');
expect(url).not.toBeNull();
expect(mockGetViewQuery).toHaveBeenCalledWith(
MOCK_QUERY,
[],
'view_logs',
'',
);
});
it('passes aggregateData filters and queryName to getViewQuery', () => {
buildDrilldownUrl(MOCK_QUERY, MOCK_AGGREGATE_DATA, 'view_logs');
expect(mockGetViewQuery).toHaveBeenCalledWith(
MOCK_QUERY,
MOCK_AGGREGATE_DATA.filters,
'view_logs',
MOCK_AGGREGATE_DATA.queryName,
);
});
});
// ─── useBaseDrilldownNavigate ─────────────────────────────────────────────────
describe('useBaseDrilldownNavigate', () => {
beforeEach(() => {
mockGetViewQuery.mockReturnValue(MOCK_VIEW_QUERY);
});
afterEach(() => {
jest.clearAllMocks();
});
it('calls safeNavigate with the built URL on a valid key', () => {
const { result } = renderHook(() =>
useBaseDrilldownNavigate({
resolvedQuery: MOCK_QUERY,
aggregateData: MOCK_AGGREGATE_DATA,
}),
);
result.current('view_logs');
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
const [url] = mockSafeNavigate.mock.calls[0];
expect(url).toContain(ROUTES.LOGS_EXPLORER);
expect(url).toContain('compositeQuery=');
});
it('opens the explorer in a new tab', () => {
const { result } = renderHook(() =>
useBaseDrilldownNavigate({
resolvedQuery: MOCK_QUERY,
aggregateData: MOCK_AGGREGATE_DATA,
}),
);
result.current('view_traces');
expect(mockSafeNavigate).toHaveBeenCalledWith(expect.any(String), {
newTab: true,
});
});
it('calls callback after successful navigation', () => {
const callback = jest.fn();
const { result } = renderHook(() =>
useBaseDrilldownNavigate({
resolvedQuery: MOCK_QUERY,
aggregateData: MOCK_AGGREGATE_DATA,
callback,
}),
);
result.current('view_logs');
expect(callback).toHaveBeenCalledTimes(1);
});
it('does not call safeNavigate for an unknown key', () => {
const { result } = renderHook(() =>
useBaseDrilldownNavigate({
resolvedQuery: MOCK_QUERY,
aggregateData: MOCK_AGGREGATE_DATA,
}),
);
result.current('view_dashboard');
expect(mockSafeNavigate).not.toHaveBeenCalled();
});
it('still calls callback when the key is unknown', () => {
const callback = jest.fn();
const { result } = renderHook(() =>
useBaseDrilldownNavigate({
resolvedQuery: MOCK_QUERY,
aggregateData: MOCK_AGGREGATE_DATA,
callback,
}),
);
result.current('view_dashboard');
expect(callback).toHaveBeenCalledTimes(1);
expect(mockSafeNavigate).not.toHaveBeenCalled();
});
it('still calls callback when getViewQuery returns null', () => {
mockGetViewQuery.mockReturnValue(null);
const callback = jest.fn();
const { result } = renderHook(() =>
useBaseDrilldownNavigate({
resolvedQuery: MOCK_QUERY,
aggregateData: MOCK_AGGREGATE_DATA,
callback,
}),
);
result.current('view_logs');
expect(callback).toHaveBeenCalledTimes(1);
expect(mockSafeNavigate).not.toHaveBeenCalled();
});
it('handles null aggregateData without throwing', () => {
const { result } = renderHook(() =>
useBaseDrilldownNavigate({
resolvedQuery: MOCK_QUERY,
aggregateData: null,
}),
);
expect(() => result.current('view_logs')).not.toThrow();
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
});
});

View File

@@ -168,7 +168,7 @@ export const getAggregateColumnHeader = (
};
};
export const getFiltersFromMetric = (metric: any): FilterData[] =>
const getFiltersFromMetric = (metric: any): FilterData[] =>
Object.keys(metric).map((key) => ({
filterKey: key,
filterValue: metric[key],

View File

@@ -2,10 +2,14 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Link, Loader } from '@signozhq/icons';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
import { processContextLinks } from 'container/NewWidget/RightContainer/ContextLinks/utils';
import useContextVariables from 'hooks/dashboard/useContextVariables';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import ContextMenu from 'periscope/components/ContextMenu';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { ContextLinksData } from 'types/api/dashboard/getAll';
@@ -14,10 +18,9 @@ import { openInNewTab } from 'utils/navigation';
import { ContextMenuItem } from './contextConfig';
import { getDataLinks } from './dataLinksUtils';
import { getAggregateColumnHeader } from './drilldownUtils';
import { getAggregateColumnHeader, getViewQuery } from './drilldownUtils';
import { getBaseContextConfig } from './menuOptions';
import { AggregateData } from './useAggregateDrilldown';
import useBaseDrilldownNavigate from './useBaseDrilldownNavigate';
interface UseBaseAggregateOptionsProps {
query: Query;
@@ -35,6 +38,19 @@ interface BaseAggregateOptionsConfig {
items?: ContextMenuItem;
}
const getRoute = (key: string): string => {
switch (key) {
case 'view_logs':
return ROUTES.LOGS_EXPLORER;
case 'view_metrics':
return ROUTES.METRICS_EXPLORER;
case 'view_traces':
return ROUTES.TRACES_EXPLORER;
default:
return '';
}
};
const useBaseAggregateOptions = ({
query,
onClose,
@@ -70,6 +86,8 @@ const useBaseAggregateOptions = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, aggregateData, panelType]);
const { safeNavigate } = useSafeNavigate();
// Use the new useContextVariables hook
const { processedVariables } = useContextVariables({
maxValues: 2,
@@ -103,16 +121,50 @@ const useBaseAggregateOptions = ({
{label}
</ContextMenu.Item>
));
} catch {
} catch (error) {
return [];
}
}, [contextLinks, processedVariables, onClose, aggregateData, query]);
const handleBaseDrilldown = useBaseDrilldownNavigate({
resolvedQuery,
aggregateData,
callback: onClose,
});
const handleBaseDrilldown = useCallback(
(key: string): void => {
const route = getRoute(key);
const timeRange = aggregateData?.timeRange;
const filtersToAdd = aggregateData?.filters || [];
const viewQuery = getViewQuery(
resolvedQuery,
filtersToAdd,
key,
aggregateData?.queryName || '',
);
let queryParams = {
[QueryParams.compositeQuery]: encodeURIComponent(JSON.stringify(viewQuery)),
...(timeRange && {
[QueryParams.startTime]: timeRange?.startTime.toString(),
[QueryParams.endTime]: timeRange?.endTime.toString(),
}),
} as Record<string, string>;
if (route === ROUTES.METRICS_EXPLORER) {
queryParams = {
...queryParams,
[QueryParams.summaryFilters]: JSON.stringify(
viewQuery?.builder.queryData[0].filters,
),
};
}
if (route) {
safeNavigate(`${route}?${createQueryParams(queryParams)}`, {
newTab: true,
});
}
onClose();
},
[resolvedQuery, safeNavigate, onClose, aggregateData],
);
const { pathname } = useLocation();

View File

@@ -1,117 +0,0 @@
import { useCallback } from 'react';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { getViewQuery } from './drilldownUtils';
import { AggregateData } from './useAggregateDrilldown';
type DrilldownKey = 'view_logs' | 'view_metrics' | 'view_traces';
const DRILLDOWN_ROUTE_MAP: Record<DrilldownKey, string> = {
view_logs: ROUTES.LOGS_EXPLORER,
view_metrics: ROUTES.METRICS_EXPLORER,
view_traces: ROUTES.TRACES_EXPLORER,
};
const getRoute = (key: string): string =>
DRILLDOWN_ROUTE_MAP[key as DrilldownKey] ?? '';
interface UseBaseDrilldownNavigateProps {
resolvedQuery: Query;
aggregateData: AggregateData | null;
callback?: () => void;
}
const useBaseDrilldownNavigate = ({
resolvedQuery,
aggregateData,
callback,
}: UseBaseDrilldownNavigateProps): ((key: string) => void) => {
const { safeNavigate } = useSafeNavigate();
return useCallback(
(key: string): void => {
const route = getRoute(key);
const viewQuery = getViewQuery(
resolvedQuery,
aggregateData?.filters ?? [],
key,
aggregateData?.queryName ?? '',
);
if (!viewQuery || !route) {
callback?.();
return;
}
const timeRange = aggregateData?.timeRange;
let queryParams: Record<string, string> = {
[QueryParams.compositeQuery]: encodeURIComponent(JSON.stringify(viewQuery)),
...(timeRange && {
[QueryParams.startTime]: timeRange.startTime.toString(),
[QueryParams.endTime]: timeRange.endTime.toString(),
}),
};
if (route === ROUTES.METRICS_EXPLORER) {
queryParams = {
...queryParams,
[QueryParams.summaryFilters]: JSON.stringify(
viewQuery.builder.queryData[0].filters,
),
};
}
safeNavigate(`${route}?${createQueryParams(queryParams)}`, {
newTab: true,
});
callback?.();
},
[resolvedQuery, safeNavigate, callback, aggregateData],
);
};
export function buildDrilldownUrl(
resolvedQuery: Query,
aggregateData: AggregateData | null,
key: string,
): string | null {
const route = getRoute(key);
const viewQuery = getViewQuery(
resolvedQuery,
aggregateData?.filters ?? [],
key,
aggregateData?.queryName ?? '',
);
if (!viewQuery || !route) {
return null;
}
const timeRange = aggregateData?.timeRange;
let queryParams: Record<string, string> = {
[QueryParams.compositeQuery]: encodeURIComponent(JSON.stringify(viewQuery)),
...(timeRange && {
[QueryParams.startTime]: timeRange.startTime.toString(),
[QueryParams.endTime]: timeRange.endTime.toString(),
}),
};
if (route === ROUTES.METRICS_EXPLORER) {
queryParams = {
...queryParams,
[QueryParams.summaryFilters]: JSON.stringify(
viewQuery.builder.queryData[0].filters,
),
};
}
return `${route}?${createQueryParams(queryParams)}`;
}
export { getRoute };
export default useBaseDrilldownNavigate;

View File

@@ -1,29 +0,0 @@
import { syncCursorRegistry } from '../syncCursorRegistry';
describe('syncCursorRegistry', () => {
describe('metadata', () => {
it('returns undefined for unknown key', () => {
expect(syncCursorRegistry.getMetadata('unknown-meta')).toBeUndefined();
});
it('stores and retrieves metadata by syncKey', () => {
const metadata = { yAxisUnit: 'ms', groupBy: [] };
syncCursorRegistry.setMetadata('meta-key', metadata);
expect(syncCursorRegistry.getMetadata('meta-key')).toBe(metadata);
});
});
describe('activeSeriesMetric', () => {
it('returns null (not undefined) for unknown key', () => {
expect(
syncCursorRegistry.getActiveSeriesMetric('unknown-metric'),
).toBeNull();
});
it('stores and retrieves metric by syncKey', () => {
const metric = { host: 'server1', __name__: 'cpu' };
syncCursorRegistry.setActiveSeriesMetric('metric-key', metric);
expect(syncCursorRegistry.getActiveSeriesMetric('metric-key')).toBe(metric);
});
});
});

View File

@@ -1,627 +0,0 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import uPlot from 'uplot';
import { syncCursorRegistry } from '../syncCursorRegistry';
import { createSyncDisplayHook } from '../syncDisplayHook';
import {
SyncTooltipFilterMode,
type TooltipControllerState,
type TooltipSyncMetadata,
} from '../types';
jest.mock('../syncCursorRegistry', () => ({
syncCursorRegistry: {
setMetadata: jest.fn(),
getMetadata: jest.fn(),
setActiveSeriesMetric: jest.fn(),
getActiveSeriesMetric: jest.fn(),
},
}));
const mockRegistry = syncCursorRegistry as {
setMetadata: jest.Mock;
getMetadata: jest.Mock;
setActiveSeriesMetric: jest.Mock;
getActiveSeriesMetric: jest.Mock;
};
const SYNC_KEY = 'test-sync-key';
// Builds a single-query groupByPerQuery from a list of dimension keys.
const makeGroupByPerQuery = (
...keys: string[]
): Record<string, BaseAutocompleteData[]> => ({
A: keys.map((key) => ({ key, type: 'tag' as const })),
});
function makeUPlotRoot(includeCrosshair = true): HTMLElement {
const root = document.createElement('div');
if (includeCrosshair) {
const el = document.createElement('div');
el.className = 'u-cursor-y';
root.append(el);
}
return root;
}
type FakeSeries = { metric?: Record<string, string>; show?: boolean };
function makeFakeUPlot(opts: {
cursorEvent?: MouseEvent | null;
cursorLeft?: number;
series?: FakeSeries[];
includeCrosshair?: boolean;
}): uPlot {
return {
root: makeUPlotRoot(opts.includeCrosshair ?? true),
cursor: {
event: opts.cursorEvent !== undefined ? opts.cursorEvent : null,
left: opts.cursorLeft ?? 50,
},
series: opts.series ?? [
{},
{ metric: { host: 'server1' } },
{ metric: { host: 'server2' } },
],
setSeries: jest.fn(),
} as unknown as uPlot;
}
function makeController(
focusedSeriesIndex: number | null = null,
): TooltipControllerState {
return {
focusedSeriesIndex,
syncedSeriesIndexes: null,
} as TooltipControllerState;
}
// Convenience cast used throughout assertions.
function mockSetSeries(u: uPlot): jest.Mock {
return (u as unknown as { setSeries: jest.Mock }).setSeries;
}
function getCrosshair(u: uPlot): HTMLElement {
const el = u.root.querySelector<HTMLElement>('.u-cursor-y');
if (!el) {
throw new Error('crosshair element missing');
}
return el;
}
// ─────────────────────────────────────────────────────────────────────────────
describe('createSyncDisplayHook', () => {
beforeEach(() => {
jest.clearAllMocks();
});
// ── guard ────────────────────────────────────────────────────────────────
describe('no crosshair element', () => {
it('returns early without calling registry when .u-cursor-y absent', () => {
const hook = createSyncDisplayHook(SYNC_KEY, undefined, makeController());
const u = makeFakeUPlot({ includeCrosshair: false });
hook(u);
expect(mockRegistry.setMetadata).not.toHaveBeenCalled();
expect(mockRegistry.getMetadata).not.toHaveBeenCalled();
expect(mockSetSeries(u)).not.toHaveBeenCalled();
});
});
// ── source panel ─────────────────────────────────────────────────────────
describe('source behavior (cursor.event != null)', () => {
it('writes syncMetadata to registry', () => {
const syncMetadata: TooltipSyncMetadata = { yAxisUnit: 'ms' };
const hook = createSyncDisplayHook(SYNC_KEY, syncMetadata, makeController());
const u = makeFakeUPlot({ cursorEvent: new MouseEvent('mousemove') });
hook(u);
expect(mockRegistry.setMetadata).toHaveBeenCalledWith(
SYNC_KEY,
syncMetadata,
);
});
it('writes focused series metric when focusedSeriesIndex is set', () => {
const series: FakeSeries[] = [
{},
{ metric: { host: 'server1' } },
{ metric: { host: 'server2' } },
];
const hook = createSyncDisplayHook(SYNC_KEY, undefined, makeController(1));
const u = makeFakeUPlot({
cursorEvent: new MouseEvent('mousemove'),
series,
});
hook(u);
expect(mockRegistry.setActiveSeriesMetric).toHaveBeenCalledWith(SYNC_KEY, {
host: 'server1',
});
});
it('writes null metric when focusedSeriesIndex is null', () => {
const hook = createSyncDisplayHook(
SYNC_KEY,
undefined,
makeController(null),
);
const u = makeFakeUPlot({ cursorEvent: new MouseEvent('mousemove') });
hook(u);
expect(mockRegistry.setActiveSeriesMetric).toHaveBeenCalledWith(
SYNC_KEY,
null,
);
});
it('clears controller.syncedSeriesIndexes', () => {
const controller = makeController();
controller.syncedSeriesIndexes = [1, 2];
const hook = createSyncDisplayHook(SYNC_KEY, undefined, controller);
const u = makeFakeUPlot({ cursorEvent: new MouseEvent('mousemove') });
hook(u);
expect(controller.syncedSeriesIndexes).toBeNull();
});
it('shows crosshair and does not read from registry', () => {
const hook = createSyncDisplayHook(SYNC_KEY, undefined, makeController());
const u = makeFakeUPlot({ cursorEvent: new MouseEvent('mousemove') });
hook(u);
expect(getCrosshair(u).style.display).toBe('');
expect(mockRegistry.getMetadata).not.toHaveBeenCalled();
});
});
// ── receiver panel ───────────────────────────────────────────────────────
describe('receiver behavior (cursor.event is null)', () => {
describe('crosshair visibility', () => {
it('shows crosshair when yAxisUnit matches source', () => {
mockRegistry.getMetadata.mockReturnValue({ yAxisUnit: 'ms' });
mockRegistry.getActiveSeriesMetric.mockReturnValue(null);
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms' },
makeController(),
);
const u = makeFakeUPlot({ cursorEvent: null });
hook(u);
expect(getCrosshair(u).style.display).toBe('');
});
it('hides crosshair when yAxisUnit differs from source', () => {
mockRegistry.getMetadata.mockReturnValue({ yAxisUnit: 'bytes' });
mockRegistry.getActiveSeriesMetric.mockReturnValue(null);
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms' },
makeController(),
);
const u = makeFakeUPlot({ cursorEvent: null });
hook(u);
expect(getCrosshair(u).style.display).toBe('none');
});
});
// ── exact groupBy match ───────────────────────────────────────────────
describe('exact groupBy match', () => {
const groupByPerQuery = makeGroupByPerQuery('host');
const series: FakeSeries[] = [
{},
{ metric: { host: 'server1' } },
{ metric: { host: 'server2' } },
];
it('focuses the matching series and records it on the controller', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server2' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50, series });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(2, { focus: true });
expect(controller.syncedSeriesIndexes).toStrictEqual([2]);
});
it('unfocuses all and emits empty matches (Filtered) when active metric is null', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue(null);
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50, series });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(null, { focus: false });
expect(controller.syncedSeriesIndexes).toStrictEqual([]);
});
it('unfocuses all when metric matches no series', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({
host: 'unknown-server',
});
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50, series });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(null, { focus: false });
expect(controller.syncedSeriesIndexes).toStrictEqual([]);
});
it('clears syncedSeriesIndexes when cursor is off-plot (left < 0)', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: -1, series });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(null, { focus: false });
expect(controller.syncedSeriesIndexes).toBeNull();
expect(mockRegistry.getActiveSeriesMetric).not.toHaveBeenCalled();
});
it('never focuses series at index 0 (x-axis)', () => {
const sameMetric = { host: 'server1' };
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue(sameMetric);
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery },
controller,
);
const u = makeFakeUPlot({
cursorEvent: null,
cursorLeft: 50,
// Index 0 has the same metric — it must always be skipped.
series: [{ metric: sameMetric }, { metric: { host: 'other' } }],
});
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(null, { focus: false });
expect(controller.syncedSeriesIndexes).toStrictEqual([]);
});
it('skips hidden series (show === false)', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery },
controller,
);
const u = makeFakeUPlot({
cursorEvent: null,
cursorLeft: 50,
series: [
{},
{ metric: { host: 'server1' }, show: false },
{ metric: { host: 'server1' } },
],
});
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(2, { focus: true });
expect(controller.syncedSeriesIndexes).toStrictEqual([2]);
});
});
// ── partial groupBy overlap ───────────────────────────────────────────
describe('partial groupBy overlap', () => {
it('subset — records every receiver series matching on the common key', () => {
// Source groupBy=[host], receiver groupBy=[host, service].
// Hook focuses the first match; the rest are surfaced via controller.syncedSeriesIndexes.
const sourceGroupBy = makeGroupByPerQuery('host');
const receiverGroupBy = makeGroupByPerQuery('host', 'service');
const series: FakeSeries[] = [
{},
{ metric: { host: 'server1', service: 'api' } },
{ metric: { host: 'server1', service: 'frontend' } },
{ metric: { host: 'server2', service: 'api' } },
];
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery: sourceGroupBy,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery: receiverGroupBy },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50, series });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(1, { focus: true });
expect(controller.syncedSeriesIndexes).toStrictEqual([1, 2]);
});
it('superset — records the one receiver series matching on the common key', () => {
// Source groupBy=[host, service], receiver groupBy=[host].
const sourceGroupBy = makeGroupByPerQuery('host', 'service');
const receiverGroupBy = makeGroupByPerQuery('host');
const series: FakeSeries[] = [
{},
{ metric: { host: 'server1' } },
{ metric: { host: 'server2' } },
];
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery: sourceGroupBy,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({
host: 'server1',
service: 'api',
});
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery: receiverGroupBy },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50, series });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(1, { focus: true });
expect(controller.syncedSeriesIndexes).toStrictEqual([1]);
});
it('partial — matches on the intersecting key only', () => {
// Source groupBy=[host, service], receiver groupBy=[service, region].
// Common key is [service]. Both receiver series with service=api match.
const sourceGroupBy = makeGroupByPerQuery('host', 'service');
const receiverGroupBy = makeGroupByPerQuery('service', 'region');
const series: FakeSeries[] = [
{},
{ metric: { service: 'api', region: 'us-east' } },
{ metric: { service: 'api', region: 'eu-west' } },
{ metric: { service: 'frontend', region: 'us-east' } },
];
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery: sourceGroupBy,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({
host: 'server1',
service: 'api',
});
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery: receiverGroupBy },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50, series });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(1, { focus: true });
expect(controller.syncedSeriesIndexes).toStrictEqual([1, 2]);
});
});
// ── union across queries in groupByPerQuery ───────────────────────────
describe('union across queries', () => {
it("treats the panel's effective groupBy as the union across its queries", () => {
// Source has query A=[host]; receiver has A=[host], B=[service].
// The shared key is `host` — receiver matches on that.
const sourceGroupBy: Record<string, BaseAutocompleteData[]> = {
A: [{ key: 'host', type: 'tag' }],
};
const receiverGroupBy: Record<string, BaseAutocompleteData[]> = {
A: [{ key: 'host', type: 'tag' }],
B: [{ key: 'service', type: 'tag' }],
};
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery: sourceGroupBy,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery: receiverGroupBy },
controller,
);
const u = makeFakeUPlot({
cursorEvent: null,
cursorLeft: 50,
series: [
{},
{ metric: { host: 'server1' } },
{ metric: { host: 'server2' } },
],
});
hook(u);
expect(controller.syncedSeriesIndexes).toStrictEqual([1]);
});
});
// ── no overlap (Filtered mode default) ────────────────────────────────
describe('no overlap → Filtered mode emits []', () => {
it('emits [] when groupBy keys are completely different', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery: makeGroupByPerQuery('host'),
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery: makeGroupByPerQuery('service') },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null });
hook(u);
expect(controller.syncedSeriesIndexes).toStrictEqual([]);
});
it('emits [] when receiver groupBy is empty', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery: makeGroupByPerQuery('host'),
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery: {} },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null });
hook(u);
expect(controller.syncedSeriesIndexes).toStrictEqual([]);
});
it('emits [] when source groupBy is absent', () => {
mockRegistry.getMetadata.mockReturnValue({ yAxisUnit: 'ms' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms', groupByPerQuery: makeGroupByPerQuery('host') },
controller,
);
const u = makeFakeUPlot({ cursorEvent: null });
hook(u);
expect(controller.syncedSeriesIndexes).toStrictEqual([]);
});
});
// ── filterMode: All ──────────────────────────────────────────────────
describe('filterMode All', () => {
it('emits null (no filter) when there is no overlap in groupBy', () => {
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery: makeGroupByPerQuery('host'),
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{
yAxisUnit: 'ms',
groupByPerQuery: makeGroupByPerQuery('service'),
filterMode: SyncTooltipFilterMode.All,
},
controller,
);
const u = makeFakeUPlot({ cursorEvent: null });
hook(u);
expect(controller.syncedSeriesIndexes).toBeNull();
});
it('emits null when metric matches no series', () => {
const groupByPerQuery = makeGroupByPerQuery('host');
mockRegistry.getMetadata.mockReturnValue({
yAxisUnit: 'ms',
groupByPerQuery,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'unknown' });
const controller = makeController();
const hook = createSyncDisplayHook(
SYNC_KEY,
{
yAxisUnit: 'ms',
groupByPerQuery,
filterMode: SyncTooltipFilterMode.All,
},
controller,
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50 });
hook(u);
expect(controller.syncedSeriesIndexes).toBeNull();
});
});
// ── caching ───────────────────────────────────────────────────────────
describe('caching optimizations', () => {
it('reuses the crosshair element across multiple invocations', () => {
mockRegistry.getMetadata.mockReturnValue({ yAxisUnit: 'ms' });
mockRegistry.getActiveSeriesMetric.mockReturnValue(null);
const hook = createSyncDisplayHook(
SYNC_KEY,
{ yAxisUnit: 'ms' },
makeController(),
);
const u = makeFakeUPlot({ cursorEvent: null });
const spy = jest.spyOn(u.root, 'querySelector');
hook(u);
hook(u);
hook(u);
// querySelector should only be called once regardless of invocation count.
expect(spy).toHaveBeenCalledTimes(1);
});
it('recomputes common keys when source groupByPerQuery reference changes', () => {
const hostGroupBy = makeGroupByPerQuery('host');
const serviceGroupBy = makeGroupByPerQuery('service');
const series: FakeSeries[] = [
{},
{ metric: { host: 'server1', service: 'api' } },
{ metric: { host: 'server2', service: 'frontend' } },
];
const hook = createSyncDisplayHook(
SYNC_KEY,
{ groupByPerQuery: makeGroupByPerQuery('host', 'service') },
makeController(),
);
const u = makeFakeUPlot({ cursorEvent: null, cursorLeft: 50, series });
// First call: source groups by host → matches series 1.
mockRegistry.getMetadata.mockReturnValue({
groupByPerQuery: hostGroupBy,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({ host: 'server1' });
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(1, { focus: true });
jest.clearAllMocks();
// Second call: source now groups by service → matches series 2.
mockRegistry.getMetadata.mockReturnValue({
groupByPerQuery: serviceGroupBy,
});
mockRegistry.getActiveSeriesMetric.mockReturnValue({
service: 'frontend',
});
hook(u);
expect(mockSetSeries(u)).toHaveBeenCalledWith(2, { focus: true });
});
});
});
});

View File

@@ -47,3 +47,7 @@ export const opsGeniePriorityDefaultValue =
export const pagerDutySeverityTextDefaultValue =
'{{ (index .Alerts 0).Labels.severity }}';
export const googleChatTitleDefaultValue = `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`;
export const googleChatTextDefaultValue = `{{ range .Alerts -}} *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} *Severity:* {{ .Labels.severity }}{{ end }}{{ if .Annotations.summary }} *Summary:* {{ .Annotations.summary }}{{ end }}{{ if .Annotations.description }} *Description:* {{ .Annotations.description }}{{ end }}{{ if .Annotations.related_logs }} *Related Logs:* {{ .Annotations.related_logs }}{{ end }}{{ if .Annotations.related_traces }} *Related Traces:* {{ .Annotations.related_traces }}{{ end }} *Labels:* {{ range .Labels.SortedPairs -}} • \`{{ .Name }}\`: {{ .Value }} {{ end }} {{ end }}`;

View File

@@ -7,6 +7,7 @@ import get from 'api/channels/get';
import Spinner from 'components/Spinner';
import {
ChannelType,
GoogleChatChannel,
MsTeamsChannel,
PagerChannel,
SlackChannel,
@@ -56,9 +57,17 @@ function ChannelsEdit(): JSX.Element {
const prepChannelConfig = (): {
type: string;
channel: SlackChannel & WebhookChannel & PagerChannel & MsTeamsChannel;
channel: SlackChannel &
WebhookChannel &
PagerChannel &
MsTeamsChannel &
GoogleChatChannel;
} => {
let channel: SlackChannel & WebhookChannel & PagerChannel & MsTeamsChannel = {
let channel: SlackChannel &
WebhookChannel &
PagerChannel &
MsTeamsChannel &
GoogleChatChannel = {
name: '',
};
if (value && 'slack_configs' in value) {
@@ -78,6 +87,15 @@ function ChannelsEdit(): JSX.Element {
channel,
};
}
if (value && 'googlechat_configs' in value) {
const googleChatConfig = value.googlechat_configs[0];
channel = googleChatConfig;
return {
type: ChannelType.GoogleChat,
channel,
};
}
if (value && 'pagerduty_configs' in value) {
const pagerConfig = value.pagerduty_configs[0];
channel = pagerConfig;

View File

@@ -1,77 +0,0 @@
import { ReactNode } from 'react';
import { Dock, PanelBottom, PanelRight } from '@signozhq/icons';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import {
TooltipContent,
TooltipProvider,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { SpanDetailVariant } from './constants';
interface DockOption {
value: SpanDetailVariant;
icon: ReactNode;
tooltip: string;
}
const DOCK_OPTIONS: DockOption[] = [
{
value: SpanDetailVariant.DIALOG,
icon: <Dock size={14} />,
tooltip: 'Open as floating panel',
},
{
value: SpanDetailVariant.DOCKED,
icon: <PanelBottom size={14} />,
tooltip: 'Dock at the bottom',
},
{
value: SpanDetailVariant.DOCKED_RIGHT,
icon: <PanelRight size={14} />,
tooltip: 'Dock on the right',
},
];
interface DockModeSwitcherProps {
value: SpanDetailVariant;
onChange: (value: SpanDetailVariant) => void;
tooltipClassName?: string;
}
function DockModeSwitcher({
value,
onChange,
tooltipClassName,
}: DockModeSwitcherProps): JSX.Element {
return (
<TooltipProvider>
<ToggleGroup
type="single"
value={value}
onChange={(v): void => {
if (v) {
onChange(v as SpanDetailVariant);
}
}}
size="sm"
>
{DOCK_OPTIONS.map((option) => (
<TooltipRoot key={option.value}>
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value={option.value}>{option.icon}</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent className={tooltipClassName}>
{option.tooltip}
</TooltipContent>
</TooltipRoot>
))}
</ToggleGroup>
</TooltipProvider>
);
}
export default DockModeSwitcher;

View File

@@ -3,10 +3,6 @@
display: flex;
flex-direction: column;
overflow: hidden;
:global(.details-header) {
height: 39px;
}
}
.body {

View File

@@ -1,16 +1,25 @@
import { useCallback, useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import {
TabsContent,
TabsList,
TabsRoot,
TabsTrigger,
} from '@signozhq/ui/tabs';
import {
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import {
Bookmark,
CalendarClock,
ChartColumnBig,
Dock,
Link2,
List,
PanelBottom,
ScrollText,
Timer,
} from '@signozhq/icons';
@@ -52,7 +61,6 @@ import {
SpanDetailVariant,
VISIBLE_ACTIONS,
} from './constants';
import DockModeSwitcher from './DockModeSwitcher';
import { useSpanAttributeActions } from './hooks/useSpanAttributeActions';
import { useTracePinnedFields } from './hooks/useTracePinnedFields';
import {
@@ -484,14 +492,31 @@ function SpanDetailsPanel({
];
if (onVariantChange) {
const isDocked = variant === SpanDetailVariant.DOCKED;
actions.push({
key: 'dock-mode',
key: 'dock-toggle',
component: (
<DockModeSwitcher
value={variant}
onChange={onVariantChange}
tooltipClassName={styles.dockToggleTooltip}
/>
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
color="secondary"
onClick={(): void =>
onVariantChange(
isDocked ? SpanDetailVariant.DIALOG : SpanDetailVariant.DOCKED,
)
}
>
{isDocked ? <Dock size={14} /> : <PanelBottom size={14} />}
</Button>
</TooltipTrigger>
<TooltipContent className={styles.dockToggleTooltip}>
{isDocked ? 'Open as floating panel' : 'Dock at the bottom'}
</TooltipContent>
</TooltipRoot>
</TooltipProvider>
),
});
}
@@ -528,10 +553,7 @@ function SpanDetailsPanel({
</>
);
if (
variant === SpanDetailVariant.DOCKED ||
variant === SpanDetailVariant.DOCKED_RIGHT
) {
if (variant === SpanDetailVariant.DOCKED) {
return <div className={styles.root}>{content}</div>;
}

View File

@@ -22,7 +22,6 @@ export enum SpanDetailVariant {
DRAWER = 'drawer',
DIALOG = 'dialog',
DOCKED = 'docked',
DOCKED_RIGHT = 'right',
}
export const KEY_ATTRIBUTE_KEYS: Record<string, string[]> = {

View File

@@ -4,28 +4,13 @@
flex-direction: column;
}
.layoutRow {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
.content {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
overflow: hidden;
}
.rightDock {
display: flex;
flex-direction: column;
border-left: 1px solid var(--l2-border);
min-width: 0;
}
// Shared Ant Collapse chrome reset for both the flamegraph and waterfall
// collapse panels.
.flameCollapse,

View File

@@ -893,7 +893,7 @@ function Success(props: ISuccessProps): JSX.Element {
/>
{/* Left panel - table with horizontal scroll */}
<ResizableBox
handle="right"
direction="horizontal"
defaultWidth={DEFAULT_SIDEBAR_WIDTH}
minWidth={MIN_SIDEBAR_WIDTH}
maxWidth={MAX_SIDEBAR_WIDTH}

View File

@@ -242,13 +242,9 @@ function TraceDetailsV3(): JSX.Element {
() =>
(getLocalStorageKey(
LOCALSTORAGE.TRACE_DETAILS_SPAN_DETAILS_POSITION,
) as SpanDetailVariant) || SpanDetailVariant.DOCKED_RIGHT,
) as SpanDetailVariant) || SpanDetailVariant.DIALOG,
);
const RIGHT_DOCK_MIN = 480;
const RIGHT_DOCK_MAX = 720;
const [rightDockWidth, setRightDockWidth] = useState(RIGHT_DOCK_MIN);
const handleVariantChange = useCallback(
(newVariant: SpanDetailVariant): void => {
setLocalStorageKey(
@@ -295,9 +291,7 @@ function TraceDetailsV3(): JSX.Element {
(!!errorFetchingTraceData || !traceData?.payload?.spans?.length);
const isDocked = spanDetailVariant === SpanDetailVariant.DOCKED;
const isRightDocked = spanDetailVariant === SpanDetailVariant.DOCKED_RIGHT;
const isWaterfallDocked = panelState.isOpen && isDocked;
const showRightDock = panelState.isOpen && isRightDocked;
const waterfallChildren = (
<ResizableBox
@@ -338,118 +332,94 @@ function TraceDetailsV3(): JSX.Element {
<NoData />
) : (
<>
<div className={styles.layoutRow}>
<div className={styles.content}>
<Collapse
// @ts-expect-error motion is passed through to rc-collapse to disable animation
motion={false}
activeKey={activeKeys.filter((k) => k === 'flame')}
onChange={(): void => handleCollapseChange('flame')}
size="small"
className={styles.flameCollapse}
items={[
{
key: 'flame',
label: (
<div className={styles.collapseLabel}>
<span className={styles.collapseTitle}>
Flame Graph
{traceData?.payload?.totalSpansCount &&
traceData.payload.totalSpansCount > FLAMEGRAPH_SPAN_LIMIT && (
<WarningPopover
message="The total span count exceeds the visualization limit. Displaying a sampled subset of spans in flamegraph."
placement="bottomLeft"
/>
)}
</span>
{traceData?.payload?.totalSpansCount ? (
<span className={styles.collapseCount}>
<span className={styles.collapseCountItem}>
<ChartNoAxesGantt size={13} />
Spans: {traceData.payload.totalSpansCount}
</span>
<span
className={cx(styles.collapseCountItem, {
[styles.hasErrors]: traceData.payload.totalErrorSpansCount > 0,
})}
>
<TriangleAlert size={13} />
Errors: {traceData.payload.totalErrorSpansCount ?? 0}
</span>
<div className={styles.content}>
<Collapse
// @ts-expect-error motion is passed through to rc-collapse to disable animation
motion={false}
activeKey={activeKeys.filter((k) => k === 'flame')}
onChange={(): void => handleCollapseChange('flame')}
size="small"
className={styles.flameCollapse}
items={[
{
key: 'flame',
label: (
<div className={styles.collapseLabel}>
<span className={styles.collapseTitle}>
Flame Graph
{traceData?.payload?.totalSpansCount &&
traceData.payload.totalSpansCount > FLAMEGRAPH_SPAN_LIMIT && (
<WarningPopover
message="The total span count exceeds the visualization limit. Displaying a sampled subset of spans in flamegraph."
placement="bottomLeft"
/>
)}
</span>
{traceData?.payload?.totalSpansCount ? (
<span className={styles.collapseCount}>
<span className={styles.collapseCountItem}>
<ChartNoAxesGantt size={13} />
Spans: {traceData.payload.totalSpansCount}
</span>
) : null}
</div>
),
children: (
<ResizableBox defaultHeight={300} minHeight={100} maxHeight={400}>
<TraceFlamegraph
filteredSpanIds={filteredSpanIds}
isFilterActive={isFilterActive}
selectedSpan={selectedSpan}
totalSpansCount={totalSpansCount}
/>
</ResizableBox>
),
},
]}
/>
<span
className={cx(styles.collapseCountItem, {
[styles.hasErrors]: traceData.payload.totalErrorSpansCount > 0,
})}
>
<TriangleAlert size={13} />
Errors: {traceData.payload.totalErrorSpansCount ?? 0}
</span>
</span>
) : null}
</div>
),
children: (
<ResizableBox defaultHeight={300} minHeight={100} maxHeight={400}>
<TraceFlamegraph
filteredSpanIds={filteredSpanIds}
isFilterActive={isFilterActive}
selectedSpan={selectedSpan}
totalSpansCount={totalSpansCount}
/>
</ResizableBox>
),
},
]}
/>
<Collapse
// @ts-expect-error motion is passed through to rc-collapse to disable animation
motion={false}
activeKey={activeKeys.filter((k) => k === 'waterfall')}
onChange={(): void => handleCollapseChange('waterfall')}
size="small"
className={cx(styles.waterfallCollapse, {
[styles.isDocked]: isWaterfallDocked,
})}
items={[
{
key: 'waterfall',
label: 'Waterfall',
children: activeKeys.includes('waterfall')
? waterfallChildren
: null,
},
]}
/>
<Collapse
// @ts-expect-error motion is passed through to rc-collapse to disable animation
motion={false}
activeKey={activeKeys.filter((k) => k === 'waterfall')}
onChange={(): void => handleCollapseChange('waterfall')}
size="small"
className={cx(styles.waterfallCollapse, {
[styles.isDocked]: isWaterfallDocked,
})}
items={[
{
key: 'waterfall',
label: 'Waterfall',
children: activeKeys.includes('waterfall') ? waterfallChildren : null,
},
]}
/>
{panelState.isOpen && isDocked && (
<div className={styles.dockedSpanDetails}>
<SpanDetailsPanel
panelState={panelState}
selectedSpan={selectedSpan}
variant={SpanDetailVariant.DOCKED}
onVariantChange={handleVariantChange}
traceStartTime={traceData?.payload?.startTimestampMillis}
traceEndTime={traceData?.payload?.endTimestampMillis}
/>
</div>
)}
</div>
{showRightDock && (
<ResizableBox
handle="left"
defaultWidth={rightDockWidth}
minWidth={RIGHT_DOCK_MIN}
maxWidth={RIGHT_DOCK_MAX}
onResize={setRightDockWidth}
className={styles.rightDock}
>
{panelState.isOpen && isDocked && (
<div className={styles.dockedSpanDetails}>
<SpanDetailsPanel
panelState={panelState}
selectedSpan={selectedSpan}
variant={SpanDetailVariant.DOCKED_RIGHT}
variant={SpanDetailVariant.DOCKED}
onVariantChange={handleVariantChange}
traceStartTime={traceData?.payload?.startTimestampMillis}
traceEndTime={traceData?.payload?.endTimestampMillis}
/>
</ResizableBox>
</div>
)}
</div>
{panelState.isOpen && spanDetailVariant === SpanDetailVariant.DIALOG && (
{panelState.isOpen && !isDocked && (
<SpanDetailsPanel
panelState={panelState}
selectedSpan={selectedSpan}

View File

@@ -23,36 +23,20 @@
background: var(--primary);
}
&--top,
&--bottom {
&--vertical {
bottom: 0;
left: 0;
right: 0;
height: 1px;
cursor: row-resize;
}
&--left,
&--right {
&--horizontal {
right: 0;
top: 0;
bottom: 0;
width: 1px;
cursor: col-resize;
}
&--top {
top: 0;
}
&--bottom {
bottom: 0;
}
&--left {
left: 0;
}
&--right {
right: 0;
}
}
}

View File

@@ -2,15 +2,9 @@ import { useCallback, useRef, useState } from 'react';
import './ResizableBox.styles.scss';
export type ResizableBoxHandle = 'top' | 'right' | 'bottom' | 'left';
export interface ResizableBoxProps {
children: React.ReactNode;
// Which edge the resize handle sits on. The edge determines the axis:
// 'top'/'bottom' → vertical resize (height), 'left'/'right' → horizontal
// resize (width). Dragging the handle away from the content grows the box;
// dragging it toward the content shrinks it.
handle?: ResizableBoxHandle;
direction?: 'vertical' | 'horizontal';
defaultHeight?: number;
minHeight?: number;
maxHeight?: number;
@@ -24,7 +18,7 @@ export interface ResizableBoxProps {
function ResizableBox({
children,
handle = 'bottom',
direction = 'vertical',
defaultHeight = 200,
minHeight = 50,
maxHeight = Infinity,
@@ -35,8 +29,7 @@ function ResizableBox({
disabled = false,
className,
}: ResizableBoxProps): JSX.Element {
const isHorizontal = handle === 'left' || handle === 'right';
const isStartHandle = handle === 'top' || handle === 'left';
const isHorizontal = direction === 'horizontal';
const [size, setSize] = useState(isHorizontal ? defaultWidth : defaultHeight);
const containerRef = useRef<HTMLDivElement>(null);
@@ -47,13 +40,10 @@ function ResizableBox({
const startSize = size;
const min = isHorizontal ? minWidth : minHeight;
const max = isHorizontal ? maxWidth : maxHeight;
// Start-edge handle: pointer moving away from content (negative delta)
// grows the box, so invert the sign.
const deltaSign = isStartHandle ? -1 : 1;
const onMouseMove = (moveEvent: MouseEvent): void => {
const currentPos = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
const delta = (currentPos - startPos) * deltaSign;
const delta = currentPos - startPos;
const newSize = Math.min(max, Math.max(min, startSize + delta));
setSize(newSize);
onResize?.(newSize);
@@ -71,16 +61,7 @@ function ResizableBox({
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
},
[
size,
isHorizontal,
isStartHandle,
minWidth,
maxWidth,
minHeight,
maxHeight,
onResize,
],
[size, isHorizontal, minWidth, maxWidth, minHeight, maxHeight, onResize],
);
const containerStyle = disabled
@@ -88,7 +69,7 @@ function ResizableBox({
: isHorizontal
? { width: size }
: { height: size };
const handleClass = `resizable-box__handle resizable-box__handle--${handle}`;
const handleClass = `resizable-box__handle resizable-box__handle--${direction}`;
return (
<div

View File

@@ -0,0 +1,8 @@
import { GoogleChatChannel } from 'container/CreateAlertChannels/config';
export type Props = GoogleChatChannel;
export interface PayloadProps {
data: string;
status: string;
}

View File

@@ -0,0 +1,10 @@
import { GoogleChatChannel } from 'container/CreateAlertChannels/config';
export interface Props extends GoogleChatChannel {
id: string;
}
export interface PayloadProps {
data: string;
status: string;
}

View File

@@ -25,7 +25,7 @@ type Alertmanager interface {
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
// TestReceiver sends a test alert to a receiver.
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
TestReceiver(context.Context, string, *alertmanagertypes.Receiver) error
// TestAlert sends an alert to a list of receivers.
TestAlert(ctx context.Context, orgID string, ruleID string, receiversMap map[*alertmanagertypes.PostableAlert][]string) error
@@ -40,10 +40,10 @@ type Alertmanager interface {
GetChannelByID(context.Context, string, valuer.UUID) (*alertmanagertypes.Channel, error)
// UpdateChannel updates a channel for the organization.
UpdateChannelByReceiverAndID(context.Context, string, alertmanagertypes.Receiver, valuer.UUID) error
UpdateChannelByReceiverAndID(context.Context, string, *alertmanagertypes.Receiver, valuer.UUID) error
// CreateChannel creates a channel for the organization.
CreateChannel(context.Context, string, alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)
CreateChannel(context.Context, string, *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)
// DeleteChannelByID deletes a channel for the organization.
DeleteChannelByID(context.Context, string, valuer.UUID) error

View File

@@ -0,0 +1,161 @@
// Copyright (c) 2026 SigNoz, Inc.
// SPDX-License-Identifier: Apache-2.0
package googlechat
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"net/http"
"net/url"
"strings"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
const (
Integration = "googlechat"
)
// Notifier implements a Notifier for Google Chat notifications.
type Notifier struct {
config *alertmanagertypes.GoogleChatReceiverConfig
logger *slog.Logger
client *http.Client
retrier *notify.Retrier
templater alertmanagertypes.Templater
}
// New returns a new Google Chat notifier. The template is consumed via the
// templater, so the *template.Template argument is accepted only for signature
// parity with the other notifiers.
func New(cfg *alertmanagertypes.GoogleChatReceiverConfig, _ *template.Template, l *slog.Logger, templater alertmanagertypes.Templater, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
if cfg == nil {
return nil, errors.NewInternalf(errors.CodeInternal, "google chat config is required")
}
if cfg.WebhookURL == nil {
return nil, errors.NewInternalf(errors.CodeInternal, "google chat webhook_url is required")
}
if err := validateWebhookURL(cfg.WebhookURL.String()); err != nil {
return nil, err
}
client, err := notify.NewClientWithTracing(commoncfg.DefaultHTTPClientConfig, Integration, httpOpts...)
if err != nil {
return nil, err
}
return &Notifier{
config: cfg,
logger: l,
client: client,
retrier: &notify.Retrier{},
templater: templater,
}, nil
}
func validateWebhookURL(rawURL string) error {
u, err := url.Parse(rawURL)
if err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid google chat webhook_url: %v", err)
}
if u.Scheme != "https" {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "google chat webhook_url must use https")
}
host := strings.ToLower(u.Hostname())
if host != "chat.googleapis.com" {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "google chat webhook_url must use chat.googleapis.com")
}
return nil
}
// Message represents the payload sent to Google Chat webhook.
type Message struct {
Text string `json:"text"`
}
// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, alerts ...*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")
cfg := n.config
// Expand the title/body templates via the shared templater so that
// per-alert custom templates supplied through annotations override the
// receiver defaults (consistent with the other SigNoz notifiers).
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(alerts)
result, err := n.templater.Expand(ctx, alertmanagertypes.ExpandRequest{
TitleTemplate: customTitle,
BodyTemplate: customBody,
DefaultTitleTemplate: cfg.Title,
DefaultBodyTemplate: cfg.Text,
}, alerts)
if err != nil {
return false, errors.NewInternalf(errors.CodeInternal, "failed to render templates: %v", err)
}
// Build the final message: the title followed by each alert's (non-empty)
// rendered body. Google Chat receives a single plain-text message.
finalText := result.Title
bodyParts := make([]string, 0, len(result.Body))
for _, body := range result.Body {
if body != "" {
bodyParts = append(bodyParts, body)
}
}
if len(bodyParts) > 0 {
finalText = result.Title + "\n" + strings.Join(bodyParts, "\n")
}
msg := &Message{
Text: finalText,
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return false, err
}
// Add threading support
// https://developers.google.com/chat/how-tos/webhooks#start_a_message_thread
webhookURL := cfg.WebhookURL.String()
u, err := url.Parse(webhookURL)
if err != nil {
return false, errors.NewInternalf(errors.CodeInternal, "unable to parse googlechat url: %v", err)
}
q := u.Query()
q.Set("threadKey", key.Hash())
q.Set("messageReplyOption", "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD")
u.RawQuery = q.Encode()
webhookURL = u.String()
resp, err := notify.PostJSON(ctx, n.client, webhookURL, &buf) //nolint:bodyclose
if err != nil {
if ctx.Err() != nil {
err = errors.NewInternalf(errors.CodeInternal, "failed to post JSON to google chat: %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

@@ -0,0 +1,196 @@
// Copyright (c) 2026 SigNoz, Inc.
// SPDX-License-Identifier: Apache-2.0
package googlechat
import (
"bytes"
"context"
"crypto/tls"
"io"
"log/slog"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
notifytest "github.com/prometheus/alertmanager/notify/test"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
)
func newTestTemplater(t *testing.T) alertmanagertypes.Templater {
t.Helper()
return alertmanagertemplate.New(test.CreateTmpl(t), slog.New(slog.DiscardHandler))
}
func TestGoogleChatWebhook(t *testing.T) {
var payload bytes.Buffer
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
_, _ = io.Copy(&payload, r.Body)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
}))
defer server.Close()
notifier := newTestNotifier(t, server, "Alert: {{ .GroupLabels.alertname }}", "{{ range .Alerts -}} Alert: {{ .Labels.alertname }} {{ end }}")
retry, err := notifier.Notify(newTestContext(), newTestAlerts("TestAlert")...)
require.NoError(t, err)
require.False(t, retry)
require.Contains(t, payload.String(), `"text"`)
require.Contains(t, payload.String(), "Alert: TestAlert")
}
func TestGoogleChatTemplating(t *testing.T) {
var payload bytes.Buffer
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
_, _ = io.Copy(&payload, r.Body)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
}))
defer server.Close()
notifier := newTestNotifier(t, server, "Alert: {{ .GroupLabels.alertname }}", "Summary: {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}")
retry, err := notifier.Notify(newTestContext(), newTestAlerts("CPU High")...)
require.NoError(t, err)
require.False(t, retry)
require.Contains(t, payload.String(), "CPU High")
require.Contains(t, payload.String(), "Summary:")
}
func TestGoogleChatRetry(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
notifier := newTestNotifier(t, server, "Alert", "Test message")
retry, err := notifier.Notify(newTestContext(), newTestAlerts("TestAlert")...)
require.Error(t, err)
require.True(t, retry)
}
func TestGoogleChatRetryCodes(t *testing.T) {
notifier, err := New(&alertmanagertypes.GoogleChatReceiverConfig{
WebhookURL: secretURLFromString(t, "https://chat.googleapis.com/v1/spaces/test/messages"),
Title: "Alert",
Text: "Test message",
}, test.CreateTmpl(t), slog.New(slog.DiscardHandler), newTestTemplater(t))
require.NoError(t, err)
for statusCode, expected := range notifytest.RetryTests(notifytest.DefaultRetryCodes()) {
actual, _ := notifier.retrier.Check(statusCode, nil)
require.Equal(t, expected, actual, "retry - error on status %d", statusCode)
}
}
func TestGoogleChatWebhookValidation(t *testing.T) {
_, err := New(&alertmanagertypes.GoogleChatReceiverConfig{
WebhookURL: secretURLFromString(t, "http://chat.googleapis.com/v1/spaces/test/messages"),
Title: "Alert",
Text: "Test message",
}, test.CreateTmpl(t), slog.New(slog.DiscardHandler), newTestTemplater(t))
require.Error(t, err)
require.Contains(t, err.Error(), "webhook_url must use https")
_, err = New(&alertmanagertypes.GoogleChatReceiverConfig{
WebhookURL: secretURLFromString(t, "https://example.com/v1/spaces/test/messages"),
Title: "Alert",
Text: "Test message",
}, test.CreateTmpl(t), slog.New(slog.DiscardHandler), newTestTemplater(t))
require.Error(t, err)
require.Contains(t, err.Error(), "webhook_url must use chat.googleapis.com")
}
func TestGoogleChatRedactedURL(t *testing.T) {
secret := "secret-token"
urlStr := "https://chat.googleapis.com/v1/spaces/test/messages?token=" + secret
notifier, err := New(&alertmanagertypes.GoogleChatReceiverConfig{
WebhookURL: secretURLFromString(t, urlStr),
Title: "Alert",
Text: "Test message",
}, test.CreateTmpl(t), slog.New(slog.DiscardHandler), newTestTemplater(t))
require.NoError(t, err)
notifier.client = &http.Client{Transport: roundTripperFunc(func(*http.Request) (*http.Response, error) {
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "request failed")
})}
_, err = notifier.Notify(newTestContext(), newTestAlerts("TestAlert")...)
require.Error(t, err)
require.NotContains(t, err.Error(), secret)
}
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req)
}
func newTestNotifier(t *testing.T, server *httptest.Server, title, text string) *Notifier {
t.Helper()
webhookURL := "https://chat.googleapis.com/v1/spaces/test/messages"
notifier, err := New(&alertmanagertypes.GoogleChatReceiverConfig{
WebhookURL: secretURLFromString(t, webhookURL),
Title: title,
Text: text,
}, test.CreateTmpl(t), slog.New(slog.DiscardHandler), newTestTemplater(t))
require.NoError(t, err)
notifier.client = newTestHTTPClient(server)
return notifier
}
func newTestHTTPClient(server *httptest.Server) *http.Client {
dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, server.Listener.Addr().String())
},
}
return &http.Client{Transport: transport}
}
func secretURLFromString(t *testing.T, rawURL string) *config.SecretURL {
t.Helper()
parsed, err := url.Parse(rawURL)
require.NoError(t, err)
return &config.SecretURL{URL: parsed}
}
func newTestAlerts(alertname string) []*types.Alert {
return []*types.Alert{{
Alert: model.Alert{
Labels: model.LabelSet{
"alertname": model.LabelValue(alertname),
},
Annotations: model.LabelSet{
"summary": model.LabelValue("summary for " + alertname),
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Minute),
},
}}
}
func newTestContext() context.Context {
return notify.WithGroupKey(context.Background(), "test-receiver")
}

View File

@@ -5,6 +5,7 @@ import (
"slices"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/email"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/googlechat"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/msteamsv2"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/opsgenie"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/pagerduty"
@@ -24,10 +25,11 @@ var customNotifierIntegrations = []string{
opsgenie.Integration,
slack.Integration,
msteamsv2.Integration,
googlechat.Integration,
}
func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
upstreamIntegrations, err := receiver.BuildReceiverIntegrations(nc, tmpl, logger)
func NewReceiverIntegrations(nc *alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
upstreamIntegrations, err := receiver.BuildReceiverIntegrations(*nc.Receiver, tmpl, logger)
if err != nil {
return nil, err
}
@@ -74,6 +76,11 @@ func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Templ
return msteamsv2.New(c, tmpl, `{{ template "msteamsv2.default.titleLink" . }}`, l, templater)
})
}
for i, c := range nc.GoogleChatConfigs {
add(googlechat.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) {
return googlechat.New(c, tmpl, l, templater)
})
}
if errs.Len() > 0 {
return nil, &errs

View File

@@ -2,6 +2,8 @@ package alertmanagerserver
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"sync"
@@ -242,6 +244,8 @@ func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanager
func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertmanagertypes.Config) error {
config := alertmanagerConfig.AlertmanagerConfig()
jsun, kerr := json.Marshal(config)
fmt.Println("yo config here", string(jsun), kerr)
var err error
// Load SigNoz's alertmanager notification templates from the configured
@@ -275,7 +279,11 @@ 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.templater)
extendedRcv, err := alertmanagerConfig.GetReceiver(rcv.Name)
if err != nil {
return err
}
integrations, err := alertmanagernotify.NewReceiverIntegrations(extendedRcv, server.tmpl, server.logger, server.templater)
if err != nil {
return err
}
@@ -350,8 +358,8 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
return nil
}
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
testAlert := alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now())
func (server *Server) TestReceiver(ctx context.Context, receiver *alertmanagertypes.Receiver) error {
testAlert := alertmanagertypes.NewTestAlert(*receiver.Receiver, time.Now(), time.Now())
return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, server.templater, testAlert.Labels, testAlert)
}
@@ -420,7 +428,7 @@ func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmana
receiverName := receiverName
g.Go(func() error {
receiver, err := server.alertmanagerConfig.GetReceiver(receiverName)
baseReceiver, err := server.alertmanagerConfig.GetReceiver(receiverName)
if err != nil {
mu.Lock()
errs = append(errs, errors.WrapInternalf(err, errors.CodeInternal, "failed to get receiver %q", receiverName))
@@ -430,7 +438,7 @@ func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmana
err = alertmanagertypes.TestReceiver(
gCtx,
receiver,
baseReceiver,
alertmanagernotify.NewReceiverIntegrations,
server.alertmanagerConfig,
server.tmpl,

View File

@@ -75,12 +75,14 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
require.NoError(t, err)
err = server.TestReceiver(context.Background(), alertmanagertypes.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
err = server.TestReceiver(context.Background(), &alertmanagertypes.Receiver{
Receiver: &config.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
},
},
},
})
@@ -101,12 +103,14 @@ func TestServerPutAlerts(t *testing.T) {
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost/test-receiver"),
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{
Receiver: &config.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost/test-receiver"),
},
},
},
}))
@@ -181,22 +185,26 @@ func TestServerTestAlert(t *testing.T) {
webhook2URL, err := url.Parse("http://" + webhook2Listener.Addr().String() + "/webhook")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "receiver-1",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook1URL.String()),
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{
Receiver: &config.Receiver{
Name: "receiver-1",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook1URL.String()),
},
},
},
}))
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "receiver-2",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook2URL.String()),
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{
Receiver: &config.Receiver{
Name: "receiver-2",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook2URL.String()),
},
},
},
}))
@@ -273,22 +281,26 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "working-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{
Receiver: &config.Receiver{
Name: "working-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
},
},
},
}))
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "failing-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost:1/webhook"),
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{
Receiver: &config.Receiver{
Name: "failing-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost:1/webhook"),
},
},
},
}))

View File

@@ -110,7 +110,7 @@ func (_c *MockAlertmanager_Collect_Call) RunAndReturn(run func(context1 context.
}
// CreateChannel provides a mock function for the type MockAlertmanager
func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string, v alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string, v *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
ret := _mock.Called(context1, s, v)
if len(ret) == 0 {
@@ -119,17 +119,17 @@ func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string,
var r0 *alertmanagertypes.Channel
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)); ok {
return returnFunc(context1, s, v)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) *alertmanagertypes.Channel); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) *alertmanagertypes.Channel); ok {
r0 = returnFunc(context1, s, v)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*alertmanagertypes.Channel)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, alertmanagertypes.Receiver) error); ok {
if returnFunc, ok := ret.Get(1).(func(context.Context, string, *alertmanagertypes.Receiver) error); ok {
r1 = returnFunc(context1, s, v)
} else {
r1 = ret.Error(1)
@@ -145,12 +145,12 @@ type MockAlertmanager_CreateChannel_Call struct {
// CreateChannel is a helper method to define mock.On call
// - context1 context.Context
// - s string
// - v alertmanagertypes.Receiver
// - v *alertmanagertypes.Receiver
func (_e *MockAlertmanager_Expecter) CreateChannel(context1 interface{}, s interface{}, v interface{}) *MockAlertmanager_CreateChannel_Call {
return &MockAlertmanager_CreateChannel_Call{Call: _e.mock.On("CreateChannel", context1, s, v)}
}
func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver)) *MockAlertmanager_CreateChannel_Call {
func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver)) *MockAlertmanager_CreateChannel_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -160,9 +160,9 @@ func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Con
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 alertmanagertypes.Receiver
var arg2 *alertmanagertypes.Receiver
if args[2] != nil {
arg2 = args[2].(alertmanagertypes.Receiver)
arg2 = args[2].(*alertmanagertypes.Receiver)
}
run(
arg0,
@@ -178,7 +178,7 @@ func (_c *MockAlertmanager_CreateChannel_Call) Return(channel *alertmanagertypes
return _c
}
func (_c *MockAlertmanager_CreateChannel_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)) *MockAlertmanager_CreateChannel_Call {
func (_c *MockAlertmanager_CreateChannel_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)) *MockAlertmanager_CreateChannel_Call {
_c.Call.Return(run)
return _c
}
@@ -1579,7 +1579,7 @@ func (_c *MockAlertmanager_TestAlert_Call) RunAndReturn(run func(ctx context.Con
}
// TestReceiver provides a mock function for the type MockAlertmanager
func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string, v alertmanagertypes.Receiver) error {
func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string, v *alertmanagertypes.Receiver) error {
ret := _mock.Called(context1, s, v)
if len(ret) == 0 {
@@ -1587,7 +1587,7 @@ func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string,
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) error); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) error); ok {
r0 = returnFunc(context1, s, v)
} else {
r0 = ret.Error(0)
@@ -1603,12 +1603,12 @@ type MockAlertmanager_TestReceiver_Call struct {
// TestReceiver is a helper method to define mock.On call
// - context1 context.Context
// - s string
// - v alertmanagertypes.Receiver
// - v *alertmanagertypes.Receiver
func (_e *MockAlertmanager_Expecter) TestReceiver(context1 interface{}, s interface{}, v interface{}) *MockAlertmanager_TestReceiver_Call {
return &MockAlertmanager_TestReceiver_Call{Call: _e.mock.On("TestReceiver", context1, s, v)}
}
func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver)) *MockAlertmanager_TestReceiver_Call {
func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver)) *MockAlertmanager_TestReceiver_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -1618,9 +1618,9 @@ func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Cont
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 alertmanagertypes.Receiver
var arg2 *alertmanagertypes.Receiver
if args[2] != nil {
arg2 = args[2].(alertmanagertypes.Receiver)
arg2 = args[2].(*alertmanagertypes.Receiver)
}
run(
arg0,
@@ -1636,7 +1636,7 @@ func (_c *MockAlertmanager_TestReceiver_Call) Return(err error) *MockAlertmanage
return _c
}
func (_c *MockAlertmanager_TestReceiver_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver) error) *MockAlertmanager_TestReceiver_Call {
func (_c *MockAlertmanager_TestReceiver_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver) error) *MockAlertmanager_TestReceiver_Call {
_c.Call.Return(run)
return _c
}
@@ -1705,7 +1705,7 @@ func (_c *MockAlertmanager_UpdateAllRoutePoliciesByRuleId_Call) RunAndReturn(run
}
// UpdateChannelByReceiverAndID provides a mock function for the type MockAlertmanager
func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID) error {
func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID) error {
ret := _mock.Called(context1, s, v, uUID)
if len(ret) == 0 {
@@ -1713,7 +1713,7 @@ func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Con
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver, valuer.UUID) error); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver, valuer.UUID) error); ok {
r0 = returnFunc(context1, s, v, uUID)
} else {
r0 = ret.Error(0)
@@ -1729,13 +1729,13 @@ type MockAlertmanager_UpdateChannelByReceiverAndID_Call struct {
// UpdateChannelByReceiverAndID is a helper method to define mock.On call
// - context1 context.Context
// - s string
// - v alertmanagertypes.Receiver
// - v *alertmanagertypes.Receiver
// - uUID valuer.UUID
func (_e *MockAlertmanager_Expecter) UpdateChannelByReceiverAndID(context1 interface{}, s interface{}, v interface{}, uUID interface{}) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
return &MockAlertmanager_UpdateChannelByReceiverAndID_Call{Call: _e.mock.On("UpdateChannelByReceiverAndID", context1, s, v, uUID)}
}
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID)) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID)) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -1745,9 +1745,9 @@ func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(conte
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 alertmanagertypes.Receiver
var arg2 *alertmanagertypes.Receiver
if args[2] != nil {
arg2 = args[2].(alertmanagertypes.Receiver)
arg2 = args[2].(*alertmanagertypes.Receiver)
}
var arg3 valuer.UUID
if args[3] != nil {
@@ -1768,7 +1768,7 @@ func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Return(err error)
return _c
}
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID) error) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID) error) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -138,7 +138,7 @@ func (service *Service) PutAlerts(ctx context.Context, orgID string, alerts aler
return server.PutAlerts(ctx, alerts)
}
func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) error {
service.serversMtx.RLock()
defer service.serversMtx.RUnlock()

View File

@@ -110,7 +110,7 @@ func (provider *provider) PutAlerts(ctx context.Context, orgID string, alerts al
return provider.service.PutAlerts(ctx, orgID, alerts)
}
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) error {
return provider.service.TestReceiver(ctx, orgID, receiver)
}
@@ -151,7 +151,7 @@ func (provider *provider) GetChannelByID(ctx context.Context, orgID string, chan
return provider.configStore.GetChannelByID(ctx, orgID, channelID)
}
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver, id valuer.UUID) error {
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver, id valuer.UUID) error {
channel, err := provider.configStore.GetChannelByID(ctx, orgID, id)
if err != nil {
return err
@@ -210,7 +210,7 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
}))
}
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
config, err := provider.configStore.Get(ctx, orgID)
if err != nil {
return nil, err

View File

@@ -29,24 +29,5 @@ func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v4/traces/{traceID}/waterfall", handler.New(
provider.authzMiddleware.ViewAccess(provider.traceDetailHandler.GetWaterfallV4),
handler.OpenAPIDef{
ID: "GetWaterfallV4",
Tags: []string{"tracedetail"},
Summary: "Get waterfall view for a trace",
Description: "Returns the waterfall view of spans including all spans if total spans are under a limit, a max count otherwise. Aggregations are dropped compared to v3",
Request: new(spantypes.PostableWaterfall),
RequestContentType: "application/json",
Response: new(spantypes.GettableWaterfallTrace),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -38,24 +38,3 @@ func (h *handler) GetWaterfall(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, result)
}
func (h *handler) GetWaterfallV4(rw http.ResponseWriter, r *http.Request) {
req := new(spantypes.PostableWaterfall)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
if err := req.Validate(); err != nil {
render.Error(rw, err)
return
}
result, err := h.module.GetWaterfallV4(r.Context(), mux.Vars(r)["traceID"], req.SelectedSpanID, req.UncollapsedSpans, req.Limit)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, result)
}

View File

@@ -2,7 +2,6 @@ package impltracedetail
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
@@ -46,7 +45,7 @@ func (m *module) GetWaterfall(ctx context.Context, traceID string, req *spantype
return spantypes.NewGettableWaterfallTrace(waterfallTrace, selectedSpans, uncollapsedSpans, selectedAllSpans, aggregationResults), nil
}
// getTraceData fetches all spans for a trace and builds the WaterfallTrace.
// getTraceData returns the waterfall cache for the given traceID with fallback on DB.
func (m *module) getTraceData(ctx context.Context, traceID string) (*spantypes.WaterfallTrace, error) {
summary, err := m.store.GetTraceSummary(ctx, traceID)
if err != nil {
@@ -62,86 +61,6 @@ func (m *module) getTraceData(ctx context.Context, traceID string) (*spantypes.W
return nil, spantypes.ErrTraceNotFound
}
nodes := make([]*spantypes.WaterfallSpan, len(spanItems))
for i := range spanItems {
nodes[i] = spanItems[i].ToWaterfallSpan(traceID)
}
return spantypes.NewWaterfallTraceFromSpans(nodes), nil
}
// GetWaterfallV4 is the OOM-safe V4 waterfall.
// For large traces (NumSpans > effectiveLimit) it uses a two-step fetch:
// minimal fields for all spans to build the tree, then full fields for the
// visible window only. Aggregations are not returned.
func (m *module) GetWaterfallV4(ctx context.Context, traceID string, selectedSpanID string, uncollapsedSpans []string, selectAllLimit uint) (*spantypes.GettableWaterfallTrace, error) {
summary, err := m.store.GetTraceSummary(ctx, traceID)
if err != nil {
return nil, err
}
effectiveLimit := min(selectAllLimit, m.config.Waterfall.MaxLimitToSelectAllSpans)
if summary.NumSpans > uint64(effectiveLimit) {
return m.getWindowedWaterfall(ctx, traceID, selectedSpanID, uncollapsedSpans, summary.Start, summary.End)
}
return m.getFullWaterfall(ctx, traceID, summary)
}
func (m *module) getFullWaterfall(ctx context.Context, traceID string, summary *spantypes.TraceSummary) (*spantypes.GettableWaterfallTrace, error) {
spanItems, err := m.store.GetTraceSpans(ctx, traceID, summary)
if err != nil {
return nil, err
}
if len(spanItems) == 0 {
return nil, spantypes.ErrTraceNotFound
}
nodes := make([]*spantypes.WaterfallSpan, len(spanItems))
for i := range spanItems {
nodes[i] = spanItems[i].ToWaterfallSpan(traceID)
}
waterfallTrace := spantypes.NewWaterfallTraceFromSpans(nodes)
selectedSpans := waterfallTrace.GetAllSpans()
return spantypes.NewGettableWaterfallTrace(waterfallTrace, selectedSpans, nil, true, nil), nil
}
// getWindowedWaterfall builds the waterfall tree with minimal data and then returns only a window of full spans.
func (m *module) getWindowedWaterfall(ctx context.Context, traceID, selectedSpanID string, uncollapsedSpans []string, start, end time.Time) (*spantypes.GettableWaterfallTrace, error) {
// Step 1: minimal fetch → build full tree → select visible window
minimalSpans, err := m.store.GetMinimalSpans(ctx, traceID, start, end)
if err != nil {
return nil, err
}
if len(minimalSpans) == 0 {
return nil, spantypes.ErrTraceNotFound
}
nodes := make([]*spantypes.WaterfallSpan, len(minimalSpans))
for i := range minimalSpans {
nodes[i] = minimalSpans[i].ToWaterfallSpan(traceID)
}
waterfallTrace := spantypes.NewWaterfallTraceFromSpans(nodes)
selectedSpans, uncollapsedSpans := waterfallTrace.GetSelectedSpans(
uncollapsedSpans,
selectedSpanID,
m.config.Waterfall.SpanPageSize,
m.config.Waterfall.MaxDepthToAutoExpand,
)
// Step 2: full fetch for the selected window only
spanIDs := make([]string, len(selectedSpans))
for i, s := range selectedSpans {
spanIDs[i] = s.SpanID
}
fullSpans, err := m.store.GetTraceSpansByIDs(ctx, traceID, start, end, spanIDs)
if err != nil {
return nil, err
}
spantypes.EnrichSelectedSpans(selectedSpans, fullSpans)
return spantypes.NewGettableWaterfallTrace(
waterfallTrace, selectedSpans, uncollapsedSpans, false, nil,
), nil
traceData := spantypes.NewWaterfallTraceFromSpans(spanItems)
return traceData, nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"fmt"
"time"
sqlbuilder "github.com/huandu/go-sqlbuilder"
@@ -13,8 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/spantypes"
)
const colServiceName = `resource_string_service$$$$name` // $ gets escaped so $$$$ converts to $$.
type traceStore struct {
telemetryStore telemetrystore.TelemetryStore
}
@@ -48,8 +45,8 @@ func (s *traceStore) GetTraceSpans(ctx context.Context, traceID string, summary
// DISTINCT ON (span_id) is ClickHouse-specific syntax not supported by sqlbuilder
query := fmt.Sprintf(`
SELECT DISTINCT ON (span_id)
timestamp, duration_nano, span_id, has_error, kind,
resource_string_service$$name, name,
timestamp, duration_nano, span_id, trace_id, has_error, kind,
resource_string_service$$name, name, links as references,
attributes_string, attributes_number, attributes_bool, resources_string,
events, status_message, status_code_string, kind_string, parent_span_id,
flags, is_remote, trace_state, status_code,
@@ -72,64 +69,3 @@ func (s *traceStore) GetTraceSpans(ctx context.Context, traceID string, summary
}
return spanItems, nil
}
func (s *traceStore) GetMinimalSpans(ctx context.Context, traceID string, start, end time.Time) ([]spantypes.MinimalSpan, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"DISTINCT ON (span_id) span_id",
"parent_span_id", "timestamp", "duration_nano", "has_error",
colServiceName,
)
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
sb.Where(
sb.E("trace_id", traceID),
sb.GE("ts_bucket_start", start.Unix()-1800),
sb.LE("ts_bucket_start", end.Unix()),
)
sb.OrderByAsc("timestamp")
sb.OrderByAsc("name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var spans []spantypes.MinimalSpan
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &spans, query, args...); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying minimal spans")
}
return spans, nil
}
func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, start, end time.Time, spanIDs []string) ([]spantypes.StorableSpan, error) {
if len(spanIDs) == 0 {
return []spantypes.StorableSpan{}, nil
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"DISTINCT ON (span_id) timestamp",
"duration_nano", "span_id", "has_error", "kind",
colServiceName, "name",
"attributes_string", "attributes_number", "attributes_bool", "resources_string",
"events", "status_message", "status_code_string", "kind_string", "parent_span_id",
"flags", "is_remote", "trace_state", "status_code",
"db_name", "db_operation", "http_method", "http_url", "http_host",
"external_http_method", "external_http_url", "response_status_code",
)
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
ids := make([]any, len(spanIDs))
for i, id := range spanIDs {
ids[i] = id
}
sb.Where(
sb.E("trace_id", traceID),
sb.In("span_id", ids...),
sb.GE("ts_bucket_start", start.Unix()-1800),
sb.LE("ts_bucket_start", end.Unix()),
)
sb.OrderByAsc("timestamp")
sb.OrderByAsc("name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var spans []spantypes.StorableSpan
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &spans, query, args...); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying trace spans by IDs")
}
return spans, nil
}

View File

@@ -10,11 +10,9 @@ import (
// Handler exposes HTTP handlers for trace detail APIs.
type Handler interface {
GetWaterfall(http.ResponseWriter, *http.Request)
GetWaterfallV4(http.ResponseWriter, *http.Request)
}
// Module defines the business logic for trace detail operations.
type Module interface {
GetWaterfall(ctx context.Context, traceID string, req *spantypes.PostableWaterfall) (*spantypes.GettableWaterfallTrace, error)
GetWaterfallV4(ctx context.Context, traceID string, selectedSpanID string, uncollapsedSpans []string, selectAllLimit uint) (*spantypes.GettableWaterfallTrace, error)
}

View File

@@ -3,6 +3,7 @@ package impluser
import (
"context"
"log/slog"
"slices"
"strings"
"time"
@@ -20,6 +21,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -369,6 +371,10 @@ func (module *setter) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.WithAdditionalf(err, "cannot delete already deleted user")
}
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(user.Email.String())) {
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
}
deleter, err := module.store.GetUser(ctx, valuer.MustNewUUID(deletedBy))
if err != nil {
return err

View File

@@ -19,8 +19,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const traceOutsideRangeWarn = "Query %s references a trace_id that exists between %s and %s (UTC) but lies outside the selected time range; adjust the time range to see results"
type builderQuery[T any] struct {
logger *slog.Logger
telemetryStore telemetrystore.TelemetryStore
@@ -201,21 +199,7 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return q.executeWindowList(ctx)
}
fromMS, toMS := q.fromMS, q.toMS
if q.spec.Signal == telemetrytypes.SignalTraces || q.spec.Signal == telemetrytypes.SignalLogs {
var overlap bool
var warning string
fromMS, toMS, overlap, warning = q.narrowWindowByTraceID(ctx, fromMS, toMS)
if !overlap {
res := emptyResultFor(q.kind, q.spec.Name)
if warning != "" {
res.Warnings = []string{warning}
}
return res, nil
}
}
stmt, err := q.stmtBuilder.Build(ctx, fromMS, toMS, q.kind, q.spec, q.variables)
stmt, err := q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec, q.variables)
if err != nil {
return nil, err
}
@@ -231,88 +215,6 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return result, nil
}
// narrowWindowByTraceID inspects the filter for trace_id predicates and clamps
// [fromMS,toMS] to the time range stored in signoz_traces.distributed_trace_summary.
// Returns the (possibly narrowed) window, overlap=false when the trace lies
// completely outside the query window (callers should short-circuit), and a
// warning string the caller should attach to the empty result when the trace
// exists but is outside the selected window.
//
// When the trace_id is not present in trace_summary the behaviour differs by
// signal:
// - traces: trace_summary is derived from the spans table, so a missing row
// means no spans exist for that trace_id; we short-circuit to empty.
// - logs: logs can carry a trace_id even when traces are not ingested at all
// (e.g. traces disabled). We must not short-circuit; instead leave the
// window untouched and let the query run.
func (q *builderQuery[T]) narrowWindowByTraceID(ctx context.Context, fromMS, toMS uint64) (uint64, uint64, bool, string) {
if q.spec.Filter == nil || q.spec.Filter.Expression == "" {
return fromMS, toMS, true, ""
}
traceIDs, found := telemetrytraces.ExtractTraceIDsFromFilter(q.spec.Filter.Expression)
if !found || len(traceIDs) == 0 {
return fromMS, toMS, true, ""
}
finder := telemetrytraces.NewTraceTimeRangeFinder(q.telemetryStore)
traceStart, traceEnd, exists, err := finder.GetTraceTimeRangeMulti(ctx, traceIDs)
if err != nil {
return fromMS, toMS, true, ""
}
if !exists {
if q.spec.Signal == telemetrytypes.SignalTraces {
q.logger.DebugContext(ctx, "trace_id not found in trace_summary; short-circuiting traces query to empty",
slog.Any("trace_ids", traceIDs))
return fromMS, toMS, false, ""
}
q.logger.DebugContext(ctx, "trace_id not found in trace_summary; leaving time range untouched for logs",
slog.Any("trace_ids", traceIDs))
return fromMS, toMS, true, ""
}
traceStartMS := uint64(traceStart) / 1_000_000
traceEndMS := uint64(traceEnd) / 1_000_000
if traceStartMS == 0 || traceEndMS == 0 {
return fromMS, toMS, true, ""
}
if traceStartMS > toMS || traceEndMS < fromMS {
traceStartUTC := time.UnixMilli(int64(traceStartMS)).UTC().Format(time.RFC3339)
traceEndUTC := time.UnixMilli(int64(traceEndMS)).UTC().Format(time.RFC3339)
return fromMS, toMS, false, fmt.Sprintf(traceOutsideRangeWarn, q.spec.Name, traceStartUTC, traceEndUTC)
}
if traceStartMS > fromMS {
fromMS = traceStartMS
}
if traceEndMS < toMS {
toMS = traceEndMS
}
q.logger.DebugContext(ctx, "optimized time range using trace_id lookup",
slog.String("signal", q.spec.Signal.StringValue()),
slog.Any("trace_ids", traceIDs),
slog.Uint64("start", fromMS),
slog.Uint64("end", toMS))
return fromMS, toMS, true, ""
}
// emptyResultFor returns an empty result payload appropriate for the given kind.
func emptyResultFor(kind qbtypes.RequestType, queryName string) *qbtypes.Result {
var value any
switch kind {
case qbtypes.RequestTypeTimeSeries:
value = &qbtypes.TimeSeriesData{QueryName: queryName}
case qbtypes.RequestTypeScalar:
value = &qbtypes.ScalarData{QueryName: queryName}
default:
value = &qbtypes.RawData{QueryName: queryName}
}
return &qbtypes.Result{
Type: kind,
Value: value,
}
}
// executeWithContext executes the query with query window and step context for partial value detection.
func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string, args []any) (*qbtypes.Result, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
@@ -408,27 +310,42 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
totalBytes := uint64(0)
start := time.Now()
// Check if filter contains trace_id(s) and optimize time range if needed.
// Applies to both traces (the listing this branch was built for) and logs
// (which carry trace_id and benefit from the same clamp before bucketing).
if q.spec.Signal == telemetrytypes.SignalTraces || q.spec.Signal == telemetrytypes.SignalLogs {
var overlap bool
var warning string
fromMS, toMS, overlap, warning = q.narrowWindowByTraceID(ctx, fromMS, toMS)
if !overlap {
res := &qbtypes.Result{
Type: qbtypes.RequestTypeRaw,
Value: &qbtypes.RawData{
QueryName: q.spec.Name,
},
Stats: qbtypes.ExecStats{
DurationMS: uint64(time.Since(start).Milliseconds()),
},
// Check if filter contains trace_id(s) and optimize time range if needed
if q.spec.Signal == telemetrytypes.SignalTraces &&
q.spec.Filter != nil && q.spec.Filter.Expression != "" {
traceIDs, found := telemetrytraces.ExtractTraceIDsFromFilter(q.spec.Filter.Expression)
if found && len(traceIDs) > 0 {
finder := telemetrytraces.NewTraceTimeRangeFinder(q.telemetryStore)
traceStart, traceEnd, ok := finder.GetTraceTimeRangeMulti(ctx, traceIDs)
traceStartMS := uint64(traceStart) / 1_000_000
traceEndMS := uint64(traceEnd) / 1_000_000
if !ok {
q.logger.DebugContext(ctx, "failed to get trace time range", slog.Any("trace_ids", traceIDs))
} else if traceStartMS > 0 && traceEndMS > 0 {
// no overlap — nothing to return
if uint64(traceStartMS) > toMS || uint64(traceEndMS) < fromMS {
return &qbtypes.Result{
Type: qbtypes.RequestTypeRaw,
Value: &qbtypes.RawData{
QueryName: q.spec.Name,
},
Stats: qbtypes.ExecStats{
DurationMS: uint64(time.Since(start).Milliseconds()),
},
}, nil
}
// clamp window to trace time range before bucketing
if uint64(traceStartMS) > fromMS {
fromMS = uint64(traceStartMS)
}
if uint64(traceEndMS) < toMS {
toMS = uint64(traceEndMS)
}
q.logger.DebugContext(ctx, "optimized time range for traces", slog.Any("trace_ids", traceIDs), slog.Uint64("start", fromMS), slog.Uint64("end", toMS))
}
if warning != "" {
res.Warnings = []string{warning}
}
return res, nil
}
}

View File

@@ -0,0 +1,5 @@
# SigNoz Cloud Integrations
Cloud integrations are unlike the rest of SigNoz integrations.
They have a different UX and so require a different API.
They will also be limited in number and are not expected to have community contributed implementations

View File

@@ -0,0 +1,220 @@
package cloudintegrations
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type cloudProviderAccountsRepository interface {
listConnected(ctx context.Context, orgId string, provider string) ([]integrationtypes.CloudIntegration, *model.ApiError)
get(ctx context.Context, orgId string, provider string, id string) (*integrationtypes.CloudIntegration, *model.ApiError)
getConnectedCloudAccount(ctx context.Context, orgId string, provider string, accountID string) (*integrationtypes.CloudIntegration, *model.ApiError)
// Insert an account or update it by (cloudProvider, id)
// for specified non-empty fields
upsert(
ctx context.Context,
orgId string,
provider string,
id *string,
config *integrationtypes.AccountConfig,
accountId *string,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*integrationtypes.CloudIntegration, *model.ApiError)
}
func newCloudProviderAccountsRepository(store sqlstore.SQLStore) (
*cloudProviderAccountsSQLRepository, error,
) {
return &cloudProviderAccountsSQLRepository{
store: store,
}, nil
}
type cloudProviderAccountsSQLRepository struct {
store sqlstore.SQLStore
}
func (r *cloudProviderAccountsSQLRepository) listConnected(
ctx context.Context, orgId string, cloudProvider string,
) ([]integrationtypes.CloudIntegration, *model.ApiError) {
accounts := []integrationtypes.CloudIntegration{}
err := r.store.BunDB().NewSelect().
Model(&accounts).
Where("org_id = ?", orgId).
Where("provider = ?", cloudProvider).
Where("removed_at is NULL").
Where("account_id is not NULL").
Where("last_agent_report is not NULL").
Order("created_at").
Scan(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"could not query connected cloud accounts: %w", err,
))
}
return accounts, nil
}
func (r *cloudProviderAccountsSQLRepository) get(
ctx context.Context, orgId string, provider string, id string,
) (*integrationtypes.CloudIntegration, *model.ApiError) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
Where("org_id = ?", orgId).
Where("provider = ?", provider).
Where("id = ?", id).
Scan(ctx)
if err == sql.ErrNoRows {
return nil, model.NotFoundError(fmt.Errorf(
"couldn't find account with Id %s", id,
))
} else if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud provider accounts: %w", err,
))
}
return &result, nil
}
func (r *cloudProviderAccountsSQLRepository) getConnectedCloudAccount(
ctx context.Context, orgId string, provider string, accountId string,
) (*integrationtypes.CloudIntegration, *model.ApiError) {
var result integrationtypes.CloudIntegration
err := r.store.BunDB().NewSelect().
Model(&result).
Where("org_id = ?", orgId).
Where("provider = ?", provider).
Where("account_id = ?", accountId).
Where("last_agent_report is not NULL").
Where("removed_at is NULL").
Scan(ctx)
if err == sql.ErrNoRows {
return nil, model.NotFoundError(fmt.Errorf(
"couldn't find connected cloud account %s", accountId,
))
} else if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud provider accounts: %w", err,
))
}
return &result, nil
}
func (r *cloudProviderAccountsSQLRepository) upsert(
ctx context.Context,
orgId string,
provider string,
id *string,
config *integrationtypes.AccountConfig,
accountId *string,
agentReport *integrationtypes.AgentReport,
removedAt *time.Time,
) (*integrationtypes.CloudIntegration, *model.ApiError) {
// Insert
if id == nil {
temp := valuer.GenerateUUID().StringValue()
id = &temp
}
// Prepare clause for setting values in `on conflict do update`
onConflictSetStmts := []string{}
setColStatement := func(col string) string {
return fmt.Sprintf("%s=excluded.%s", col, col)
}
if config != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("config"),
)
}
if accountId != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("account_id"),
)
}
if agentReport != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("last_agent_report"),
)
}
if removedAt != nil {
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("removed_at"),
)
}
// set updated_at to current timestamp if it's an upsert
onConflictSetStmts = append(
onConflictSetStmts, setColStatement("updated_at"),
)
onConflictClause := ""
if len(onConflictSetStmts) > 0 {
onConflictClause = fmt.Sprintf(
"conflict(id) do update SET\n%s",
strings.Join(onConflictSetStmts, ",\n"),
)
}
integration := integrationtypes.CloudIntegration{
OrgID: orgId,
Provider: provider,
Identifiable: types.Identifiable{ID: valuer.MustNewUUID(*id)},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Config: config,
AccountID: accountId,
LastAgentReport: agentReport,
RemovedAt: removedAt,
}
_, dbErr := r.store.BunDB().NewInsert().
Model(&integration).
On(onConflictClause).
Exec(ctx)
if dbErr != nil {
// for now returning internal error even if there is a conflict,
// will be handled better in the future iteration
return nil, model.InternalError(fmt.Errorf(
"could not upsert cloud account record: %w", dbErr,
))
}
upsertedAccount, apiErr := r.get(ctx, orgId, provider, *id)
if apiErr != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't fetch upserted account by id: %w", apiErr.ToError(),
))
}
return upsertedAccount, nil
}

View File

@@ -0,0 +1,43 @@
package cloudintegrations
import (
"github.com/SigNoz/signoz/pkg/errors"
)
var (
CodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
CodeMismatchCloudProvider = errors.MustNewCode("cloud_provider_mismatch")
)
// List of all valid cloud regions on Amazon Web Services
var ValidAWSRegions = map[string]bool{
"af-south-1": true, // Africa (Cape Town).
"ap-east-1": true, // Asia Pacific (Hong Kong).
"ap-northeast-1": true, // Asia Pacific (Tokyo).
"ap-northeast-2": true, // Asia Pacific (Seoul).
"ap-northeast-3": true, // Asia Pacific (Osaka).
"ap-south-1": true, // Asia Pacific (Mumbai).
"ap-south-2": true, // Asia Pacific (Hyderabad).
"ap-southeast-1": true, // Asia Pacific (Singapore).
"ap-southeast-2": true, // Asia Pacific (Sydney).
"ap-southeast-3": true, // Asia Pacific (Jakarta).
"ap-southeast-4": true, // Asia Pacific (Melbourne).
"ca-central-1": true, // Canada (Central).
"ca-west-1": true, // Canada West (Calgary).
"eu-central-1": true, // Europe (Frankfurt).
"eu-central-2": true, // Europe (Zurich).
"eu-north-1": true, // Europe (Stockholm).
"eu-south-1": true, // Europe (Milan).
"eu-south-2": true, // Europe (Spain).
"eu-west-1": true, // Europe (Ireland).
"eu-west-2": true, // Europe (London).
"eu-west-3": true, // Europe (Paris).
"il-central-1": true, // Israel (Tel Aviv).
"me-central-1": true, // Middle East (UAE).
"me-south-1": true, // Middle East (Bahrain).
"sa-east-1": true, // South America (Sao Paulo).
"us-east-1": true, // US East (N. Virginia).
"us-east-2": true, // US East (Ohio).
"us-west-1": true, // US West (N. California).
"us-west-2": true, // US West (Oregon).
}

View File

@@ -0,0 +1,625 @@
package cloudintegrations
import (
"context"
"fmt"
"net/url"
"slices"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/exp/maps"
)
var SupportedCloudProviders = []string{
"aws",
}
func validateCloudProviderName(name string) *model.ApiError {
if !slices.Contains(SupportedCloudProviders, name) {
return model.BadRequest(fmt.Errorf("invalid cloud provider: %s", name))
}
return nil
}
type Controller struct {
accountsRepo cloudProviderAccountsRepository
serviceConfigRepo ServiceConfigDatabase
}
func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
accountsRepo, err := newCloudProviderAccountsRepository(sqlStore)
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
}
serviceConfigRepo, err := newServiceConfigRepository(sqlStore)
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider service config repo: %w", err)
}
return &Controller{
accountsRepo: accountsRepo,
serviceConfigRepo: serviceConfigRepo,
}, nil
}
type ConnectedAccountsListResponse struct {
Accounts []integrationtypes.Account `json:"accounts"`
}
func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) (
*ConnectedAccountsListResponse, *model.ApiError,
) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgId, cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list cloud accounts")
}
connectedAccounts := []integrationtypes.Account{}
for _, a := range accountRecords {
connectedAccounts = append(connectedAccounts, a.Account())
}
return &ConnectedAccountsListResponse{
Accounts: connectedAccounts,
}, nil
}
type GenerateConnectionUrlRequest struct {
// Optional. To be specified for updates.
AccountId *string `json:"account_id,omitempty"`
AccountConfig integrationtypes.AccountConfig `json:"account_config"`
AgentConfig SigNozAgentConfig `json:"agent_config"`
}
type SigNozAgentConfig struct {
// The region in which SigNoz agent should be installed.
Region string `json:"region"`
IngestionUrl string `json:"ingestion_url"`
IngestionKey string `json:"ingestion_key"`
SigNozAPIUrl string `json:"signoz_api_url"`
SigNozAPIKey string `json:"signoz_api_key"`
Version string `json:"version,omitempty"`
}
type GenerateConnectionUrlResponse struct {
AccountId string `json:"account_id"`
ConnectionUrl string `json:"connection_url"`
}
func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest) (*GenerateConnectionUrlResponse, *model.ApiError) {
// Account connection with a simple connection URL may not be available for all providers.
if cloudProvider != "aws" {
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
}
account, apiErr := c.accountsRepo.upsert(
ctx, orgId, cloudProvider, req.AccountId, &req.AccountConfig, nil, nil, nil,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
}
agentVersion := "v0.0.8"
if req.AgentConfig.Version != "" {
agentVersion = req.AgentConfig.Version
}
connectionUrl := fmt.Sprintf(
"https://%s.console.aws.amazon.com/cloudformation/home?region=%s#/stacks/quickcreate?",
req.AgentConfig.Region, req.AgentConfig.Region,
)
for qp, value := range map[string]string{
"param_SigNozIntegrationAgentVersion": agentVersion,
"param_SigNozApiUrl": req.AgentConfig.SigNozAPIUrl,
"param_SigNozApiKey": req.AgentConfig.SigNozAPIKey,
"param_SigNozAccountId": account.ID.StringValue(),
"param_IngestionUrl": req.AgentConfig.IngestionUrl,
"param_IngestionKey": req.AgentConfig.IngestionKey,
"stackName": "signoz-integration",
"templateURL": fmt.Sprintf(
"https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json",
agentVersion,
),
} {
connectionUrl += fmt.Sprintf("&%s=%s", qp, url.QueryEscape(value))
}
return &GenerateConnectionUrlResponse{
AccountId: account.ID.StringValue(),
ConnectionUrl: connectionUrl,
}, nil
}
type AccountStatusResponse struct {
Id string `json:"id"`
CloudAccountId *string `json:"cloud_account_id,omitempty"`
Status integrationtypes.AccountStatus `json:"status"`
}
func (c *Controller) GetAccountStatus(ctx context.Context, orgId string, cloudProvider string, accountId string) (
*AccountStatusResponse, *model.ApiError,
) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
account, apiErr := c.accountsRepo.get(ctx, orgId, cloudProvider, accountId)
if apiErr != nil {
return nil, apiErr
}
resp := AccountStatusResponse{
Id: account.ID.StringValue(),
CloudAccountId: account.AccountID,
Status: account.Status(),
}
return &resp, nil
}
type AgentCheckInRequest struct {
ID string `json:"account_id"`
AccountID string `json:"cloud_account_id"`
// Arbitrary cloud specific Agent data
Data map[string]any `json:"data,omitempty"`
}
type AgentCheckInResponse struct {
AccountId string `json:"account_id"`
CloudAccountId string `json:"cloud_account_id"`
RemovedAt *time.Time `json:"removed_at"`
IntegrationConfig IntegrationConfigForAgent `json:"integration_config"`
}
type IntegrationConfigForAgent struct {
EnabledRegions []string `json:"enabled_regions"`
TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"`
}
func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest) (*AgentCheckInResponse, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
existingAccount, apiErr := c.accountsRepo.get(ctx, orgId, cloudProvider, req.ID)
if existingAccount != nil && existingAccount.AccountID != nil && *existingAccount.AccountID != req.AccountID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in with new %s account id %s for account %s with existing %s id %s",
cloudProvider, req.AccountID, existingAccount.ID.StringValue(), cloudProvider, *existingAccount.AccountID,
))
}
existingAccount, apiErr = c.accountsRepo.getConnectedCloudAccount(ctx, orgId, cloudProvider, req.AccountID)
if existingAccount != nil && existingAccount.ID.StringValue() != req.ID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in to %s account %s with id %s. already connected with id %s",
cloudProvider, req.AccountID, req.ID, existingAccount.ID.StringValue(),
))
}
agentReport := integrationtypes.AgentReport{
TimestampMillis: time.Now().UnixMilli(),
Data: req.Data,
}
account, apiErr := c.accountsRepo.upsert(
ctx, orgId, cloudProvider, &req.ID, nil, &req.AccountID, &agentReport, nil,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
}
// prepare and return integration config to be consumed by agent
compiledStrategy, err := NewCompiledCollectionStrategy(cloudProvider)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't init telemetry collection strategy: %w", err,
))
}
agentConfig := IntegrationConfigForAgent{
EnabledRegions: []string{},
TelemetryCollectionStrategy: compiledStrategy,
}
if account.Config != nil && account.Config.EnabledRegions != nil {
agentConfig.EnabledRegions = account.Config.EnabledRegions
}
services, err := services.Map(cloudProvider)
if err != nil {
return nil, err
}
svcConfigs, apiErr := c.serviceConfigRepo.getAllForAccount(
ctx, orgId, account.ID.StringValue(),
)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, "couldn't get service configs for cloud account",
)
}
// accumulate config in a fixed order to ensure same config generated across runs
configuredServices := maps.Keys(svcConfigs)
slices.Sort(configuredServices)
for _, svcType := range configuredServices {
definition, ok := services[svcType]
if !ok {
continue
}
config := svcConfigs[svcType]
err := AddServiceStrategy(svcType, compiledStrategy, definition.Strategy, config)
if err != nil {
return nil, err
}
}
return &AgentCheckInResponse{
AccountId: account.ID.StringValue(),
CloudAccountId: *account.AccountID,
RemovedAt: account.RemovedAt,
IntegrationConfig: agentConfig,
}, nil
}
type UpdateAccountConfigRequest struct {
Config integrationtypes.AccountConfig `json:"config"`
}
func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*integrationtypes.Account, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
accountRecord, apiErr := c.accountsRepo.upsert(
ctx, orgId, cloudProvider, &accountId, &req.Config, nil, nil, nil,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
}
account := accountRecord.Account()
return &account, nil
}
func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*integrationtypes.CloudIntegration, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
account, apiErr := c.accountsRepo.get(ctx, orgId, cloudProvider, accountId)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't disconnect account")
}
tsNow := time.Now()
account, apiErr = c.accountsRepo.upsert(
ctx, orgId, cloudProvider, &accountId, nil, nil, nil, &tsNow,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't disconnect account")
}
return account, nil
}
type ListServicesResponse struct {
Services []ServiceSummary `json:"services"`
}
func (c *Controller) ListServices(
ctx context.Context,
orgID string,
cloudProvider string,
cloudAccountId *string,
) (*ListServicesResponse, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
definitions, apiErr := services.List(cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list cloud services")
}
svcConfigs := map[string]*integrationtypes.CloudServiceConfig{}
if cloudAccountId != nil {
activeAccount, apiErr := c.accountsRepo.getConnectedCloudAccount(
ctx, orgID, cloudProvider, *cloudAccountId,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't get active account")
}
svcConfigs, apiErr = c.serviceConfigRepo.getAllForAccount(
ctx, orgID, activeAccount.ID.StringValue(),
)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, "couldn't get service configs for cloud account",
)
}
}
summaries := []ServiceSummary{}
for _, def := range definitions {
summary := ServiceSummary{
Metadata: def.Metadata,
}
summary.Config = svcConfigs[summary.Id]
summaries = append(summaries, summary)
}
return &ListServicesResponse{
Services: summaries,
}, nil
}
func (c *Controller) GetServiceDetails(
ctx context.Context,
orgID string,
cloudProvider string,
serviceId string,
cloudAccountId *string,
) (*ServiceDetails, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
definition, err := services.GetServiceDefinition(cloudProvider, serviceId)
if err != nil {
return nil, err
}
details := ServiceDetails{
Definition: *definition,
}
if cloudAccountId != nil {
activeAccount, apiErr := c.accountsRepo.getConnectedCloudAccount(
ctx, orgID, cloudProvider, *cloudAccountId,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't get active account")
}
config, apiErr := c.serviceConfigRepo.get(
ctx, orgID, activeAccount.ID.StringValue(), serviceId,
)
if apiErr != nil && apiErr.Type() != model.ErrorNotFound {
return nil, model.WrapApiError(apiErr, "couldn't fetch service config")
}
if config != nil {
details.Config = config
enabled := false
if config.Metrics != nil && config.Metrics.Enabled {
enabled = true
}
// add links to service dashboards, making them clickable.
for i, d := range definition.Assets.Dashboards {
dashboardUuid := c.dashboardUuid(
cloudProvider, serviceId, d.Id,
)
if enabled {
definition.Assets.Dashboards[i].Url = fmt.Sprintf("/dashboard/%s", dashboardUuid)
} else {
definition.Assets.Dashboards[i].Url = "" // to unset the in-memory URL if enabled once and disabled afterwards
}
}
}
}
return &details, nil
}
type UpdateServiceConfigRequest struct {
CloudAccountId string `json:"cloud_account_id"`
Config integrationtypes.CloudServiceConfig `json:"config"`
}
func (u *UpdateServiceConfigRequest) Validate(def *services.Definition) error {
if def.Id != services.S3Sync && u.Config.Logs != nil && u.Config.Logs.S3Buckets != nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "s3 buckets can only be added to service-type[%s]", services.S3Sync)
} else if def.Id == services.S3Sync && u.Config.Logs != nil && u.Config.Logs.S3Buckets != nil {
for region := range u.Config.Logs.S3Buckets {
if _, found := ValidAWSRegions[region]; !found {
return errors.NewInvalidInputf(CodeInvalidCloudRegion, "invalid cloud region: %s", region)
}
}
}
return nil
}
type UpdateServiceConfigResponse struct {
Id string `json:"id"`
Config integrationtypes.CloudServiceConfig `json:"config"`
}
func (c *Controller) UpdateServiceConfig(
ctx context.Context,
orgID string,
cloudProvider string,
serviceType string,
req *UpdateServiceConfigRequest,
) (*UpdateServiceConfigResponse, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr
}
// can only update config for a valid service.
definition, err := services.GetServiceDefinition(cloudProvider, serviceType)
if err != nil {
return nil, err
}
if err := req.Validate(definition); err != nil {
return nil, err
}
// can only update config for a connected cloud account id
_, apiErr := c.accountsRepo.getConnectedCloudAccount(
ctx, orgID, cloudProvider, req.CloudAccountId,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't find connected cloud account")
}
updatedConfig, apiErr := c.serviceConfigRepo.upsert(
ctx, orgID, cloudProvider, req.CloudAccountId, serviceType, req.Config,
)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't update service config")
}
return &UpdateServiceConfigResponse{
Id: serviceType,
Config: *updatedConfig,
}, nil
}
// All dashboards that are available based on cloud integrations configuration
// across all cloud providers
func (c *Controller) AvailableDashboards(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) {
allDashboards := []*dashboardtypes.Dashboard{}
for _, provider := range []string{"aws"} {
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider)
if apiErr != nil {
return nil, model.WrapApiError(
apiErr, fmt.Sprintf("couldn't get available dashboards for %s", provider),
)
}
allDashboards = append(allDashboards, providerDashboards...)
}
return allDashboards, nil
}
func (c *Controller) AvailableDashboardsForCloudProvider(ctx context.Context, orgID valuer.UUID, cloudProvider string) ([]*dashboardtypes.Dashboard, *model.ApiError) {
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID.StringValue(), cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
}
// for v0, service dashboards are only available when metrics are enabled.
servicesWithAvailableMetrics := map[string]*time.Time{}
for _, ar := range accountRecords {
if ar.AccountID != nil {
configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount(
ctx, orgID.StringValue(), ar.ID.StringValue(),
)
if apiErr != nil {
return nil, apiErr
}
for svcId, config := range configsBySvcId {
if config.Metrics != nil && config.Metrics.Enabled {
servicesWithAvailableMetrics[svcId] = &ar.CreatedAt
}
}
}
}
allServices, apiErr := services.List(cloudProvider)
if apiErr != nil {
return nil, apiErr
}
svcDashboards := []*dashboardtypes.Dashboard{}
for _, svc := range allServices {
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
if serviceDashboardsCreatedAt != nil {
for _, d := range svc.Assets.Dashboards {
author := fmt.Sprintf("%s-integration", cloudProvider)
svcDashboards = append(svcDashboards, &dashboardtypes.Dashboard{
ID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
Locked: true,
Data: *d.Definition,
TimeAuditable: types.TimeAuditable{
CreatedAt: *serviceDashboardsCreatedAt,
UpdatedAt: *serviceDashboardsCreatedAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: author,
UpdatedBy: author,
},
OrgID: orgID,
})
}
servicesWithAvailableMetrics[svc.Id] = nil
}
}
return svcDashboards, nil
}
func (c *Controller) GetDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
if apiErr != nil {
return nil, apiErr
}
allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider)
if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list available dashboards")
}
for _, d := range allDashboards {
if d.ID == dashboardUuid {
return d, nil
}
}
return nil, model.NotFoundError(fmt.Errorf("couldn't find dashboard with uuid: %s", dashboardUuid))
}
func (c *Controller) dashboardUuid(
cloudProvider string, svcId string, dashboardId string,
) string {
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
}
func (c *Controller) parseDashboardUuid(dashboardUuid string) (cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError) {
parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 || parts[0] != "cloud-integration" {
return "", "", "", model.BadRequest(fmt.Errorf("invalid cloud integration dashboard id"))
}
return parts[1], parts[2], parts[3], nil
}
func (c *Controller) IsCloudIntegrationDashboardUuid(dashboardUuid string) bool {
_, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
return apiErr == nil
}

View File

@@ -0,0 +1,94 @@
package cloudintegrations
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
)
type ServiceSummary struct {
services.Metadata
Config *integrationtypes.CloudServiceConfig `json:"config"`
}
type ServiceDetails struct {
services.Definition
Config *integrationtypes.CloudServiceConfig `json:"config"`
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
}
type AccountStatus struct {
Integration AccountIntegrationStatus `json:"integration"`
}
type AccountIntegrationStatus struct {
LastHeartbeatTsMillis *int64 `json:"last_heartbeat_ts_ms"`
}
type LogsConfig struct {
Enabled bool `json:"enabled"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
}
type MetricsConfig struct {
Enabled bool `json:"enabled"`
}
type ServiceConnectionStatus struct {
Logs *SignalConnectionStatus `json:"logs"`
Metrics *SignalConnectionStatus `json:"metrics"`
}
type SignalConnectionStatus struct {
LastReceivedTsMillis int64 `json:"last_received_ts_ms"` // epoch milliseconds
LastReceivedFrom string `json:"last_received_from"` // resource identifier
}
type CompiledCollectionStrategy = services.CollectionStrategy
func NewCompiledCollectionStrategy(provider string) (*CompiledCollectionStrategy, error) {
if provider == "aws" {
return &CompiledCollectionStrategy{
Provider: "aws",
AWSMetrics: &services.AWSMetricsStrategy{},
AWSLogs: &services.AWSLogsStrategy{},
}, nil
}
return nil, errors.NewNotFoundf(services.CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", provider)
}
// Helper for accumulating strategies for enabled services.
func AddServiceStrategy(serviceType string, cs *CompiledCollectionStrategy,
definitionStrat *services.CollectionStrategy, config *integrationtypes.CloudServiceConfig) error {
if definitionStrat.Provider != cs.Provider {
return errors.NewInternalf(CodeMismatchCloudProvider, "can't add %s service strategy to compiled strategy for %s",
definitionStrat.Provider, cs.Provider)
}
if cs.Provider == "aws" {
if config.Logs != nil && config.Logs.Enabled {
if serviceType == services.S3Sync {
// S3 bucket sync; No cloudwatch logs are appended for this service type;
// Though definition is populated with a custom cloudwatch group that helps in calculating logs connection status
cs.S3Buckets = config.Logs.S3Buckets
} else if definitionStrat.AWSLogs != nil { // services that includes a logs subscription
cs.AWSLogs.Subscriptions = append(
cs.AWSLogs.Subscriptions,
definitionStrat.AWSLogs.Subscriptions...,
)
}
}
if config.Metrics != nil && config.Metrics.Enabled && definitionStrat.AWSMetrics != nil {
cs.AWSMetrics.StreamFilters = append(
cs.AWSMetrics.StreamFilters,
definitionStrat.AWSMetrics.StreamFilters...,
)
}
return nil
}
return errors.NewNotFoundf(services.CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", cs.Provider)
}

View File

@@ -0,0 +1,165 @@
package cloudintegrations
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type ServiceConfigDatabase interface {
get(
ctx context.Context,
orgID string,
cloudAccountId string,
serviceType string,
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
upsert(
ctx context.Context,
orgID string,
cloudProvider string,
cloudAccountId string,
serviceId string,
config integrationtypes.CloudServiceConfig,
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
getAllForAccount(
ctx context.Context,
orgID string,
cloudAccountId string,
) (
configsBySvcId map[string]*integrationtypes.CloudServiceConfig,
apiErr *model.ApiError,
)
}
func newServiceConfigRepository(store sqlstore.SQLStore) (
*serviceConfigSQLRepository, error,
) {
return &serviceConfigSQLRepository{
store: store,
}, nil
}
type serviceConfigSQLRepository struct {
store sqlstore.SQLStore
}
func (r *serviceConfigSQLRepository) get(
ctx context.Context,
orgID string,
cloudAccountId string,
serviceType string,
) (*integrationtypes.CloudServiceConfig, *model.ApiError) {
var result integrationtypes.CloudIntegrationService
err := r.store.BunDB().NewSelect().
Model(&result).
Join("JOIN cloud_integration ci ON ci.id = cis.cloud_integration_id").
Where("ci.org_id = ?", orgID).
Where("ci.id = ?", cloudAccountId).
Where("cis.type = ?", serviceType).
Scan(ctx)
if err == sql.ErrNoRows {
return nil, model.NotFoundError(fmt.Errorf(
"couldn't find config for cloud account %s",
cloudAccountId,
))
} else if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud service config: %w", err,
))
}
return &result.Config, nil
}
func (r *serviceConfigSQLRepository) upsert(
ctx context.Context,
orgID string,
cloudProvider string,
cloudAccountId string,
serviceId string,
config integrationtypes.CloudServiceConfig,
) (*integrationtypes.CloudServiceConfig, *model.ApiError) {
// get cloud integration id from account id
// if the account is not connected, we don't need to upsert the config
var cloudIntegrationId string
err := r.store.BunDB().NewSelect().
Model((*integrationtypes.CloudIntegration)(nil)).
Column("id").
Where("provider = ?", cloudProvider).
Where("account_id = ?", cloudAccountId).
Where("org_id = ?", orgID).
Where("removed_at is NULL").
Where("last_agent_report is not NULL").
Scan(ctx, &cloudIntegrationId)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"couldn't query cloud integration id: %w", err,
))
}
serviceConfig := integrationtypes.CloudIntegrationService{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Config: config,
Type: serviceId,
CloudIntegrationID: cloudIntegrationId,
}
_, err = r.store.BunDB().NewInsert().
Model(&serviceConfig).
On("conflict(cloud_integration_id, type) do update set config=excluded.config, updated_at=excluded.updated_at").
Exec(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"could not upsert cloud service config: %w", err,
))
}
return &serviceConfig.Config, nil
}
func (r *serviceConfigSQLRepository) getAllForAccount(
ctx context.Context,
orgID string,
cloudAccountId string,
) (map[string]*integrationtypes.CloudServiceConfig, *model.ApiError) {
serviceConfigs := []integrationtypes.CloudIntegrationService{}
err := r.store.BunDB().NewSelect().
Model(&serviceConfigs).
Join("JOIN cloud_integration ci ON ci.id = cis.cloud_integration_id").
Where("ci.id = ?", cloudAccountId).
Where("ci.org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf(
"could not query service configs from db: %w", err,
))
}
result := map[string]*integrationtypes.CloudServiceConfig{}
for _, r := range serviceConfigs {
result[r.Type] = &r.Config
}
return result, nil
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 85 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.5" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M0 41.579C0 20.293 17.84 3.157 40 3.157s40 17.136 40 38.422S62.16 80 40 80 0 62.864 0 41.579z" fill="#9d5025"/><path d="M0 38.422C0 17.136 17.84 0 40 0s40 17.136 40 38.422-17.84 38.422-40 38.422S0 59.707 0 38.422z" fill="#f58536"/><path d="M51.672 7.387v13.952H28.327V7.387zm18.061 40.378v11.364h-11.83V47.765zm-14.958 0v11.364h-11.83V47.765zm-18.206 0v11.364h-11.83V47.765zm-14.959 0v11.364H9.78V47.765z"/><path d="M14.63 37.929h2.13v11.149h-2.13z"/><path d="M14.63 37.929h17.088v2.045H14.63z"/><path d="M29.589 37.929h2.13v11.149H29.59zm18.206 0h2.13v11.149h-2.13z"/><path d="M47.795 37.929h17.088v2.045H47.795z"/><path d="M62.754 37.929h2.13v11.149h-2.129zm-40.631-7.954h2.13v8.977h-2.13zM38.935 19.28h2.13v10.859h-2.129z"/><path d="M22.123 29.116h35.32v2.045h-35.32z"/><path d="M55.314 29.975h2.13v8.977h-2.129z"/></g></symbol></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,467 @@
{
"id": "alb",
"title": "ALB",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supported_signals": {
"metrics": true,
"logs": false
},
"data_collected": {
"metrics": [
{
"name": "aws_ApplicationELB_ActiveConnectionCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ActiveConnectionCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ActiveConnectionCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ActiveConnectionCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_AnomalousHostCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_AnomalousHostCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_AnomalousHostCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_AnomalousHostCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ConsumedLCUs_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ConsumedLCUs_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ConsumedLCUs_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ConsumedLCUs_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyHostCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyHostCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyHostCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyHostCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateDNS_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateDNS_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateDNS_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateDNS_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateRouting_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateRouting_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateRouting_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_HealthyStateRouting_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_MitigatedHostCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_MitigatedHostCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_MitigatedHostCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_MitigatedHostCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_NewConnectionCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_NewConnectionCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_NewConnectionCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_NewConnectionCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_PeakLCUs_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_PeakLCUs_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_PeakLCUs_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_PeakLCUs_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ProcessedBytes_count",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ProcessedBytes_max",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ProcessedBytes_min",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_ProcessedBytes_sum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCountPerTarget_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCountPerTarget_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCountPerTarget_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCountPerTarget_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_RequestCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_TargetResponseTime_count",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_TargetResponseTime_max",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_TargetResponseTime_min",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_TargetResponseTime_sum",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnHealthyHostCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnHealthyHostCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnHealthyHostCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnHealthyHostCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateDNS_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateDNS_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateDNS_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateDNS_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateRouting_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateRouting_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateRouting_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ApplicationELB_UnhealthyStateRouting_sum",
"unit": "None",
"type": "Gauge",
"description": ""
}
],
"logs": []
},
"telemetry_collection_strategy": {
"aws_metrics": {
"cloudwatch_metric_stream_filters": [
{
"Namespace": "AWS/ApplicationELB"
}
]
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "ALB Overview",
"description": "Overview of Application Load Balancer",
"image": "file://assets/dashboards/overview.png",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,3 @@
### Monitor Application Load Balancers with SigNoz
Collect key ALB metrics and view them with an out of the box dashboard.

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

View File

@@ -0,0 +1,14 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
<stop stop-color="#4D27A8" offset="0%"></stop>
<stop stop-color="#A166FF" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="url(#linearGradient-1)">
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
</g>
<path d="M6,6.76751613 L8,5.43446738 L8,18.5659476 L6,17.2328988 L6,6.76751613 Z M5,6.49950633 L5,17.4999086 C5,17.6669147 5.084,17.8239204 5.223,17.9159238 L8.223,19.9159969 C8.307,19.971999 8.403,20 8.5,20 C8.581,20 8.662,19.9809993 8.736,19.9409978 C8.898,19.8539947 9,19.6849885 9,19.4999817 L9,16.9998903 L10,16.9998903 L10,15.9998537 L9,15.9998537 L9,7.99956118 L10,7.99956118 L10,6.99952461 L9,6.99952461 L9,4.49943319 C9,4.31542646 8.898,4.14542025 8.736,4.0594171 C8.574,3.97241392 8.377,3.98141425 8.223,4.08341798 L5.223,6.08349112 C5.084,6.17649452 5,6.33250022 5,6.49950633 L5,6.49950633 Z M19,17.2328988 L17,18.5659476 L17,5.43446738 L19,6.76751613 L19,17.2328988 Z M19.777,6.08349112 L16.777,4.08341798 C16.623,3.98141425 16.426,3.97241392 16.264,4.0594171 C16.102,4.14542025 16,4.31542646 16,4.49943319 L16,6.99952461 L15,6.99952461 L15,7.99956118 L16,7.99956118 L16,15.9998537 L15,15.9998537 L15,16.9998903 L16,16.9998903 L16,19.4999817 C16,19.6849885 16.102,19.8539947 16.264,19.9409978 C16.338,19.9809993 16.419,20 16.5,20 C16.597,20 16.693,19.971999 16.777,19.9159969 L19.777,17.9159238 C19.916,17.8239204 20,17.6669147 20,17.4999086 L20,6.49950633 C20,6.33250022 19.916,6.17649452 19.777,6.08349112 L19.777,6.08349112 Z M13,7.99956118 L14,7.99956118 L14,6.99952461 L13,6.99952461 L13,7.99956118 Z M11,7.99956118 L12,7.99956118 L12,6.99952461 L11,6.99952461 L11,7.99956118 Z M13,16.9998903 L14,16.9998903 L14,15.9998537 L13,15.9998537 L13,16.9998903 Z M11,16.9998903 L12,16.9998903 L12,15.9998537 L11,15.9998537 L11,16.9998903 Z M13.18,14.884813 L10.18,12.3847215 C10.065,12.288718 10,12.1487129 10,11.9997075 C10,11.851702 10.065,11.7106969 10.18,11.6156934 L13.18,9.11560199 L13.82,9.88463011 L11.281,11.9997075 L13.82,14.1157848 L13.18,14.884813 Z" id="Amazon-API-Gateway_Icon_16_Squid" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,199 @@
{
"id": "api-gateway",
"title": "API Gateway",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supported_signals": {
"metrics": true,
"logs": true
},
"data_collected": {
"metrics": [
{
"name": "aws_ApiGateway_4XXError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4XXError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4XXError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4XXError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5XXError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5XXError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5XXError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5XXError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheHitCount_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheHitCount_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheHitCount_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheHitCount_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheMissCount_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheMissCount_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheMissCount_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_CacheMissCount_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Count_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Count_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Count_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Count_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationLatency_count",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationLatency_max",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationLatency_min",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationLatency_sum",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Latency_count",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Latency_max",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Latency_min",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_Latency_sum",
"unit": "Milliseconds",
"type": "Gauge"
}
],
"logs": [
{
"name": "Account Id",
"path": "resources.cloud.account.id",
"type": "string"
},
{
"name": "Log Group Name",
"path": "resources.aws.cloudwatch.log_group_name",
"type": "string"
},
{
"name": "Log Stream Name",
"path": "resources.aws.cloudwatch.log_stream_name",
"type": "string"
}
]
},
"telemetry_collection_strategy": {
"aws_metrics": {
"cloudwatch_metric_stream_filters": [
{
"Namespace": "AWS/ApiGateway"
}
]
},
"aws_logs": {
"cloudwatch_logs_subscriptions": [
{
"log_group_name_prefix": "API-Gateway",
"filter_pattern": ""
}
]
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "API Gateway Overview",
"description": "Overview of API Gateway",
"image": "file://assets/dashboards/overview.png",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,3 @@
### Monitor API Gateway with SigNoz
Collect key API Gateway metrics and view them with an out of the box dashboard.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1,394 @@
{
"id": "dynamodb",
"title": "DynamoDB",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supported_signals": {
"metrics": true,
"logs": false
},
"data_collected": {
"metrics": [
{
"name": "aws_DynamoDB_AccountMaxReads_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxReads_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxReads_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxReads_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelReads_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelReads_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelReads_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelReads_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxWrites_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxWrites_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxWrites_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountMaxWrites_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ReturnedItemCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ReturnedItemCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ReturnedItemCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ReturnedItemCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_SuccessfulRequestLatency_count",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_SuccessfulRequestLatency_max",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_SuccessfulRequestLatency_min",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_SuccessfulRequestLatency_sum",
"unit": "Milliseconds",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ThrottledRequests_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ThrottledRequests_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ThrottledRequests_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_ThrottledRequests_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_UserErrors_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_UserErrors_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_UserErrors_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_UserErrors_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_WriteThrottleEvents_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_WriteThrottleEvents_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_WriteThrottleEvents_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_DynamoDB_WriteThrottleEvents_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
}
]
},
"telemetry_collection_strategy": {
"aws_metrics": {
"cloudwatch_metric_stream_filters": [
{
"Namespace": "AWS/DynamoDB"
}
]
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "DynamoDB Overview",
"description": "Overview of DynamoDB",
"image": "file://assets/dashboards/overview.png",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,3 @@
### Monitor DynamoDB with SigNoz
Collect DynamoDB Key Metrics and view them with an out of the box dashboard.

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<path fill="#9D5025" d="M1.702 2.98L1 3.312v9.376l.702.332 2.842-4.777L1.702 2.98z" />
<path fill="#F58536" d="M3.339 12.657l-1.637.363V2.98l1.637.353v9.324z" />
<path fill="#9D5025" d="M2.476 2.612l.863-.406 4.096 6.216-4.096 5.372-.863-.406V2.612z" />
<path fill="#F58536" d="M5.38 13.248l-2.041.546V2.206l2.04.548v10.494z" />
<path fill="#9D5025" d="M4.3 1.75l1.08-.512 6.043 7.864-6.043 5.66-1.08-.511V1.749z" />
<path fill="#F58536" d="M7.998 13.856l-2.618.906V1.238l2.618.908v11.71z" />
<path fill="#9D5025" d="M6.602.66L7.998 0l6.538 8.453L7.998 16l-1.396-.66V.66z" />
<path fill="#F58536" d="M15 12.686L7.998 16V0L15 3.314v9.372z" />
</svg>

After

Width:  |  Height:  |  Size: 805 B

View File

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

View File

@@ -0,0 +1,3 @@
### Monitor EC2 with SigNoz
Collect key EC2 metrics and view them with an out of the box dashboard.

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

View File

@@ -0,0 +1,851 @@
{
"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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

View File

@@ -0,0 +1,851 @@
{
"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

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
<title>Icon-Architecture/64/Arch_Amazon-Elastic-Container-Service_64</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
<stop stop-color="#C8511B" offset="0%"></stop>
<stop stop-color="#FF9900" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Icon-Architecture/64/Arch_Amazon-Elastic-Container-Service_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icon-Architecture-BG/64/Containers" fill="url(#linearGradient-1)">
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
</g>
<path d="M64,48.2340095 L56,43.4330117 L56,32.0000169 C56,31.6440171 55.812,31.3150172 55.504,31.1360173 L44,24.4260204 L44,14.7520248 L64,26.5710194 L64,48.2340095 Z M65.509,25.13902 L43.509,12.139026 C43.199,11.9560261 42.818,11.9540261 42.504,12.131026 C42.193,12.3090259 42,12.6410257 42,13.0000256 L42,25.0000201 C42,25.3550199 42.189,25.6840198 42.496,25.8640197 L54,32.5740166 L54,44.0000114 C54,44.3510113 54.185,44.6770111 54.486,44.857011 L64.486,50.8570083 C64.644,50.9520082 64.822,51 65,51 C65.17,51 65.34,50.9570082 65.493,50.8700083 C65.807,50.6930084 66,50.3600085 66,50 L66,26.0000196 C66,25.6460198 65.814,25.31902 65.509,25.13902 L65.509,25.13902 Z M40.445,66.863001 L17,54.3990067 L17,26.5710194 L37,14.7520248 L37,24.4510204 L26.463,31.1560173 C26.175,31.3400172 26,31.6580171 26,32.0000169 L26,49.0000091 C26,49.373009 26.208,49.7150088 26.538,49.8870087 L39.991,56.8870055 C40.28,57.0370055 40.624,57.0380055 40.912,56.8880055 L53.964,50.1440086 L61.996,54.9640064 L40.445,66.863001 Z M64.515,54.1420068 L54.515,48.1420095 C54.217,47.9640096 53.849,47.9520096 53.541,48.1120095 L40.455,54.8730065 L28,48.3930094 L28,32.5490167 L38.537,25.8440197 C38.825,25.6600198 39,25.3420199 39,25.0000201 L39,13.0000256 C39,12.6410257 38.808,12.3090259 38.496,12.131026 C38.184,11.9540261 37.802,11.9560261 37.491,12.139026 L15.491,25.13902 C15.187,25.31902 15,25.6460198 15,26.0000196 L15,55 C15,55.3690062 15.204,55.7090061 15.53,55.883006 L39.984,68.8830001 C40.131,68.961 40.292,69 40.453,69 C40.62,69 40.786,68.958 40.937,68.8750001 L64.484,55.875006 C64.797,55.7020061 64.993,55.3750062 65.0001416,55.0180064 C65.006,54.6600066 64.821,54.3260067 64.515,54.1420068 L64.515,54.1420068 Z" id="Amazon-Elastic-Container-Service_Icon_64_Squid" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,872 @@
{
"id": "ecs",
"title": "ECS",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supported_signals": {
"metrics": true,
"logs": true
},
"data_collected": {
"metrics": [
{
"name": "aws_ECS_CPUUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_CPUUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_CPUUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_CPUUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_count",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_max",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_min",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_sum",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_count",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_max",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_min",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_sum",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuReserved_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuReserved_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuReserved_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuReserved_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuUtilized_count",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuUtilized_max",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuUtilized_min",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_CpuUtilized_sum",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DeploymentCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DeploymentCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DeploymentCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DeploymentCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_count",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_max",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_min",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_sum",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_count",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_max",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_min",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_sum",
"unit": "Gigabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryReserved_count",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryReserved_max",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryReserved_min",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryReserved_sum",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryUtilized_count",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryUtilized_max",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryUtilized_min",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_MemoryUtilized_sum",
"unit": "Megabytes",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_PendingTaskCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_PendingTaskCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_PendingTaskCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_PendingTaskCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_RunningTaskCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_RunningTaskCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_RunningTaskCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_RunningTaskCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ServiceCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ServiceCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ServiceCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_ServiceCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageReadBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageReadBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageReadBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageReadBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_count",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_max",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_min",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_sum",
"unit": "Bytes\/Second",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskSetCount_count",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskSetCount_max",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskSetCount_min",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_ContainerInsights_TaskSetCount_sum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_MemoryUtilization_count",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_MemoryUtilization_max",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_MemoryUtilization_min",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "aws_ECS_MemoryUtilization_sum",
"unit": "Percent",
"type": "Gauge",
"description": ""
}
],
"logs": [
{
"name": "Account ID",
"path": "resources.cloud.account.id",
"type": "string"
},
{
"name": "Log Group Name",
"path": "resources.aws.cloudwatch.log_group_name",
"type": "string"
},
{
"name": "Log Stream Name",
"path": "resources.aws.cloudwatch.log_stream_name",
"type": "string"
}
]
},
"telemetry_collection_strategy": {
"aws_metrics": {
"cloudwatch_metric_stream_filters": [
{
"Namespace": "AWS/ECS"
},
{
"Namespace": "ECS/ContainerInsights"
}
]
},
"aws_logs": {
"cloudwatch_logs_subscriptions": [
{
"log_group_name_prefix": "/ecs",
"filter_pattern": ""
},
{
"log_group_name_prefix": "/aws/ecs/containerinsights",
"filter_pattern": ""
}
]
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "AWS ECS Overview",
"description": "Overview of ECS",
"image": "file://assets/dashboards/overview.png",
"definition": "file://assets/dashboards/overview.json"
},
{
"id": "containerinsights",
"title": "ECS ContainerInsights",
"description": "Overview of ECS ContainerInsights",
"image": "file://assets/dashboards/containerinsights.png",
"definition": "file://assets/dashboards/containerinsights.json"
},
{
"id": "enhanced_containerinsights",
"title": "ECS Enhanced ContainerInsights",
"description": "Enhanced ECS ContainerInsights",
"image": "file://assets/dashboards/enhanced_containerinsights.png",
"definition": "file://assets/dashboards/enhanced_containerinsights.json"
}
]
}
}

View File

@@ -0,0 +1,3 @@
### Monitor Elastic Container Service with SigNoz
Collect ECS Logs and key Metrics and view them with an out of the box dashboard.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

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