mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-01 20:30:37 +01:00
Compare commits
4 Commits
nv/dashboa
...
feat/noz-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38286e38dd | ||
|
|
13812fac62 | ||
|
|
df77b8d125 | ||
|
|
028ac27496 |
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
144
frontend/src/container/PanelWrapper/preparePieChartData.ts
Normal file
144
frontend/src/container/PanelWrapper/preparePieChartData.ts
Normal 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));
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
.rolesListingTable {
|
||||
margin-top: 12px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
.rolesSettingsContent {
|
||||
padding: 0 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.rolesSettingsToolbar {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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 |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
40
pkg/types/cloudintegrationtypes/cloudprovider_gcp.go
Normal file
40
pkg/types/cloudintegrationtypes/cloudprovider_gcp.go
Normal 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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user