Compare commits

..

4 Commits

Author SHA1 Message Date
Yunus M
38286e38dd fix: opening /ai-assistant opens 2 new conversations instead of one 2026-07-01 15:19:56 +05:30
Abhi kumar
13812fac62 fix(dashboard): pie panel collapses multi-column ClickHouse query to a single slice (#11919)
* fix(dashboard): pie panel collapses multi-column clickhouse scalar to one slice

A pie panel backed by a ClickHouse query with several aggregations
(e.g. `count() AS col1, sum() AS col2`) rendered a single slice labelled
with the query name and only the first value column's value; the other
value columns were silently dropped.

Root cause: the scalar response carries every value column in the scalar
table, but PiePanelWrapper read the legacy `data.result` time-series field
instead. For a scalar that field collapses to a single series that keeps
only the first value column, so the pie never saw the rest. This is the
pie counterpart of the table collapse fixed in #11794.

Fix: when the scalar table has more than one value column, build pie
slices from the scalar table under `newResult` (the same source the table
and value panels already use) — one slice per value column, group-by
columns become the label. Single-aggregation and grouped pies keep the
existing series path unchanged. Frontend-only, V1.

* fix: formatter datetime

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-07-01 05:15:47 +00:00
Vinicius Lourenço
df77b8d125 fix(settings): ensure scroll on tiny screens (#11916)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-30 18:45:47 +00:00
Swapnil Nakade
028ac27496 feat: adding cloud integration API changes for GCP (#11892)
* feat: adding cloud integration API changes for GCP

* chore: generating openapi specs

* fix: integration tests

* ci: fixing golang ci lint
2026-06-30 17:13:53 +00:00
42 changed files with 1145 additions and 142 deletions

View File

@@ -61,6 +61,13 @@ jobs:
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
echo 'VITE_ENVIRONMENT="production"' >> frontend/.env
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env

View File

@@ -67,6 +67,12 @@ jobs:
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
echo 'VITE_ENVIRONMENT="staging"' >> frontend/.env
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env

View File

@@ -27,6 +27,13 @@ jobs:
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> .env
echo 'VITE_ENVIRONMENT="production"' >> .env
echo 'VITE_VERSION="${{ github.ref_name }}"' >> .env

View File

@@ -177,9 +177,11 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return nil, err
}
azureCloudProviderModule := implcloudprovider.NewAzureCloudProvider(defStore)
gcpCloudProviderModule := implcloudprovider.NewGCPCloudProvider(defStore)
cloudProvidersMap := map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule{
cloudintegrationtypes.CloudProviderTypeAWS: awsCloudProviderModule,
cloudintegrationtypes.CloudProviderTypeAzure: azureCloudProviderModule,
cloudintegrationtypes.CloudProviderTypeGCP: gcpCloudProviderModule,
}
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), dashboardModule, global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)

View File

@@ -1024,6 +1024,8 @@ components:
$ref: '#/components/schemas/CloudintegrationtypesAWSAccountConfig'
azure:
$ref: '#/components/schemas/CloudintegrationtypesAzureAccountConfig'
gcp:
$ref: '#/components/schemas/CloudintegrationtypesGCPAccountConfig'
type: object
CloudintegrationtypesAgentReport:
nullable: true
@@ -1169,6 +1171,8 @@ components:
$ref: '#/components/schemas/CloudintegrationtypesAWSConnectionArtifact'
azure:
$ref: '#/components/schemas/CloudintegrationtypesAzureConnectionArtifact'
gcp:
$ref: '#/components/schemas/CloudintegrationtypesGCPConnectionArtifact'
type: object
CloudintegrationtypesCredentials:
properties:
@@ -1199,6 +1203,46 @@ components:
nullable: true
type: array
type: object
CloudintegrationtypesGCPAccountConfig:
properties:
deploymentProjectId:
type: string
deploymentRegion:
type: string
projectIds:
items:
type: string
type: array
required:
- deploymentProjectId
- deploymentRegion
- projectIds
type: object
CloudintegrationtypesGCPConnectionArtifact:
type: object
CloudintegrationtypesGCPIntegrationConfig:
type: object
CloudintegrationtypesGCPServiceConfig:
properties:
logs:
$ref: '#/components/schemas/CloudintegrationtypesGCPServiceLogsConfig'
metrics:
$ref: '#/components/schemas/CloudintegrationtypesGCPServiceMetricsConfig'
type: object
CloudintegrationtypesGCPServiceLogsConfig:
properties:
enabled:
type: boolean
required:
- enabled
type: object
CloudintegrationtypesGCPServiceMetricsConfig:
properties:
enabled:
type: boolean
required:
- enabled
type: object
CloudintegrationtypesGettableAccountWithConnectionArtifact:
properties:
connectionArtifact:
@@ -1331,6 +1375,8 @@ components:
$ref: '#/components/schemas/CloudintegrationtypesAWSPostableAccountConfig'
azure:
$ref: '#/components/schemas/CloudintegrationtypesAzureAccountConfig'
gcp:
$ref: '#/components/schemas/CloudintegrationtypesGCPAccountConfig'
type: object
CloudintegrationtypesPostableAgentCheckIn:
properties:
@@ -1355,6 +1401,8 @@ components:
$ref: '#/components/schemas/CloudintegrationtypesAWSIntegrationConfig'
azure:
$ref: '#/components/schemas/CloudintegrationtypesAzureIntegrationConfig'
gcp:
$ref: '#/components/schemas/CloudintegrationtypesGCPIntegrationConfig'
type: object
CloudintegrationtypesService:
properties:
@@ -1399,6 +1447,8 @@ components:
$ref: '#/components/schemas/CloudintegrationtypesAWSServiceConfig'
azure:
$ref: '#/components/schemas/CloudintegrationtypesAzureServiceConfig'
gcp:
$ref: '#/components/schemas/CloudintegrationtypesGCPServiceConfig'
type: object
CloudintegrationtypesServiceDashboard:
properties:
@@ -1441,6 +1491,7 @@ components:
- cosmosdb
- cassandradb
- redis
- cloudsql
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -1502,6 +1553,8 @@ components:
$ref: '#/components/schemas/CloudintegrationtypesAWSAccountConfig'
azure:
$ref: '#/components/schemas/CloudintegrationtypesUpdatableAzureAccountConfig'
gcp:
$ref: '#/components/schemas/CloudintegrationtypesUpdatableGCPAccountConfig'
type: object
CloudintegrationtypesUpdatableAzureAccountConfig:
properties:
@@ -1512,6 +1565,22 @@ components:
required:
- resourceGroups
type: object
CloudintegrationtypesUpdatableGCPAccountConfig:
properties:
deploymentProjectId:
type: string
deploymentRegion:
type: string
projectIds:
items:
type: string
nullable: true
type: array
required:
- deploymentProjectId
- deploymentRegion
- projectIds
type: object
CloudintegrationtypesUpdatableService:
properties:
config:

View File

@@ -0,0 +1,36 @@
package implcloudprovider
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
)
type gcpcloudprovider struct {
serviceDefinitions cloudintegrationtypes.ServiceDefinitionStore
}
func NewGCPCloudProvider(defStore cloudintegrationtypes.ServiceDefinitionStore) cloudintegration.CloudProviderModule {
return &gcpcloudprovider{
serviceDefinitions: defStore,
}
}
func (g *gcpcloudprovider) BuildIntegrationConfig(ctx context.Context, account *cloudintegrationtypes.Account, services []*cloudintegrationtypes.StorableCloudIntegrationService) (*cloudintegrationtypes.ProviderIntegrationConfig, error) {
// for manual flow we don't have any integration config to return, so returning empty config for now.
return &cloudintegrationtypes.ProviderIntegrationConfig{}, nil
}
func (g *gcpcloudprovider) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
// for manual flow we don't have any connection artifact to return, so returning empty artifact for now.
return &cloudintegrationtypes.ConnectionArtifact{}, nil
}
func (g *gcpcloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
return g.serviceDefinitions.Get(ctx, cloudintegrationtypes.CloudProviderTypeGCP, serviceID)
}
func (g *gcpcloudprovider) ListServiceDefinitions(ctx context.Context) ([]*cloudintegrationtypes.ServiceDefinition, error) {
return g.serviceDefinitions.List(ctx, cloudintegrationtypes.CloudProviderTypeGCP)
}

View File

@@ -1,38 +1,8 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
CI="1"
# API
VITE_BASE_PATH=""
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
VITE_WEBSOCKET_API_ENDPOINT=""
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
# Pylon
VITE_PYLON_ENABLED="false"
VITE_PYLON_APP_ID=""
VITE_PYLON_IDENTITY_SECRET=""
# Appcues
VITE_APPCUES_ENABLED="false"
VITE_APPCUES_APP_ID=""
# PostHog
VITE_POSTHOG_ENABLED="false"
VITE_POSTHOG_API_HOST=""
VITE_POSTHOG_KEY=""
VITE_POSTHOG_UI_HOST=""
# Sentry
VITE_SENTRY_ENABLED="false"
VITE_SENTRY_AUTH_TOKEN=""
VITE_SENTRY_ORG=""
VITE_SENTRY_PROJECT_ID=""
VITE_SENTRY_TUNNEL=""
VITE_SENTRY_DSN=""
# Docs
VITE_DOCS_BASE_URL="https://signoz.io"
# Build info
VITE_ENVIRONMENT="development"
VITE_VERSION=""
CI="1"

View File

@@ -111,10 +111,11 @@
<div id="root"></div>
<script>
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
var pylonSettings =
((window.signozBootData || {}).settings || {}).pylon || {};
var pylonEnabled = pylonSettings.enabled === true;
if (pylonSettings.appId && pylonEnabled) {
var pylonEnabled = pylonSettings.enabled !== false;
if (PYLON_APP_ID && pylonEnabled) {
(function () {
var e = window;
var t = document;
@@ -132,7 +133,7 @@
e.setAttribute('async', 'true');
e.setAttribute(
'src',
'https://widget.usepylon.com/widget/' + pylonSettings.appId,
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
);
var n = t.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
@@ -149,14 +150,15 @@
window.AppcuesSettings = { enableURLDetection: true };
</script>
<script>
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
var appcuesSettings =
((window.signozBootData || {}).settings || {}).appcues || {};
var appcuesEnabled = appcuesSettings.enabled === true;
if (appcuesSettings.appId && appcuesEnabled) {
var appcuesEnabled = appcuesSettings.enabled !== false;
if (APPCUES_APP_ID && appcuesEnabled) {
(function (d, t) {
var a = d.createElement(t);
a.async = 1;
a.src = '//fast.appcues.com/' + appcuesSettings.appId + '.js';
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
var s = d.getElementsByTagName(t)[0];
s.parentNode.insertBefore(a, s);
})(document, 'script');

View File

@@ -3,6 +3,7 @@
"project": ["src/**/*.ts", "src/**/*.tsx"],
"ignore": ["src/api/generated/**/*.ts", "src/typings/*.ts"],
"ignoreDependencies": [
"http-proxy-middleware",
"@typescript/native-preview"
]
}

View File

@@ -79,6 +79,7 @@
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
"http-proxy-middleware": "4.1.1",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",

View File

@@ -164,6 +164,9 @@ importers:
history:
specifier: 4.10.1
version: 4.10.1
http-proxy-middleware:
specifier: 4.1.1
version: 4.1.1
http-status-codes:
specifier: 2.3.0
version: 2.3.0
@@ -5458,6 +5461,10 @@ packages:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
http-proxy-middleware@4.1.1:
resolution: {integrity: sha512-KX5ZofGXLFXqFAkQoOWZ+rTtaLTut7m0gyL+QzJrdejtIZ+F4bPPDoe7reISg2+v0CAz5OfVwEJEhty7X+e57g==}
engines: {node: ^22.15.0 || ^24.0.0 || >=26.0.0}
http-status-codes@2.3.0:
resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==}
@@ -5465,6 +5472,9 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
httpxy@0.5.3:
resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==}
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@@ -14505,6 +14515,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
http-proxy-middleware@4.1.1:
dependencies:
debug: 4.3.4(supports-color@5.5.0)
httpxy: 0.5.3
is-glob: 4.0.3
is-plain-obj: 4.1.0
micromatch: 4.0.8
transitivePeerDependencies:
- supports-color
http-status-codes@2.3.0: {}
https-proxy-agent@5.0.1:
@@ -14514,6 +14534,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
httpxy@0.5.3: {}
human-signals@2.1.0: {}
human-signals@8.0.1: {}

View File

@@ -292,10 +292,10 @@ function App(): JSX.Element {
isChatSupportEnabled &&
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser) &&
window.signozBootData?.settings?.pylon?.enabled
(window.signozBootData?.settings?.pylon.enabled ?? true)
) {
const email = user.email || '';
const secret = window.signozBootData?.settings?.pylon?.identitySecret || '';
const secret = process.env.PYLON_IDENTITY_SECRET || '';
let emailHash = '';
if (email && secret) {
@@ -304,7 +304,7 @@ function App(): JSX.Element {
window.pylon = {
chat_settings: {
app_id: window.signozBootData?.settings?.pylon?.appId,
app_id: process.env.PYLON_APP_ID,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
@@ -335,23 +335,22 @@ function App(): JSX.Element {
useEffect(() => {
if (isCloudUser || isEnterpriseSelfHostedUser) {
if (
window.signozBootData?.settings?.posthog?.enabled &&
window.signozBootData?.settings?.posthog?.key
(window.signozBootData?.settings?.posthog.enabled ?? true) &&
process.env.POSTHOG_KEY
) {
posthog.init(window.signozBootData.settings.posthog.key, {
api_host: window.signozBootData.settings.posthog.apiHost,
ui_host: window.signozBootData.settings.posthog.uiHost,
posthog.init(process.env.POSTHOG_KEY, {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
});
}
if (
!isSentryInitialized &&
window.signozBootData?.settings?.sentry?.enabled
(window.signozBootData?.settings?.sentry.enabled ?? true)
) {
Sentry.init({
dsn: window.signozBootData.settings.sentry.dsn,
tunnel: window.signozBootData.settings.sentry.tunnel,
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
environment: process.env.ENVIRONMENT,
release: process.env.VERSION,
integrations: [

View File

@@ -2630,9 +2630,25 @@ export interface CloudintegrationtypesAzureAccountConfigDTO {
resourceGroups: string[];
}
export interface CloudintegrationtypesGCPAccountConfigDTO {
/**
* @type string
*/
deploymentProjectId: string;
/**
* @type string
*/
deploymentRegion: string;
/**
* @type array
*/
projectIds: string[];
}
export interface CloudintegrationtypesAccountConfigDTO {
aws?: CloudintegrationtypesAWSAccountConfigDTO;
azure?: CloudintegrationtypesAzureAccountConfigDTO;
gcp?: CloudintegrationtypesGCPAccountConfigDTO;
}
export interface CloudintegrationtypesAccountDTO {
@@ -2740,9 +2756,29 @@ export interface CloudintegrationtypesAzureServiceConfigDTO {
metrics: CloudintegrationtypesAzureServiceMetricsConfigDTO;
}
export interface CloudintegrationtypesGCPServiceLogsConfigDTO {
/**
* @type boolean
*/
enabled: boolean;
}
export interface CloudintegrationtypesGCPServiceMetricsConfigDTO {
/**
* @type boolean
*/
enabled: boolean;
}
export interface CloudintegrationtypesGCPServiceConfigDTO {
logs?: CloudintegrationtypesGCPServiceLogsConfigDTO;
metrics?: CloudintegrationtypesGCPServiceMetricsConfigDTO;
}
export interface CloudintegrationtypesServiceConfigDTO {
aws?: CloudintegrationtypesAWSServiceConfigDTO;
azure?: CloudintegrationtypesAzureServiceConfigDTO;
gcp?: CloudintegrationtypesGCPServiceConfigDTO;
}
export enum CloudintegrationtypesServiceIDDTO {
@@ -2773,6 +2809,7 @@ export enum CloudintegrationtypesServiceIDDTO {
cosmosdb = 'cosmosdb',
cassandradb = 'cassandradb',
redis = 'redis',
cloudsql = 'cloudsql',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -2837,9 +2874,14 @@ export interface CloudintegrationtypesCollectedMetricDTO {
unit?: string;
}
export interface CloudintegrationtypesGCPConnectionArtifactDTO {
[key: string]: unknown;
}
export interface CloudintegrationtypesConnectionArtifactDTO {
aws?: CloudintegrationtypesAWSConnectionArtifactDTO;
azure?: CloudintegrationtypesAzureConnectionArtifactDTO;
gcp?: CloudintegrationtypesGCPConnectionArtifactDTO;
}
export interface CloudintegrationtypesCredentialsDTO {
@@ -2872,6 +2914,10 @@ export interface CloudintegrationtypesDataCollectedDTO {
metrics?: CloudintegrationtypesCollectedMetricDTO[] | null;
}
export interface CloudintegrationtypesGCPIntegrationConfigDTO {
[key: string]: unknown;
}
export interface CloudintegrationtypesGettableAccountWithConnectionArtifactDTO {
connectionArtifact: CloudintegrationtypesConnectionArtifactDTO;
/**
@@ -2963,6 +3009,7 @@ export type CloudintegrationtypesIntegrationConfigDTO =
export interface CloudintegrationtypesProviderIntegrationConfigDTO {
aws?: CloudintegrationtypesAWSIntegrationConfigDTO;
azure?: CloudintegrationtypesAzureIntegrationConfigDTO;
gcp?: CloudintegrationtypesGCPIntegrationConfigDTO;
}
export interface CloudintegrationtypesGettableAgentCheckInDTO {
@@ -3025,6 +3072,7 @@ export interface CloudintegrationtypesGettableServicesMetadataDTO {
export interface CloudintegrationtypesPostableAccountConfigDTO {
aws?: CloudintegrationtypesAWSPostableAccountConfigDTO;
azure?: CloudintegrationtypesAzureAccountConfigDTO;
gcp?: CloudintegrationtypesGCPAccountConfigDTO;
}
export interface CloudintegrationtypesPostableAccountDTO {
@@ -3154,9 +3202,25 @@ export interface CloudintegrationtypesUpdatableAzureAccountConfigDTO {
resourceGroups: string[];
}
export interface CloudintegrationtypesUpdatableGCPAccountConfigDTO {
/**
* @type string
*/
deploymentProjectId: string;
/**
* @type string
*/
deploymentRegion: string;
/**
* @type array,null
*/
projectIds: string[] | null;
}
export interface CloudintegrationtypesUpdatableAccountConfigDTO {
aws?: CloudintegrationtypesAWSAccountConfigDTO;
azure?: CloudintegrationtypesUpdatableAzureAccountConfigDTO;
gcp?: CloudintegrationtypesUpdatableGCPAccountConfigDTO;
}
export interface CloudintegrationtypesUpdatableAccountDTO {

View File

@@ -3,7 +3,6 @@
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
border-radius: 4px;
}

View File

@@ -680,6 +680,13 @@ describe('formatUniversalUnit', () => {
});
describe('Datetime', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-01-01T00:00:00Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('formats datetime units', () => {
expect(formatUniversalUnit(900, UniversalYAxisUnit.DATETIME_FROM_NOW)).toBe(
'56 years ago',

View File

@@ -1,9 +1,14 @@
@use '../../styles/scrollbar' as *;
.members-settings-page {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4) var(--padding-2) var(--padding-6) var(--padding-4);
height: 100%;
overflow-y: auto;
@include custom-scrollbar;
}
.members-settings {

View File

@@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Group } from '@visx/group';
import { Pie } from '@visx/shape';
@@ -8,12 +8,10 @@ import { themeColors } from 'constants/theme';
import { getPieChartClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import { useIsDarkMode } from 'hooks/useDarkMode';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { isNaN } from 'lodash-es';
import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
import { preparePieChartData } from './preparePieChartData';
import { lightenColor, tooltipStyles } from './utils';
import './PiePanelWrapper.styles.scss';
@@ -44,37 +42,15 @@ function PiePanelWrapper({
detectBounds: true,
});
const panelData = queryResponse.data?.payload?.data?.result || [];
const isDarkMode = useIsDarkMode();
let pieChartData: {
label: string;
value: string;
color: string;
record: any;
}[] = [].concat(
...(panelData
.map((d) => {
const label = getLabelName(d.metric, d.queryName || '', d.legend || '');
return {
label,
value: d?.values?.[0]?.[1],
record: d,
color:
widget?.customLegendColors?.[label] ||
generateColor(
label,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
),
};
})
.filter((d) => d !== undefined) as never[]),
);
pieChartData = pieChartData.filter(
(arc) =>
arc.value && !isNaN(parseFloat(arc.value)) && parseFloat(arc.value) > 0,
const pieChartData = useMemo(
() =>
preparePieChartData(queryResponse.data?.payload, {
customLegendColors: widget?.customLegendColors,
colorMap: isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
}),
[queryResponse.data?.payload, widget?.customLegendColors, isDarkMode],
);
let size = 0;

View File

@@ -0,0 +1,185 @@
import { themeColors } from 'constants/theme';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
import { preparePieChartData } from '../preparePieChartData';
const options = { colorMap: themeColors.chartcolors };
/**
* Mirrors a query-range payload: the (possibly collapsed) time-series `result`
* plus the scalar table nested under `newResult` (as getQueryResults produces it).
*/
function makePayload(
result: QueryData[],
tables: QueryDataV3[],
): MetricRangePayloadProps {
return {
data: {
result,
resultType: 'scalar',
newResult: { data: { result: tables, resultType: 'scalar' } },
},
} as MetricRangePayloadProps;
}
function tableEntry(
columns: NonNullable<QueryDataV3['table']>['columns'],
rows: NonNullable<QueryDataV3['table']>['rows'],
overrides: Partial<QueryDataV3> = {},
): QueryDataV3 {
return {
queryName: 'A',
legend: '',
series: null,
list: null,
table: { columns, rows },
...overrides,
} as QueryDataV3;
}
describe('preparePieChartData', () => {
it('renders a slice per value column for a multi-column ClickHouse scalar', () => {
// SELECT count() AS col1, sum(value) AS col2 — the backend collapses the
// time-series result onto col1; the full data lives in the scalar table.
const payload = makePayload(
[
{
metric: {},
queryName: 'A',
legend: '',
values: [[0, '23399927']],
} as QueryData,
],
[
tableEntry(
[
{ name: 'col1', queryName: 'A', isValueColumn: true, id: 'col1' },
{ name: 'col2', queryName: 'A', isValueColumn: true, id: 'col2' },
],
[{ data: { col1: 23399927, col2: 588691297 } }],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices).toHaveLength(2);
expect(slices.map((s) => [s.label, s.value])).toStrictEqual([
['col1', '23399927'],
['col2', '588691297'],
]);
});
it('prefixes the group when multiple value columns are grouped', () => {
const payload = makePayload(
[],
[
tableEntry(
[
{ name: 'env', queryName: 'A', isValueColumn: false, id: 'env' },
{ name: 'col1', queryName: 'A', isValueColumn: true, id: 'col1' },
{ name: 'col2', queryName: 'A', isValueColumn: true, id: 'col2' },
],
[{ data: { env: 'prod', col1: 10, col2: 20 } }],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices.map((s) => s.label)).toStrictEqual([
'prod · col1',
'prod · col2',
]);
expect(slices[0].record.metric).toStrictEqual({ env: 'prod' });
});
it('drops non-positive and non-numeric values', () => {
const payload = makePayload(
[],
[
tableEntry(
[
{ name: 'col1', queryName: 'A', isValueColumn: true, id: 'col1' },
{ name: 'col2', queryName: 'A', isValueColumn: true, id: 'col2' },
{ name: 'col3', queryName: 'A', isValueColumn: true, id: 'col3' },
],
[{ data: { col1: 5, col2: 0, col3: 'n/a' } }],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices.map((s) => s.label)).toStrictEqual(['col1']);
});
it('keeps the series path for a single value column (grouped panel)', () => {
// One value column → the time-series result is authoritative (one slice per
// group), so existing behaviour is preserved.
const payload = makePayload(
[
{
metric: { 'service.name': 'adservice' },
queryName: 'A',
legend: 'adservice',
values: [[0, '100']],
} as QueryData,
{
metric: { 'service.name': 'cartservice' },
queryName: 'A',
legend: 'cartservice',
values: [[0, '200']],
} as QueryData,
],
[
tableEntry(
[
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
{ name: 'count', queryName: 'A', isValueColumn: true, id: 'A' },
],
[
{ data: { 'service.name': 'adservice', A: 100 } },
{ data: { 'service.name': 'cartservice', A: 200 } },
],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices.map((s) => [s.label, s.value])).toStrictEqual([
['adservice', '100'],
['cartservice', '200'],
]);
});
it('uses the legacy series result when there is no scalar table', () => {
const payload = makePayload(
[
{
metric: { 'service.name': 'adservice' },
queryName: 'A',
legend: '{{service.name}}',
values: [[1000, '42']],
} as QueryData,
],
[],
);
const slices = preparePieChartData(payload, options);
expect(slices).toHaveLength(1);
expect(slices[0].value).toBe('42');
});
it('returns no slices for an empty payload', () => {
expect(preparePieChartData(undefined, options)).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,144 @@
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { isNaN } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
export interface PieChartSlice {
label: string;
value: string;
color: string;
record: {
queryName: string;
legend?: string;
/** Group-by labels, used for drilldown; absent when the slice has no group. */
metric?: QueryData['metric'];
};
}
interface PreparePieChartDataOptions {
customLegendColors?: Record<string, string>;
colorMap: Record<string, string>;
}
const colorFor = (
label: string,
{ customLegendColors, colorMap }: PreparePieChartDataOptions,
): string => customLegendColors?.[label] || generateColor(label, colorMap);
const isPositive = (value: string): boolean =>
!!value && !isNaN(parseFloat(value)) && parseFloat(value) > 0;
/**
* Time-series result: one slice per series, value = first datapoint. This is the
* original pie behaviour — kept verbatim (same label/value/colour/record) so
* single-value and grouped panels are unaffected.
*/
function slicesFromSeries(
result: QueryData[],
options: PreparePieChartDataOptions,
): PieChartSlice[] {
return result
.filter((d) => d?.values?.[0]?.[1] !== undefined)
.map((d) => {
const label = getLabelName(d.metric, d.queryName || '', d.legend || '');
return {
label,
value: d.values[0][1],
color: colorFor(label, options),
record: d,
};
});
}
/**
* V5 scalar table: one slice per (row × value column). With more than one value
* column the column name keeps the slices distinct, so a ClickHouse query like
* `count() AS col1, sum() AS col2` renders a slice per column instead of
* collapsing onto the first; group-by columns become the slice label.
*/
function slicesFromTables(
tables: QueryDataV3[],
options: PreparePieChartDataOptions,
): PieChartSlice[] {
const slices: PieChartSlice[] = [];
tables.forEach((entry) => {
const { table } = entry;
if (!table?.columns?.length || !table?.rows?.length) {
return;
}
const valueColumns = table.columns.filter((column) => column.isValueColumn);
if (valueColumns.length === 0) {
return;
}
const labelColumns = table.columns.filter((column) => !column.isValueColumn);
const hasMultipleValueColumns = valueColumns.length > 1;
table.rows.forEach((row) => {
const groupLabel = labelColumns
.map((column) => row.data[column.id || column.name])
.filter((part) => part != null)
.map(String)
.join(', ');
// Drilldown filters by group-by labels; leave it undefined when there
// are none (e.g. a ClickHouse query) so no filterless menu is offered.
const metric = labelColumns.length
? labelColumns.reduce<Record<string, string>>((acc, column) => {
acc[column.name] = String(row.data[column.id || column.name]);
return acc;
}, {})
: undefined;
valueColumns.forEach((column) => {
let label: string;
if (hasMultipleValueColumns) {
label = groupLabel ? `${groupLabel} · ${column.name}` : column.name;
} else {
label = groupLabel || entry.legend || entry.queryName || '';
}
slices.push({
label,
value: String(row.data[column.id || column.name]),
color: colorFor(label, options),
record: { queryName: entry.queryName, legend: entry.legend, metric },
});
});
});
});
return slices;
}
/**
* Builds pie slices from a query-range payload, dropping non-positive/non-numeric
* values.
*
* A scalar response with several value columns (e.g. a ClickHouse
* `count() AS col1, sum() AS col2`) collapses to a single series in
* `data.result` — only the first value column survives. The full data is kept in
* the scalar table under `newResult`, so in that case slices are built from the
* table (one per value column). Otherwise the legacy time-series result is used,
* preserving existing behaviour for single-value and grouped panels.
*/
export function preparePieChartData(
payload: MetricRangePayloadProps | undefined,
options: PreparePieChartDataOptions,
): PieChartSlice[] {
const tables = (payload?.data?.newResult?.data?.result || []).filter(
(entry) => entry?.table?.rows?.length,
);
const hasMultipleValueColumns = tables.some(
(entry) =>
(entry.table?.columns || []).filter((column) => column.isValueColumn)
.length > 1,
);
const slices = hasMultipleValueColumns
? slicesFromTables(tables, options)
: slicesFromSeries(payload?.data?.result || [], options);
return slices.filter((slice) => isPositive(slice.value));
}

View File

@@ -1,7 +1,6 @@
.rolesListingTable {
margin-top: 12px;
border-radius: 4px;
overflow: hidden;
}
.scrollContainer {

View File

@@ -40,6 +40,7 @@
.rolesSettingsContent {
padding: 0 16px;
padding-bottom: 16px;
}
.rolesSettingsToolbar {

View File

@@ -26,8 +26,8 @@ export default function AIAssistantPage(): JSX.Element {
// Skip the mount-time Opened fire when the user expanded an already-open
// drawer/modal — that surface already emitted Opened with the right source.
// Router state (vs a module flag) survives StrictMode double-mount and
// aborted navigations.
// Router state (vs a module flag) survives page remounts and aborted
// navigations.
const fromInApp = location.state?.fromInApp === true;
useEffect(() => {
if (fromInApp) {
@@ -52,18 +52,34 @@ export default function AIAssistantPage(): JSX.Element {
(s) => s.startNewConversation,
);
// Keep a ref so the effect can read latest conversations without re-firing
// when startNewConversation mutates the store mid-effect.
// Keep refs so the effect can read the latest store state without re-firing
// when it mutates the store mid-effect (it only depends on the URL param).
const conversationsRef = useRef(conversations);
conversationsRef.current = conversations;
const activeConversationIdRef = useRef(activeConversationId);
activeConversationIdRef.current = activeConversationId;
useEffect(() => {
if (conversationsRef.current[conversationId]) {
// URL points at a known conversation → just activate it.
if (conversationId && conversationsRef.current[conversationId]) {
setActiveConversation(conversationId);
} else {
const newId = startNewConversation();
history.replace(ROUTES.AI_ASSISTANT.replace(':conversationId', newId));
return;
}
// The URL has no usable conversation id (bare `/ai-assistant`, or a stale
// param). Prefer resuming the active conversation — including the
// rehydrating placeholder for the persisted thread — over minting a new
// one. This is what stops a throwaway blank chat from flashing as a
// second thread during load, and stops a duplicate when the page
// remounts during startup route churn (the active id is already set, so
// we resume instead of create). Starting fresh is the last resort, only
// when there is genuinely nothing to resume.
const activeId = activeConversationIdRef.current;
const resumeId =
activeId && conversationsRef.current[activeId]
? activeId
: startNewConversation();
history.replace(ROUTES.AI_ASSISTANT.replace(':conversationId', resumeId));
// Only re-run when the URL param changes, not when conversations mutates.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [conversationId]);

View File

@@ -0,0 +1,181 @@
import { MemoryRouter, Route } from 'react-router-dom';
// eslint-disable-next-line no-restricted-imports
import { render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('container/AIAssistant/ConversationView', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="conversation-view" />,
}));
jest.mock('container/AIAssistant/components/ConversationsList', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="conversations-list" />,
}));
jest.mock('components/Noz/Noz', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="noz" />,
}));
jest.mock('container/AIAssistant/hooks/useAIAssistantAnalyticsContext', () => ({
normalizePage: (page: string): string => page,
useAIAssistantAnalyticsContext: (): unknown => ({ mode: 'page' }),
}));
// eslint-disable-next-line import/first
import AIAssistantPage from '../AIAssistantPage';
function renderAt(entry: string): { unmount: () => void } {
return render(
<MemoryRouter initialEntries={[entry]}>
<Route
exact
path={[ROUTES.AI_ASSISTANT_BASE, ROUTES.AI_ASSISTANT]}
component={AIAssistantPage}
/>
</MemoryRouter>,
);
}
function renderAtBase(): { unmount: () => void } {
return renderAt(ROUTES.AI_ASSISTANT_BASE);
}
function conversationCount(): number {
return Object.keys(useAIAssistantStore.getState().conversations).length;
}
function conversationIds(): string[] {
return Object.keys(useAIAssistantStore.getState().conversations);
}
function activeId(): string | null {
return useAIAssistantStore.getState().activeConversationId;
}
describe('AIAssistantPage', () => {
beforeEach(() => {
useAIAssistantStore.setState({
conversations: {},
streams: {},
activeConversationId: null,
});
});
it('opens exactly one conversation when navigating to /ai-assistant', () => {
const { unmount } = renderAtBase();
expect(conversationCount()).toBe(1);
unmount();
});
it('does not stack a second conversation when the page remounts at the bare URL (route churn)', () => {
// First mount at `/ai-assistant` creates one blank conversation and
// redirects to `/ai-assistant/:id`.
const { unmount } = renderAtBase();
expect(conversationCount()).toBe(1);
const firstId = conversationIds()[0];
// Startup route-list churn unmounts and remounts the page while the URL
// is momentarily back at the bare `/ai-assistant`. This previously
// created a second blank conversation — now it reuses the first.
unmount();
const { unmount: unmount2 } = renderAtBase();
expect(conversationCount()).toBe(1);
// The surviving conversation is the original one, resumed — not a fresh mint.
expect(conversationIds()).toStrictEqual([firstId]);
expect(activeId()).toBe(firstId);
unmount2();
});
it('activates the conversation named in the URL without creating a new one', () => {
useAIAssistantStore.setState({
conversations: {
existing: {
id: 'existing',
messages: [],
createdAt: 1,
updatedAt: 1,
},
},
streams: {},
activeConversationId: null,
});
const { unmount } = renderAt(
ROUTES.AI_ASSISTANT.replace(':conversationId', 'existing'),
);
expect(conversationCount()).toBe(1);
expect(activeId()).toBe('existing');
unmount();
});
it('resumes the active conversation on /ai-assistant/new instead of minting a new one', () => {
// The sidenav only routes to `/ai-assistant/new` as a fallback, but if an
// active conversation exists the page must resume it rather than spawn a
// throwaway blank thread for the unknown "new" param.
useAIAssistantStore.setState({
conversations: {
active: {
id: 'active',
messages: [],
createdAt: 1,
updatedAt: 1,
},
},
streams: {},
activeConversationId: 'active',
});
const { unmount } = renderAt(
ROUTES.AI_ASSISTANT.replace(':conversationId', 'new'),
);
expect(conversationCount()).toBe(1);
expect(conversationIds()).toStrictEqual(['active']);
expect(activeId()).toBe('active');
unmount();
});
it('resumes the persisted (hydrating) conversation during load instead of creating a second', () => {
// Simulates `onRehydrateStorage` priming the persisted active
// conversation as a hydrating placeholder before `fetchThreads` resolves.
useAIAssistantStore.setState({
conversations: {
persisted: {
id: 'persisted',
messages: [],
createdAt: 1,
updatedAt: 1,
isHydrating: true,
},
},
streams: {},
activeConversationId: 'persisted',
});
const { unmount } = renderAtBase();
// Opening the bare URL must resume the persisted conversation, not mint a
// throwaway blank alongside it (which flashed as a 2nd thread during load).
expect(conversationCount()).toBe(1);
expect(
Object.keys(useAIAssistantStore.getState().conversations),
).toStrictEqual(['persisted']);
unmount();
});
});

View File

@@ -0,0 +1,14 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
import { legacyCreateProxyMiddleware } from 'http-proxy-middleware';
export default function (app) {
app.use(
'/tunnel',
legacyCreateProxyMiddleware({
target: `${process.env.TUNNEL_DOMAIN}/tunnel`,
changeOrigin: true,
}),
);
}

View File

@@ -11,24 +11,18 @@ declare module '*.md?raw' {
}
interface ImportMetaEnv {
readonly VITE_BASE_PATH: string;
readonly VITE_FRONTEND_API_ENDPOINT: string;
readonly VITE_WEBSOCKET_API_ENDPOINT: string;
readonly VITE_PYLON_ENABLED: string;
readonly VITE_PYLON_APP_ID: string;
readonly VITE_PYLON_IDENTITY_SECRET: string;
readonly VITE_APPCUES_ENABLED: string;
readonly VITE_APPCUES_APP_ID: string;
readonly VITE_POSTHOG_ENABLED: string;
readonly VITE_POSTHOG_API_HOST: string;
readonly VITE_POSTHOG_KEY: string;
readonly VITE_POSTHOG_UI_HOST: string;
readonly VITE_SENTRY_AUTH_TOKEN: string;
readonly VITE_SENTRY_ORG: string;
readonly VITE_SENTRY_PROJECT_ID: string;
readonly VITE_SENTRY_ENABLED: string;
readonly VITE_SENTRY_TUNNEL: string;
readonly VITE_SENTRY_DSN: string;
readonly VITE_TUNNEL_URL: string;
readonly VITE_TUNNEL_DOMAIN: string;
readonly VITE_DOCS_BASE_URL: string;
readonly VITE_ENVIRONMENT: string;
readonly VITE_VERSION: string;

View File

@@ -6,15 +6,9 @@ import type { Plugin, TransformResult, UserConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import vitePluginChecker from 'vite-plugin-checker';
import viteCompression from 'vite-plugin-compression';
import { createHtmlPlugin } from 'vite-plugin-html';
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
import tsconfigPaths from 'vite-tsconfig-paths';
import type {
Appcues,
Posthog,
Pylon,
Sentry,
WebSettings,
} from 'types/generated/webSettings';
// In dev the Go backend is not involved, so replace the [[.BaseHref]] placeholder
// with the configured base path so relative assets resolve correctly from the Vite dev server.
@@ -29,33 +23,17 @@ function devBasePathPlugin(basePath: string): Plugin {
};
}
function devBootDataPlugin(env: ImportMetaEnv): Plugin {
function devBootDataPlugin(env: Record<string, string>): Plugin {
return {
name: 'dev-boot-data',
apply: 'serve',
transformIndexHtml(html): string {
const settings = {
posthog: {
enabled: env.VITE_POSTHOG_ENABLED === 'true',
apiHost: env.VITE_POSTHOG_API_HOST || '',
key: env.VITE_POSTHOG_KEY || '',
uiHost: env.VITE_POSTHOG_UI_HOST || '',
} satisfies Required<Posthog>,
appcues: {
enabled: env.VITE_APPCUES_ENABLED === 'true',
appId: env.VITE_APPCUES_APP_ID || '',
} satisfies Required<Appcues>,
sentry: {
enabled: env.VITE_SENTRY_ENABLED === 'true',
dsn: env.VITE_SENTRY_DSN || '',
tunnel: env.VITE_SENTRY_TUNNEL || '',
} satisfies Required<Sentry>,
pylon: {
enabled: env.VITE_PYLON_ENABLED === 'true',
appId: env.VITE_PYLON_APP_ID || '',
identitySecret: env.VITE_PYLON_IDENTITY_SECRET || '',
} satisfies Required<Pylon>,
} satisfies Required<WebSettings>;
posthog: { enabled: env.VITE_POSTHOG_ENABLED !== 'false' },
appcues: { enabled: env.VITE_APPCUES_ENABLED !== 'false' },
sentry: { enabled: env.VITE_SENTRY_ENABLED !== 'false' },
pylon: { enabled: env.VITE_PYLON_ENABLED !== 'false' },
};
return html.replaceAll('[[.Settings]]', JSON.stringify(settings));
},
};
@@ -77,7 +55,7 @@ function rawMarkdownPlugin(): Plugin {
}
export default defineConfig(({ mode }): UserConfig => {
const env = loadEnv(mode, process.cwd(), '') as ImportMetaEnv;
const env = loadEnv(mode, process.cwd(), '');
// Base path for serving the app (e.g., '/signoz/'). Defaults to '/'.
const basePath = env.VITE_BASE_PATH || '/';
@@ -87,6 +65,14 @@ export default defineConfig(({ mode }): UserConfig => {
devBasePathPlugin(basePath),
devBootDataPlugin(env),
react(),
createHtmlPlugin({
inject: {
data: {
PYLON_APP_ID: env.VITE_PYLON_APP_ID || '',
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID || '',
},
},
}),
vitePluginChecker({
typescript: true,
// this doubles the build tim
@@ -171,6 +157,17 @@ export default defineConfig(({ mode }): UserConfig => {
'process.env.WEBSOCKET_API_ENDPOINT': JSON.stringify(
env.VITE_WEBSOCKET_API_ENDPOINT,
),
'process.env.PYLON_APP_ID': JSON.stringify(env.VITE_PYLON_APP_ID),
'process.env.PYLON_IDENTITY_SECRET': JSON.stringify(
env.VITE_PYLON_IDENTITY_SECRET,
),
'process.env.APPCUES_APP_ID': JSON.stringify(env.VITE_APPCUES_APP_ID),
'process.env.POSTHOG_KEY': JSON.stringify(env.VITE_POSTHOG_KEY),
'process.env.SENTRY_ORG': JSON.stringify(env.VITE_SENTRY_ORG),
'process.env.SENTRY_PROJECT_ID': JSON.stringify(env.VITE_SENTRY_PROJECT_ID),
'process.env.SENTRY_DSN': JSON.stringify(env.VITE_SENTRY_DSN),
'process.env.TUNNEL_URL': JSON.stringify(env.VITE_TUNNEL_URL),
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
'process.env.ENVIRONMENT': JSON.stringify(env.VITE_ENVIRONMENT),
'process.env.VERSION': JSON.stringify(env.VITE_VERSION),

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#aecbfa;}.cls-1,.cls-2,.cls-3{fill-rule:evenodd;}.cls-2{fill:#669df6;}.cls-3{fill:#4285f4;}</style></defs><title>Icon_24px_SQL_Color</title><g data-name="Product Icons"><g ><polygon class="cls-1" points="4.67 10.44 4.67 13.45 12 17.35 12 14.34 4.67 10.44"/><polygon class="cls-1" points="4.67 15.09 4.67 18.1 12 22 12 18.99 4.67 15.09"/><polygon class="cls-2" points="12 17.35 19.33 13.45 19.33 10.44 12 14.34 12 17.35"/><polygon class="cls-2" points="12 22 19.33 18.1 19.33 15.09 12 18.99 12 22"/><polygon class="cls-3" points="19.33 8.91 19.33 5.9 12 2 12 5.01 19.33 8.91"/><polygon class="cls-2" points="12 2 4.67 5.9 4.67 8.91 12 5.01 12 2"/><polygon class="cls-1" points="4.67 5.87 4.67 8.89 12 12.79 12 9.77 4.67 5.87"/><polygon class="cls-2" points="12 12.79 19.33 8.89 19.33 5.87 12 9.77 12 12.79"/></g></g></svg>

After

Width:  |  Height:  |  Size: 933 B

View File

@@ -0,0 +1,27 @@
{
"id": "cloudsql",
"title": "GCP Cloud SQL",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": true
},
"dataCollected": {
"metrics": [],
"logs": []
},
"telemetryCollectionStrategy": {
"gcp": {}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "GCP Cloud SQL Overview",
"description": "Overview of GCP Cloud SQL metrics",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

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

View File

@@ -481,6 +481,7 @@ func (handler *handler) UpdateService(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
// TODO: Rename AgentCheckIn to just CheckIn.
func (handler *handler) AgentCheckIn(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -31,11 +31,13 @@ type AgentReport struct {
type AccountConfig struct {
AWS *AWSAccountConfig `json:"aws,omitempty" required:"false" nullable:"false"`
Azure *AzureAccountConfig `json:"azure,omitempty" required:"false" nullable:"false"`
GCP *GCPAccountConfig `json:"gcp,omitempty" required:"false" nullable:"false"`
}
type UpdatableAccountConfig struct {
AWS *UpdatableAWSAccountConfig `json:"aws,omitempty" required:"false" nullable:"false"`
Azure *UpdatableAzureAccountConfig `json:"azure,omitempty" required:"false" nullable:"false"`
GCP *UpdatableGCPAccountConfig `json:"gcp,omitempty" required:"false" nullable:"false"`
}
type PostableAccount struct {
@@ -48,6 +50,7 @@ type PostableAccountConfig struct {
AgentVersion string
AWS *AWSPostableAccountConfig `json:"aws,omitempty" required:"false" nullable:"false"`
Azure *AzurePostableAccountConfig `json:"azure,omitempty" required:"false" nullable:"false"`
GCP *GCPPostableAccountConfig `json:"gcp,omitempty" required:"false" nullable:"false"`
}
type Credentials struct {
@@ -66,6 +69,7 @@ type ConnectionArtifact struct {
// required till new providers are added
AWS *AWSConnectionArtifact `json:"aws,omitempty" required:"false" nullable:"false"`
Azure *AzureConnectionArtifact `json:"azure,omitempty" required:"false" nullable:"false"`
GCP *GCPConnectionArtifact `json:"gcp,omitempty" required:"false" nullable:"false"`
}
type GetConnectionArtifactRequest = PostableAccount
@@ -211,6 +215,30 @@ func NewAccountConfigFromPostable(provider CloudProviderType, config *PostableAc
}
return &AccountConfig{Azure: &AzureAccountConfig{DeploymentRegion: config.Azure.DeploymentRegion, ResourceGroups: config.Azure.ResourceGroups}}, nil
case CloudProviderTypeGCP:
if config.GCP == nil {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "GCP config can not be nil for GCP provider")
}
if config.GCP.DeploymentProjectID == "" {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "deployment project ID is required for GCP provider")
}
if err := validateGCPRegion(config.GCP.DeploymentRegion); err != nil {
return nil, err
}
if len(config.GCP.ProjectIDs) == 0 {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "at least one project id is required for GCP provider")
}
return &AccountConfig{
GCP: &GCPAccountConfig{
DeploymentProjectID: config.GCP.DeploymentProjectID,
ProjectIDs: config.GCP.ProjectIDs,
DeploymentRegion: config.GCP.DeploymentRegion,
},
}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
@@ -244,6 +272,30 @@ func NewAccountConfigFromUpdatable(provider CloudProviderType, config *Updatable
}
return &AccountConfig{Azure: &AzureAccountConfig{ResourceGroups: config.Config.Azure.ResourceGroups}}, nil
case CloudProviderTypeGCP:
if config.Config.GCP == nil {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "GCP config can not be nil for GCP provider")
}
if err := validateGCPRegion(config.Config.GCP.DeploymentRegion); err != nil {
return nil, err
}
if len(config.Config.GCP.ProjectIDs) == 0 {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "at least one project id is required for GCP provider")
}
if config.Config.GCP.DeploymentProjectID == "" {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "deployment project ID is required for GCP provider")
}
return &AccountConfig{
GCP: &GCPAccountConfig{
DeploymentProjectID: config.Config.GCP.DeploymentProjectID,
ProjectIDs: config.Config.GCP.ProjectIDs,
DeploymentRegion: config.Config.GCP.DeploymentRegion,
},
}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
@@ -332,15 +384,16 @@ func (config *PostableAccountConfig) SetAgentVersion(agentVersion string) {
// thats why not naming it MarshalJSON(), as it will interfere with default JSON marshalling of AccountConfig struct.
// NOTE: this entertains first non-null provider's config.
func (config *AccountConfig) ToJSON() ([]byte, error) {
if config.AWS != nil {
switch {
case config.AWS != nil:
return json.Marshal(config.AWS)
}
if config.Azure != nil {
case config.Azure != nil:
return json.Marshal(config.Azure)
case config.GCP != nil:
return json.Marshal(config.GCP)
default:
return nil, errors.NewInternalf(errors.CodeInternal, "no provider account config found")
}
return nil, errors.NewInternalf(errors.CodeInternal, "no provider account config found")
}
func NewIngestionKeyName(provider CloudProviderType) string {

View File

@@ -50,6 +50,7 @@ type IntegrationConfig struct {
type ProviderIntegrationConfig struct {
AWS *AWSIntegrationConfig `json:"aws,omitempty" required:"false" nullable:"false"`
Azure *AzureIntegrationConfig `json:"azure,omitempty" required:"false" nullable:"false"`
GCP *GCPIntegrationConfig `json:"gcp,omitempty" required:"false" nullable:"false"`
}
// NewGettableAgentCheckIn constructs a backward-compatible response from an AgentCheckInResponse.

View File

@@ -63,6 +63,7 @@ type StorableCloudIntegrationService struct {
type StorableServiceConfig struct {
AWS *StorableAWSServiceConfig
Azure *StorableAzureServiceConfig
GCP *StorableGCPServiceConfig
}
type StorableAWSServiceConfig struct {
@@ -92,6 +93,15 @@ type StorableAzureMetricsServiceConfig struct {
Enabled bool `json:"enabled"`
}
type StorableGCPServiceConfig struct {
Logs *StorableGCPServiceLogsConfig `json:"logs,omitempty"`
Metrics *StorableGCPServiceMetricsConfig `json:"metrics,omitempty"`
}
type StorableGCPServiceLogsConfig = GCPServiceLogsConfig
type StorableGCPServiceMetricsConfig = GCPServiceMetricsConfig
// Scan scans value from DB.
func (r *StorableAgentReport) Scan(src any) error {
var data []byte
@@ -225,6 +235,30 @@ func newStorableServiceConfig(provider CloudProviderType, serviceID ServiceID, s
}
return &StorableServiceConfig{Azure: storableAzureServiceConfig}, nil
case CloudProviderTypeGCP:
storableGCPServiceConfig := new(StorableGCPServiceConfig)
if supportedSignals.Logs {
if serviceConfig.GCP.Logs == nil {
return nil, errors.NewInvalidInputf(ErrCodeCloudIntegrationInvalidConfig, "logs config is required for GCP service: %s", serviceID.StringValue())
}
storableGCPServiceConfig.Logs = &StorableGCPServiceLogsConfig{
Enabled: serviceConfig.GCP.Logs.Enabled,
}
}
if supportedSignals.Metrics {
if serviceConfig.GCP.Metrics == nil {
return nil, errors.NewInvalidInputf(ErrCodeCloudIntegrationInvalidConfig, "metrics config is required for GCP service: %s", serviceID.StringValue())
}
storableGCPServiceConfig.Metrics = &StorableGCPServiceMetricsConfig{
Enabled: serviceConfig.GCP.Metrics.Enabled,
}
}
return &StorableServiceConfig{GCP: storableGCPServiceConfig}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
@@ -246,6 +280,13 @@ func newStorableServiceConfigFromJSON(provider CloudProviderType, jsonStr string
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't parse Azure service config JSON")
}
return &StorableServiceConfig{Azure: azureConfig}, nil
case CloudProviderTypeGCP:
gcpConfig := new(StorableGCPServiceConfig)
err := json.Unmarshal([]byte(jsonStr), gcpConfig)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't parse GCP service config JSON")
}
return &StorableServiceConfig{GCP: gcpConfig}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
@@ -266,6 +307,13 @@ func (config *StorableServiceConfig) toJSON(provider CloudProviderType) ([]byte,
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't serialize Azure service config to JSON")
}
return jsonBytes, nil
case CloudProviderTypeGCP:
jsonBytes, err := json.Marshal(config.GCP)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't serialize GCP service config to JSON")
}
return jsonBytes, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())

View File

@@ -11,6 +11,7 @@ var (
// cloud providers.
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
CloudProviderTypeGCP = CloudProviderType{valuer.NewString("gcp")}
ErrCodeCloudProviderInvalidInput = errors.MustNewCode("cloud_integration_invalid_cloud_provider")
)
@@ -21,6 +22,8 @@ func NewCloudProvider(provider string) (CloudProviderType, error) {
return CloudProviderTypeAWS, nil
case CloudProviderTypeAzure.StringValue():
return CloudProviderTypeAzure, nil
case CloudProviderTypeGCP.StringValue():
return CloudProviderTypeGCP, nil
default:
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
}

View File

@@ -0,0 +1,40 @@
package cloudintegrationtypes
type GCPAccountConfig struct {
// Project ID where central pub/sub for logs exist
DeploymentProjectID string `json:"deploymentProjectId" required:"true"`
// Project ID where otel collector will be deployed
DeploymentRegion string `json:"deploymentRegion" required:"true"`
// List of project IDs to monitor
ProjectIDs []string `json:"projectIds" required:"true" nullable:"false"`
}
type GCPPostableAccountConfig = GCPAccountConfig
type UpdatableGCPAccountConfig struct {
// Project ID where central pub/sub for logs exist
DeploymentProjectID string `json:"deploymentProjectId" required:"true"`
// Compute service region where otel collector will be deployed
DeploymentRegion string `json:"deploymentRegion" required:"true"`
// List of project IDs to monitor
ProjectIDs []string `json:"projectIds" required:"true"`
}
type GCPConnectionArtifact struct{}
type GCPIntegrationConfig struct{}
type GCPTelemetryCollectionStrategy struct{}
type GCPServiceConfig struct {
Logs *GCPServiceLogsConfig `json:"logs,omitempty" required:"false"`
Metrics *GCPServiceMetricsConfig `json:"metrics,omitempty" required:"false"`
}
type GCPServiceLogsConfig struct {
Enabled bool `json:"enabled" required:"true"`
}
type GCPServiceMetricsConfig struct {
Enabled bool `json:"enabled" required:"true"`
}

View File

@@ -102,6 +102,51 @@ var (
AzureRegionWestUS = CloudProviderRegion{valuer.NewString("westus")} // West US.
AzureRegionWestUS2 = CloudProviderRegion{valuer.NewString("westus2")} // West US 2.
AzureRegionWestUS3 = CloudProviderRegion{valuer.NewString("westus3")} // West US 3.
// GCP regions.
GCPRegionAfricaSouth1 = CloudProviderRegion{valuer.NewString("africa-south1")} // Johannesburg, South Africa. Africa.
GCPRegionAsiaEast1 = CloudProviderRegion{valuer.NewString("asia-east1")} // Changhua County, Taiwan. APAC.
GCPRegionAsiaEast2 = CloudProviderRegion{valuer.NewString("asia-east2")} // Hong Kong. APAC.
GCPRegionAsiaNortheast1 = CloudProviderRegion{valuer.NewString("asia-northeast1")} // Tokyo, Japan. APAC.
GCPRegionAsiaNortheast2 = CloudProviderRegion{valuer.NewString("asia-northeast2")} // Osaka, Japan. APAC.
GCPRegionAsiaNortheast3 = CloudProviderRegion{valuer.NewString("asia-northeast3")} // Seoul, South Korea. APAC.
GCPRegionAsiaSouth1 = CloudProviderRegion{valuer.NewString("asia-south1")} // Mumbai, India. APAC.
GCPRegionAsiaSouth2 = CloudProviderRegion{valuer.NewString("asia-south2")} // Delhi, India. APAC.
GCPRegionAsiaSoutheast1 = CloudProviderRegion{valuer.NewString("asia-southeast1")} // Jurong West, Singapore. APAC.
GCPRegionAsiaSoutheast2 = CloudProviderRegion{valuer.NewString("asia-southeast2")} // Jakarta, Indonesia. APAC.
GCPRegionAsiaSoutheast3 = CloudProviderRegion{valuer.NewString("asia-southeast3")} // Bangkok, Thailand. APAC.
GCPRegionAustraliaSoutheast1 = CloudProviderRegion{valuer.NewString("australia-southeast1")} // Sydney, Australia. APAC.
GCPRegionAustraliaSoutheast2 = CloudProviderRegion{valuer.NewString("australia-southeast2")} // Melbourne, Australia. APAC.
GCPRegionEuropeCentral2 = CloudProviderRegion{valuer.NewString("europe-central2")} // Warsaw, Poland. Europe.
GCPRegionEuropeNorth1 = CloudProviderRegion{valuer.NewString("europe-north1")} // Hamina, Finland. Europe.
GCPRegionEuropeNorth2 = CloudProviderRegion{valuer.NewString("europe-north2")} // Stockholm, Sweden. Europe.
GCPRegionEuropeSouthwest1 = CloudProviderRegion{valuer.NewString("europe-southwest1")} // Madrid, Spain. Europe.
GCPRegionEuropeWest1 = CloudProviderRegion{valuer.NewString("europe-west1")} // St. Ghislain, Belgium. Europe.
GCPRegionEuropeWest2 = CloudProviderRegion{valuer.NewString("europe-west2")} // London, England. Europe.
GCPRegionEuropeWest3 = CloudProviderRegion{valuer.NewString("europe-west3")} // Frankfurt, Germany. Europe.
GCPRegionEuropeWest4 = CloudProviderRegion{valuer.NewString("europe-west4")} // Eemshaven, Netherlands. Europe.
GCPRegionEuropeWest6 = CloudProviderRegion{valuer.NewString("europe-west6")} // Zurich, Switzerland. Europe.
GCPRegionEuropeWest8 = CloudProviderRegion{valuer.NewString("europe-west8")} // Milan, Italy. Europe.
GCPRegionEuropeWest9 = CloudProviderRegion{valuer.NewString("europe-west9")} // Paris, France. Europe.
GCPRegionEuropeWest10 = CloudProviderRegion{valuer.NewString("europe-west10")} // Berlin, Germany. Europe.
GCPRegionEuropeWest12 = CloudProviderRegion{valuer.NewString("europe-west12")} // Turin, Italy. Europe.
GCPRegionMECentral1 = CloudProviderRegion{valuer.NewString("me-central1")} // Doha, Qatar. Middle East.
GCPRegionMECentral2 = CloudProviderRegion{valuer.NewString("me-central2")} // Dammam, Saudi Arabia. Middle East.
GCPRegionMEWest1 = CloudProviderRegion{valuer.NewString("me-west1")} // Tel Aviv, Israel. Middle East.
GCPRegionNorthamericaNortheast1 = CloudProviderRegion{valuer.NewString("northamerica-northeast1")} // Montréal, Québec, Canada. North America.
GCPRegionNorthamericaNortheast2 = CloudProviderRegion{valuer.NewString("northamerica-northeast2")} // Toronto, Ontario, Canada. North America.
GCPRegionNorthamericaSouth1 = CloudProviderRegion{valuer.NewString("northamerica-south1")} // Querétaro, Mexico. North America.
GCPRegionSouthamericaEast1 = CloudProviderRegion{valuer.NewString("southamerica-east1")} // Osasco, São Paulo, Brazil. South America.
GCPRegionSouthamericaWest1 = CloudProviderRegion{valuer.NewString("southamerica-west1")} // Santiago, Chile. South America.
GCPRegionUSCentral1 = CloudProviderRegion{valuer.NewString("us-central1")} // Council Bluffs, Iowa. North America.
GCPRegionUSEast1 = CloudProviderRegion{valuer.NewString("us-east1")} // Moncks Corner, South Carolina. North America.
GCPRegionUSEast4 = CloudProviderRegion{valuer.NewString("us-east4")} // Ashburn, Virginia. North America.
GCPRegionUSEast5 = CloudProviderRegion{valuer.NewString("us-east5")} // Columbus, Ohio. North America.
GCPRegionUSSouth1 = CloudProviderRegion{valuer.NewString("us-south1")} // Dallas, Texas. North America.
GCPRegionUSWest1 = CloudProviderRegion{valuer.NewString("us-west1")} // The Dalles, Oregon. North America.
GCPRegionUSWest2 = CloudProviderRegion{valuer.NewString("us-west2")} // Los Angeles, California. North America.
GCPRegionUSWest3 = CloudProviderRegion{valuer.NewString("us-west3")} // Salt Lake City, Utah. North America.
GCPRegionUSWest4 = CloudProviderRegion{valuer.NewString("us-west4")} // Las Vegas, Nevada. North America.
)
func Enum() []any {
@@ -127,6 +172,18 @@ func Enum() []any {
AzureRegionSwedenCentral, AzureRegionSwitzerlandNorth, AzureRegionSwitzerlandWest,
AzureRegionUAECentral, AzureRegionUAENorth, AzureRegionUKSouth, AzureRegionUKWest,
AzureRegionWestCentralUS, AzureRegionWestEurope, AzureRegionWestIndia, AzureRegionWestUS, AzureRegionWestUS2, AzureRegionWestUS3,
// GCP regions.
GCPRegionAfricaSouth1, GCPRegionAsiaEast1, GCPRegionAsiaEast2, GCPRegionAsiaNortheast1, GCPRegionAsiaNortheast2, GCPRegionAsiaNortheast3,
GCPRegionAsiaSouth1, GCPRegionAsiaSouth2, GCPRegionAsiaSoutheast1, GCPRegionAsiaSoutheast2, GCPRegionAsiaSoutheast3,
GCPRegionAustraliaSoutheast1, GCPRegionAustraliaSoutheast2,
GCPRegionEuropeCentral2, GCPRegionEuropeNorth1, GCPRegionEuropeNorth2, GCPRegionEuropeSouthwest1,
GCPRegionEuropeWest1, GCPRegionEuropeWest2, GCPRegionEuropeWest3, GCPRegionEuropeWest4, GCPRegionEuropeWest6,
GCPRegionEuropeWest8, GCPRegionEuropeWest9, GCPRegionEuropeWest10, GCPRegionEuropeWest12,
GCPRegionMECentral1, GCPRegionMECentral2, GCPRegionMEWest1,
GCPRegionNorthamericaNortheast1, GCPRegionNorthamericaNortheast2, GCPRegionNorthamericaSouth1,
GCPRegionSouthamericaEast1, GCPRegionSouthamericaWest1,
GCPRegionUSCentral1, GCPRegionUSEast1, GCPRegionUSEast4, GCPRegionUSEast5, GCPRegionUSSouth1,
GCPRegionUSWest1, GCPRegionUSWest2, GCPRegionUSWest3, GCPRegionUSWest4,
}
}
@@ -154,6 +211,19 @@ var SupportedRegions = map[CloudProviderType][]CloudProviderRegion{
AzureRegionUAECentral, AzureRegionUAENorth, AzureRegionUKSouth, AzureRegionUKWest,
AzureRegionWestCentralUS, AzureRegionWestEurope, AzureRegionWestIndia, AzureRegionWestUS, AzureRegionWestUS2, AzureRegionWestUS3,
},
CloudProviderTypeGCP: {
GCPRegionAfricaSouth1, GCPRegionAsiaEast1, GCPRegionAsiaEast2, GCPRegionAsiaNortheast1, GCPRegionAsiaNortheast2, GCPRegionAsiaNortheast3,
GCPRegionAsiaSouth1, GCPRegionAsiaSouth2, GCPRegionAsiaSoutheast1, GCPRegionAsiaSoutheast2, GCPRegionAsiaSoutheast3,
GCPRegionAustraliaSoutheast1, GCPRegionAustraliaSoutheast2,
GCPRegionEuropeCentral2, GCPRegionEuropeNorth1, GCPRegionEuropeNorth2, GCPRegionEuropeSouthwest1,
GCPRegionEuropeWest1, GCPRegionEuropeWest2, GCPRegionEuropeWest3, GCPRegionEuropeWest4, GCPRegionEuropeWest6,
GCPRegionEuropeWest8, GCPRegionEuropeWest9, GCPRegionEuropeWest10, GCPRegionEuropeWest12,
GCPRegionMECentral1, GCPRegionMECentral2, GCPRegionMEWest1,
GCPRegionNorthamericaNortheast1, GCPRegionNorthamericaNortheast2, GCPRegionNorthamericaSouth1,
GCPRegionSouthamericaEast1, GCPRegionSouthamericaWest1,
GCPRegionUSCentral1, GCPRegionUSEast1, GCPRegionUSEast4, GCPRegionUSEast5, GCPRegionUSSouth1,
GCPRegionUSWest1, GCPRegionUSWest2, GCPRegionUSWest3, GCPRegionUSWest4,
},
}
func validateAWSRegion(region string) error {
@@ -175,3 +245,13 @@ func validateAzureRegion(region string) error {
return errors.NewInvalidInputf(ErrCodeInvalidCloudRegion, "invalid Azure region: %s", region)
}
func validateGCPRegion(region string) error {
for _, r := range SupportedRegions[CloudProviderTypeGCP] {
if r.StringValue() == region {
return nil
}
}
return errors.NewInvalidInputf(ErrCodeInvalidCloudRegion, "invalid GCP region: %s", region)
}

View File

@@ -21,6 +21,7 @@ type CloudIntegrationService struct {
type ServiceConfig struct {
AWS *AWSServiceConfig `json:"aws,omitempty" required:"false" nullable:"false"`
Azure *AzureServiceConfig `json:"azure,omitempty" required:"false" nullable:"false"`
GCP *GCPServiceConfig `json:"gcp,omitempty" required:"false" nullable:"false"`
}
// ServiceMetadata helps to quickly list available services and whether it is enabled or not.
@@ -96,6 +97,7 @@ type DataCollected struct {
type TelemetryCollectionStrategy struct {
AWS *AWSTelemetryCollectionStrategy `json:"aws,omitempty" required:"false" nullable:"false"`
Azure *AzureTelemetryCollectionStrategy `json:"azure,omitempty" required:"false" nullable:"false"`
GCP *GCPTelemetryCollectionStrategy `json:"gcp,omitempty" required:"false" nullable:"false"`
}
// Assets represents the collection of dashboards.
@@ -145,6 +147,10 @@ func NewCloudIntegrationService(serviceID ServiceID, cloudIntegrationID valuer.U
if config.Azure == nil {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "Azure config is required for Azure service")
}
case CloudProviderTypeGCP:
if config.GCP == nil {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "GCP config is required for GCP service")
}
}
return &CloudIntegrationService{
@@ -261,6 +267,22 @@ func NewServiceConfigFromJSON(provider CloudProviderType, jsonString string) (*S
}
return &ServiceConfig{Azure: azureServiceConfig}, nil
case CloudProviderTypeGCP:
gcpServiceConfig := new(GCPServiceConfig)
if storableServiceConfig.GCP.Logs != nil {
gcpServiceConfig.Logs = &GCPServiceLogsConfig{
Enabled: storableServiceConfig.GCP.Logs.Enabled,
}
}
if storableServiceConfig.GCP.Metrics != nil {
gcpServiceConfig.Metrics = &GCPServiceMetricsConfig{
Enabled: storableServiceConfig.GCP.Metrics.Enabled,
}
}
return &ServiceConfig{GCP: gcpServiceConfig}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
@@ -285,6 +307,10 @@ func (service *CloudIntegrationService) Update(provider CloudProviderType, servi
if config.Azure == nil {
return errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "Azure config is required for Azure service")
}
case CloudProviderTypeGCP:
if config.GCP == nil {
return errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "GCP config is required for GCP service")
}
default:
return errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
@@ -306,6 +332,10 @@ func (config *ServiceConfig) IsServiceEnabled(provider CloudProviderType) bool {
logsEnabled := config.Azure.Logs != nil && config.Azure.Logs.Enabled
metricsEnabled := config.Azure.Metrics != nil && config.Azure.Metrics.Enabled
return logsEnabled || metricsEnabled
case CloudProviderTypeGCP:
logsEnabled := config.GCP.Logs != nil && config.GCP.Logs.Enabled
metricsEnabled := config.GCP.Metrics != nil && config.GCP.Metrics.Enabled
return logsEnabled || metricsEnabled
default:
return false
}
@@ -319,6 +349,8 @@ func (config *ServiceConfig) IsMetricsEnabled(provider CloudProviderType) bool {
return config.AWS.Metrics != nil && config.AWS.Metrics.Enabled
case CloudProviderTypeAzure:
return config.Azure.Metrics != nil && config.Azure.Metrics.Enabled
case CloudProviderTypeGCP:
return config.GCP.Metrics != nil && config.GCP.Metrics.Enabled
default:
return false
}
@@ -331,6 +363,8 @@ func (config *ServiceConfig) IsLogsEnabled(provider CloudProviderType) bool {
return config.AWS.Logs != nil && config.AWS.Logs.Enabled
case CloudProviderTypeAzure:
return config.Azure.Logs != nil && config.Azure.Logs.Enabled
case CloudProviderTypeGCP:
return config.GCP.Logs != nil && config.GCP.Logs.Enabled
default:
return false
}

View File

@@ -39,6 +39,9 @@ var (
AzureServiceCosmosDB = ServiceID{valuer.NewString("cosmosdb")}
AzureServiceCassandraDB = ServiceID{valuer.NewString("cassandradb")}
AzureServiceRedis = ServiceID{valuer.NewString("redis")}
// GCP services.
GCPServiceCloudSQL = ServiceID{valuer.NewString("cloudsql")}
)
func (ServiceID) Enum() []any {
@@ -70,6 +73,7 @@ func (ServiceID) Enum() []any {
AzureServiceCosmosDB,
AzureServiceCassandraDB,
AzureServiceRedis,
GCPServiceCloudSQL,
}
}
@@ -106,6 +110,9 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
AzureServiceCassandraDB,
AzureServiceRedis,
},
CloudProviderTypeGCP: {
GCPServiceCloudSQL,
},
}
func NewServiceID(provider CloudProviderType, service string) (ServiceID, error) {

View File

@@ -143,7 +143,7 @@ def test_get_credentials_unsupported_provider(
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/cloud_integrations/gcp/credentials"),
signoz.self.host_configs["8080"].get("/api/v1/cloud_integrations/unknown/credentials"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=10,
)

View File

@@ -56,14 +56,14 @@ def test_create_account_unsupported_provider(
) -> None:
"""Test that creating an account with an unsupported cloud provider returns 400."""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
cloud_provider = "gcp"
cloud_provider = "unknown"
endpoint = f"/api/v1/cloud_integrations/{cloud_provider}/accounts"
response = requests.post(
signoz.self.host_configs["8080"].get(endpoint),
headers={"Authorization": f"Bearer {admin_token}"},
json={
"config": {"gcp": {"deploymentRegion": "us-central1", "regions": ["us-central1"]}},
"config": {"unknown": {"deploymentRegion": "us-central1", "regions": ["us-central1"]}},
"credentials": {
"sigNozApiURL": "https://test.signoz.cloud",
"sigNozApiKey": "test-key",

View File

@@ -341,7 +341,7 @@ def test_list_services_unsupported_provider(
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/cloud_integrations/gcp/services"),
signoz.self.host_configs["8080"].get("/api/v1/cloud_integrations/unknown/services"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=10,
)