mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-30 15:40:27 +01:00
Compare commits
1 Commits
traceop
...
feat/overv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74811b36c8 |
@@ -2103,6 +2103,8 @@ components:
|
||||
type: boolean
|
||||
org_id:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -2125,6 +2127,27 @@ components:
|
||||
publicDashboard:
|
||||
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
|
||||
type: object
|
||||
DashboardtypesGettableSystemDashboard:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
|
||||
id:
|
||||
type: string
|
||||
org_id:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesPostablePublicDashboard:
|
||||
properties:
|
||||
defaultTimeRange:
|
||||
@@ -2142,6 +2165,11 @@ components:
|
||||
timeRangeEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardtypesUpdatableSystemDashboard:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
|
||||
type: object
|
||||
ErrorsJSON:
|
||||
properties:
|
||||
code:
|
||||
@@ -10055,6 +10083,169 @@ paths:
|
||||
summary: Update a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/system/{source}/dashboard:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns the system dashboard for the callers org
|
||||
keyed by source (e.g. ai-o11y-overview).
|
||||
operationId: GetSystemDashboard
|
||||
parameters:
|
||||
- in: path
|
||||
name: source
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableSystemDashboard'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get system dashboard
|
||||
tags:
|
||||
- system-dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint replaces the system dashboard for the callers org
|
||||
with the provided payload.
|
||||
operationId: UpdateSystemDashboard
|
||||
parameters:
|
||||
- in: path
|
||||
name: source
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesUpdatableSystemDashboard'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableSystemDashboard'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Update system dashboard
|
||||
tags:
|
||||
- system-dashboard
|
||||
/api/v1/system/{source}/dashboard/reset:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint drops any customisation to the system dashboard,
|
||||
writes the defaults back, and returns the freshly seeded dashboard.
|
||||
operationId: ResetSystemDashboard
|
||||
parameters:
|
||||
- in: path
|
||||
name: source
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableSystemDashboard'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Reset system dashboard to defaults
|
||||
tags:
|
||||
- system-dashboard
|
||||
/api/v1/testChannel:
|
||||
post:
|
||||
deprecated: true
|
||||
|
||||
@@ -2781,6 +2781,10 @@ export interface DashboardtypesDashboardDTO {
|
||||
* @type string
|
||||
*/
|
||||
org_id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -2812,6 +2816,40 @@ export interface DashboardtypesGettablePublicDashboardDataDTO {
|
||||
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesGettableSystemDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: DashboardtypesStorableDashboardDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
org_id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface DashboardtypesPostablePublicDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -2838,6 +2876,10 @@ export interface DashboardtypesUpdatablePublicDashboardDTO {
|
||||
timeRangeEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardtypesUpdatableSystemDashboardDTO {
|
||||
data?: DashboardtypesStorableDashboardDataDTO;
|
||||
}
|
||||
|
||||
export interface ErrorsJSONDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -7335,6 +7377,39 @@ export type UpdateSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
mapperId: string;
|
||||
};
|
||||
export type GetSystemDashboardPathParameters = {
|
||||
source: string;
|
||||
};
|
||||
export type GetSystemDashboard200 = {
|
||||
data: DashboardtypesGettableSystemDashboardDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateSystemDashboardPathParameters = {
|
||||
source: string;
|
||||
};
|
||||
export type UpdateSystemDashboard200 = {
|
||||
data: DashboardtypesGettableSystemDashboardDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ResetSystemDashboardPathParameters = {
|
||||
source: string;
|
||||
};
|
||||
export type ResetSystemDashboard200 = {
|
||||
data: DashboardtypesGettableSystemDashboardDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListUsersDeprecated200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
318
frontend/src/api/generated/services/system-dashboard/index.ts
Normal file
318
frontend/src/api/generated/services/system-dashboard/index.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
DashboardtypesUpdatableSystemDashboardDTO,
|
||||
GetSystemDashboard200,
|
||||
GetSystemDashboardPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
ResetSystemDashboard200,
|
||||
ResetSystemDashboardPathParameters,
|
||||
UpdateSystemDashboard200,
|
||||
UpdateSystemDashboardPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* This endpoint returns the system dashboard for the callers org keyed by source (e.g. ai-o11y-overview).
|
||||
* @summary Get system dashboard
|
||||
*/
|
||||
export const getSystemDashboard = (
|
||||
{ source }: GetSystemDashboardPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetSystemDashboard200>({
|
||||
url: `/api/v1/system/${source}/dashboard`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetSystemDashboardQueryKey = ({
|
||||
source,
|
||||
}: GetSystemDashboardPathParameters) => {
|
||||
return [`/api/v1/system/${source}/dashboard`] as const;
|
||||
};
|
||||
|
||||
export const getGetSystemDashboardQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ source }: GetSystemDashboardPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetSystemDashboardQueryKey({ source });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>
|
||||
> = ({ signal }) => getSystemDashboard({ source }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!source,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetSystemDashboardQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>
|
||||
>;
|
||||
export type GetSystemDashboardQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get system dashboard
|
||||
*/
|
||||
|
||||
export function useGetSystemDashboard<
|
||||
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ source }: GetSystemDashboardPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSystemDashboard>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetSystemDashboardQueryOptions({ source }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get system dashboard
|
||||
*/
|
||||
export const invalidateGetSystemDashboard = async (
|
||||
queryClient: QueryClient,
|
||||
{ source }: GetSystemDashboardPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetSystemDashboardQueryKey({ source }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint replaces the system dashboard for the callers org with the provided payload.
|
||||
* @summary Update system dashboard
|
||||
*/
|
||||
export const updateSystemDashboard = (
|
||||
{ source }: UpdateSystemDashboardPathParameters,
|
||||
dashboardtypesUpdatableSystemDashboardDTO: BodyType<DashboardtypesUpdatableSystemDashboardDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateSystemDashboard200>({
|
||||
url: `/api/v1/system/${source}/dashboard`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesUpdatableSystemDashboardDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateSystemDashboardMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSystemDashboard>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSystemDashboardPathParameters;
|
||||
data: BodyType<DashboardtypesUpdatableSystemDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSystemDashboard>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSystemDashboardPathParameters;
|
||||
data: BodyType<DashboardtypesUpdatableSystemDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateSystemDashboard'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateSystemDashboard>>,
|
||||
{
|
||||
pathParams: UpdateSystemDashboardPathParameters;
|
||||
data: BodyType<DashboardtypesUpdatableSystemDashboardDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateSystemDashboard(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateSystemDashboardMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateSystemDashboard>>
|
||||
>;
|
||||
export type UpdateSystemDashboardMutationBody =
|
||||
BodyType<DashboardtypesUpdatableSystemDashboardDTO>;
|
||||
export type UpdateSystemDashboardMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update system dashboard
|
||||
*/
|
||||
export const useUpdateSystemDashboard = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSystemDashboard>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSystemDashboardPathParameters;
|
||||
data: BodyType<DashboardtypesUpdatableSystemDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateSystemDashboard>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSystemDashboardPathParameters;
|
||||
data: BodyType<DashboardtypesUpdatableSystemDashboardDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateSystemDashboardMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint drops any customisation to the system dashboard, writes the defaults back, and returns the freshly seeded dashboard.
|
||||
* @summary Reset system dashboard to defaults
|
||||
*/
|
||||
export const resetSystemDashboard = (
|
||||
{ source }: ResetSystemDashboardPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ResetSystemDashboard200>({
|
||||
url: `/api/v1/system/${source}/dashboard/reset`,
|
||||
method: 'POST',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getResetSystemDashboardMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetSystemDashboard>>,
|
||||
TError,
|
||||
{ pathParams: ResetSystemDashboardPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetSystemDashboard>>,
|
||||
TError,
|
||||
{ pathParams: ResetSystemDashboardPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['resetSystemDashboard'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof resetSystemDashboard>>,
|
||||
{ pathParams: ResetSystemDashboardPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return resetSystemDashboard(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ResetSystemDashboardMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof resetSystemDashboard>>
|
||||
>;
|
||||
|
||||
export type ResetSystemDashboardMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Reset system dashboard to defaults
|
||||
*/
|
||||
export const useResetSystemDashboard = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resetSystemDashboard>>,
|
||||
TError,
|
||||
{ pathParams: ResetSystemDashboardPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof resetSystemDashboard>>,
|
||||
TError,
|
||||
{ pathParams: ResetSystemDashboardPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getResetSystemDashboardMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
.codeBlock {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.codeBlockSyntaxHighlighter {
|
||||
background-color: var(--l2-background) !important;
|
||||
border-radius: 4px !important;
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
|
||||
pre {
|
||||
color: var(--l2-foreground) !important;
|
||||
font-family: 'Geist Mono' !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--l1-foreground) !important;
|
||||
font-family: 'Geist Mono' !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import CodeBlock from './CodeBlock';
|
||||
|
||||
const mockCopyToClipboard = jest.fn();
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
useCopyToClipboard: (): [unknown, (text: string) => void] => [
|
||||
undefined,
|
||||
mockCopyToClipboard,
|
||||
],
|
||||
}));
|
||||
|
||||
describe('CodeBlock', () => {
|
||||
beforeEach(() => {
|
||||
mockCopyToClipboard.mockReset();
|
||||
});
|
||||
|
||||
it('renders code block mode by default', () => {
|
||||
render(<CodeBlock code={'const x = 1;\n'} language="javascript" />);
|
||||
|
||||
const container = screen.getByTestId('code-block-container');
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveTextContent('const x = 1;');
|
||||
});
|
||||
|
||||
it('renders inline code when inline is true', () => {
|
||||
render(<CodeBlock code="inline value" inline />);
|
||||
|
||||
const inlineCode = screen.getByText('inline value');
|
||||
expect(inlineCode.tagName.toLowerCase()).toBe('code');
|
||||
expect(screen.queryByTestId('code-block-container')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('copies code and triggers callback', async () => {
|
||||
const onCopy = jest.fn();
|
||||
render(<CodeBlock code="SELECT * FROM logs;" onCopy={onCopy} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /copy code/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('SELECT * FROM logs;');
|
||||
});
|
||||
expect(onCopy).toHaveBeenCalledWith('SELECT * FROM logs;');
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import SyntaxHighlighter, {
|
||||
a11yDark,
|
||||
} from 'components/MarkdownRenderer/syntaxHighlighter';
|
||||
|
||||
import styles from './CodeBlock.module.scss';
|
||||
|
||||
export interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
showCopyButton?: boolean;
|
||||
onCopy?: (copiedCode: string) => void;
|
||||
}
|
||||
|
||||
function CodeBlock({
|
||||
code,
|
||||
language = 'text',
|
||||
className,
|
||||
inline = false,
|
||||
showLineNumbers = false,
|
||||
showCopyButton = true,
|
||||
onCopy,
|
||||
}: CodeBlockProps): JSX.Element {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const normalizedCode = useMemo(() => code?.replace(/\n$/, '') ?? '', [code]);
|
||||
|
||||
const handleCopy = (): void => {
|
||||
copyToClipboard(normalizedCode);
|
||||
setIsCopied(true);
|
||||
onCopy?.(normalizedCode);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (inline) {
|
||||
return <code className={className}>{normalizedCode}</code>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.codeBlock} ${className}`}
|
||||
style={{ position: 'relative' }}
|
||||
data-testid="code-block-container"
|
||||
>
|
||||
{showCopyButton ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
prefix={isCopied ? <Check size={14} /> : <Copy size={14} />}
|
||||
aria-label="Copy code"
|
||||
title={isCopied ? 'Copied' : 'Copy'}
|
||||
style={{ position: 'absolute', right: 8, top: 8, zIndex: 1 }}
|
||||
/>
|
||||
) : null}
|
||||
<SyntaxHighlighter
|
||||
style={a11yDark}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLongLines
|
||||
className={styles.codeBlockSyntaxHighlighter}
|
||||
>
|
||||
{normalizedCode}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CodeBlock.defaultProps = {
|
||||
language: 'text',
|
||||
className: undefined,
|
||||
inline: false,
|
||||
showLineNumbers: false,
|
||||
showCopyButton: true,
|
||||
onCopy: undefined,
|
||||
};
|
||||
|
||||
export default CodeBlock;
|
||||
@@ -104,12 +104,7 @@ export const usePanelContextMenu = ({
|
||||
}
|
||||
|
||||
if (data && data?.record?.queryName) {
|
||||
onClick(data.coord, {
|
||||
...data.record,
|
||||
label: data.label,
|
||||
seriesColor: data.seriesColor,
|
||||
timeRange,
|
||||
});
|
||||
onClick(data.coord, { ...data.record, label: data.label, timeRange });
|
||||
}
|
||||
},
|
||||
[onClick, queryResponse],
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
.cloud-account-setup-modal {
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
[data-slot='drawer-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: thin;
|
||||
padding-right: 16px;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-background) var(--l1-background);
|
||||
}
|
||||
|
||||
.cloud-account-setup-prerequisites {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__list-item {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&-bullet {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__list-item-highlight {
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-how-it-works-accordion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 24px 0;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
padding: 4px 16px 4px 0px;
|
||||
|
||||
&.open {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
animation: cloud-account-setup-accordion-reveal 220ms ease-out forwards;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form__code-block-tabs {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
margin-bottom: 12px;
|
||||
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
[role='tabpanel'] {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
[data-slot='tabs-trigger'] {
|
||||
padding: 4px 24px !important;
|
||||
border: none !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloud-account-setup-accordion-reveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
&__confirm-selection-count {
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
&__close-button {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&,
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&__alert {
|
||||
width: 100%;
|
||||
|
||||
[data-slot='callout'] {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.retry-time {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&__description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&__select {
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
&__form-item {
|
||||
margin: 0;
|
||||
}
|
||||
&__include-all-regions-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
margin-bottom: 12px;
|
||||
&-label {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&__note {
|
||||
padding: 12px;
|
||||
color: var(--callout-primary-description);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.06px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
}
|
||||
&__submit-button {
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import awsDarkLogoUrl from '@/assets/Logos/aws-dark.svg';
|
||||
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
return (
|
||||
<div className="hero-section">
|
||||
<div className="hero-section__details">
|
||||
<div className="hero-section__details-header">
|
||||
<div className="hero-section__icon">
|
||||
<img src={awsDarkLogoUrl} alt="AWS" />
|
||||
</div>
|
||||
|
||||
<div className="hero-section__details-title">AWS</div>
|
||||
</div>
|
||||
<div className="hero-section__details-description">
|
||||
AWS is a cloud computing platform that provides a range of services for
|
||||
building and running applications.
|
||||
</div>
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -117,12 +117,6 @@
|
||||
min-width: 140px !important;
|
||||
}
|
||||
|
||||
&.azure {
|
||||
.ant-select-selector {
|
||||
min-width: 282px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-item-option-active {
|
||||
background: var(--l3-background) !important;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui';
|
||||
@@ -6,29 +6,19 @@ import { Select, Skeleton } from 'antd';
|
||||
import { SelectProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import cx from 'classnames';
|
||||
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
|
||||
import {
|
||||
CloudAccount as IntegrationCloudAccount,
|
||||
IntegrationType,
|
||||
} from 'container/Integrations/types';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown, Dot, PencilLine, Plug, Plus } from 'lucide-react';
|
||||
|
||||
import AzureCloudAccountSetupModal from '../../AzureCloudServices/AddNewAccount/CloudAccountSetupModal';
|
||||
import AzureAccountSettingsModal from '../../AzureCloudServices/EditAccount/AccountSettingsModal';
|
||||
import {
|
||||
mapAccountDtoToAwsCloudAccount,
|
||||
mapAccountDtoToAzureCloudAccount,
|
||||
} from '../../mapCloudAccountFromDto';
|
||||
import AwsCloudAccountSetupModal from '../AddNewAccount/CloudAccountSetupModal';
|
||||
import AwsAccountSettingsModal from '../EditAccount/AccountSettingsModal';
|
||||
import { CloudAccount as AwsCloudAccount } from '../types';
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../../mapAwsCloudAccountFromDto';
|
||||
import { CloudAccount } from '../../types';
|
||||
import AccountSettingsModal from './AccountSettingsModal';
|
||||
import CloudAccountSetupModal from './CloudAccountSetupModal';
|
||||
|
||||
import './AccountActions.style.scss';
|
||||
|
||||
function AccountActionsRenderer({
|
||||
type,
|
||||
accounts,
|
||||
isLoading,
|
||||
activeAccount,
|
||||
@@ -37,10 +27,9 @@ function AccountActionsRenderer({
|
||||
onIntegrationModalOpen,
|
||||
onAccountSettingsModalOpen,
|
||||
}: {
|
||||
type: IntegrationType;
|
||||
accounts: IntegrationCloudAccount[] | undefined;
|
||||
accounts: CloudAccount[] | undefined;
|
||||
isLoading: boolean;
|
||||
activeAccount: IntegrationCloudAccount | null;
|
||||
activeAccount: CloudAccount | null;
|
||||
selectOptions: SelectProps['options'];
|
||||
onAccountChange: (value: string) => void;
|
||||
onIntegrationModalOpen: () => void;
|
||||
@@ -68,11 +57,9 @@ function AccountActionsRenderer({
|
||||
<Select
|
||||
value={activeAccount?.providerAccountId}
|
||||
options={selectOptions}
|
||||
rootClassName={cx('cloud-account-selector', {
|
||||
[type.toLowerCase()]: type,
|
||||
})}
|
||||
rootClassName="cloud-account-selector"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={`Select ${type} Account`}
|
||||
placeholder="Select AWS Account"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
@@ -115,49 +102,21 @@ function AccountActionsRenderer({
|
||||
);
|
||||
}
|
||||
|
||||
function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
function AccountActions(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: listAccountsResponse, isLoading } = useListAccounts({
|
||||
cloudProvider: type,
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
});
|
||||
|
||||
const accounts = useMemo((): IntegrationCloudAccount[] | undefined => {
|
||||
const accounts = useMemo((): CloudAccount[] | undefined => {
|
||||
const raw = listAccountsResponse?.data?.accounts;
|
||||
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mappedAccounts: IntegrationCloudAccount[] = [];
|
||||
|
||||
if (type === IntegrationType.AWS_SERVICES) {
|
||||
raw.forEach((account) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
const mapped = mapAccountDtoToAwsCloudAccount(account);
|
||||
if (mapped) {
|
||||
mappedAccounts.push(mapped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (type === IntegrationType.AZURE_SERVICES) {
|
||||
raw.forEach((account) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
const mapped = mapAccountDtoToAzureCloudAccount(account);
|
||||
if (mapped) {
|
||||
mappedAccounts.push(mapped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mappedAccounts;
|
||||
}, [listAccountsResponse, type]);
|
||||
return raw
|
||||
.map(mapAccountDtoToAwsCloudAccount)
|
||||
.filter((account): account is CloudAccount => account !== null);
|
||||
}, [listAccountsResponse]);
|
||||
|
||||
const initialAccount = useMemo(
|
||||
() =>
|
||||
@@ -168,8 +127,9 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
[accounts, urlQuery],
|
||||
);
|
||||
|
||||
const [activeAccount, setActiveAccount] =
|
||||
useState<IntegrationCloudAccount | null>(initialAccount);
|
||||
const [activeAccount, setActiveAccount] = useState<CloudAccount | null>(
|
||||
initialAccount,
|
||||
);
|
||||
|
||||
// Update state when initial value changes
|
||||
useEffect(() => {
|
||||
@@ -189,17 +149,16 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
}, [initialAccount]);
|
||||
|
||||
const [isIntegrationModalOpen, setIsIntegrationModalOpen] = useState(false);
|
||||
|
||||
const startAccountConnectionAttempt = (): void => {
|
||||
setIsIntegrationModalOpen(true);
|
||||
logEvent(`${type} Integration: Account connection attempt started`, {});
|
||||
logEvent('AWS Integration: Account connection attempt started', {});
|
||||
};
|
||||
|
||||
const [isAccountSettingsModalOpen, setIsAccountSettingsModalOpen] =
|
||||
useState(false);
|
||||
const openAccountSettings = (): void => {
|
||||
setIsAccountSettingsModalOpen(true);
|
||||
logEvent(`${type} Integration: Account settings viewed`, {
|
||||
logEvent('AWS Integration: Account settings viewed', {
|
||||
cloudAccountId: activeAccount?.cloud_account_id,
|
||||
});
|
||||
};
|
||||
@@ -207,16 +166,13 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
// log telemetry event when an account is viewed.
|
||||
useEffect(() => {
|
||||
if (activeAccount) {
|
||||
logEvent(`${type} Integration: Account viewed`, {
|
||||
logEvent('AWS Integration: Account viewed', {
|
||||
cloudAccountId: activeAccount?.cloud_account_id,
|
||||
status: activeAccount?.status,
|
||||
enabledRegions:
|
||||
'regions' in activeAccount.config
|
||||
? activeAccount.config.regions
|
||||
: activeAccount.config.resource_groups,
|
||||
enabledRegions: activeAccount?.config?.regions,
|
||||
});
|
||||
}
|
||||
}, [activeAccount, type]);
|
||||
}, [activeAccount]);
|
||||
|
||||
const selectOptions: SelectProps['options'] = useMemo(
|
||||
() =>
|
||||
@@ -232,7 +188,6 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
return (
|
||||
<div className="hero-section__actions">
|
||||
<AccountActionsRenderer
|
||||
type={type}
|
||||
accounts={accounts}
|
||||
isLoading={isLoading}
|
||||
activeAccount={activeAccount}
|
||||
@@ -249,39 +204,17 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
/>
|
||||
|
||||
{isIntegrationModalOpen && (
|
||||
<>
|
||||
{type === IntegrationType.AWS_SERVICES && (
|
||||
<AwsCloudAccountSetupModal
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.AZURE_SERVICES && (
|
||||
<AzureCloudAccountSetupModal
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<CloudAccountSetupModal
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAccountSettingsModalOpen && activeAccount && (
|
||||
<>
|
||||
{type === IntegrationType.AWS_SERVICES && (
|
||||
<AwsAccountSettingsModal
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount as AwsCloudAccount}
|
||||
setActiveAccount={
|
||||
setActiveAccount as Dispatch<SetStateAction<AwsCloudAccount | null>>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.AZURE_SERVICES && (
|
||||
<AzureAccountSettingsModal
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<AccountSettingsModal
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -9,10 +9,10 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
import logEvent from '../../../../../api/common/logEvent';
|
||||
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
|
||||
import { RegionSelector } from '../RegionForm/RegionSelector';
|
||||
import { CloudAccount } from '../types';
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
|
||||
import './AccountSettingsModal.style.scss';
|
||||
|
||||
@@ -110,7 +110,11 @@ function AccountSettingsModal({
|
||||
form,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
account?.providerAccountId,
|
||||
account?.id,
|
||||
handleRemoveIntegrationAccountSuccess,
|
||||
isSaveDisabled,
|
||||
handleSubmit,
|
||||
isLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
]);
|
||||
@@ -129,7 +133,6 @@ function AccountSettingsModal({
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
|
||||
cloudProvider={INTEGRATION_TYPES.AWS}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Callout } from '@signozhq/ui';
|
||||
import { Spin } from 'antd';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Alert, Spin } from 'antd';
|
||||
import { LoaderCircle, TriangleAlert } from 'lucide-react';
|
||||
|
||||
import { ModalStateEnum } from '../HeroSection/types';
|
||||
import { ModalStateEnum } from '../types';
|
||||
|
||||
function AlertMessage({
|
||||
modalState,
|
||||
@@ -12,13 +12,14 @@ function AlertMessage({
|
||||
switch (modalState) {
|
||||
case ModalStateEnum.WAITING:
|
||||
return (
|
||||
<Callout
|
||||
title={
|
||||
<Alert
|
||||
message={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<Spin
|
||||
indicator={
|
||||
<LoaderCircle
|
||||
size={14}
|
||||
color={Color.BG_AMBER_400}
|
||||
className="anticon anticon-loading anticon-spin ant-spin-dot"
|
||||
/>
|
||||
}
|
||||
@@ -27,19 +28,21 @@ function AlertMessage({
|
||||
<span className="retry-time">10</span> secs...
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon={false}
|
||||
className="cloud-account-setup-form__alert"
|
||||
type="warning"
|
||||
/>
|
||||
);
|
||||
case ModalStateEnum.ERROR:
|
||||
return (
|
||||
<Callout
|
||||
title={
|
||||
<Alert
|
||||
message={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<TriangleAlert type="solid" size={15} color={Color.BG_SAKURA_400} />
|
||||
{`We couldn't establish a connection to your AWS account. Please try again`}
|
||||
</div>
|
||||
}
|
||||
type="error"
|
||||
className="cloud-account-setup-form__alert"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -0,0 +1,180 @@
|
||||
.cloud-account-setup-modal {
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
[data-slot='drawer-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
&__confirm-selection-count {
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
&__close-button {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&,
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&__alert {
|
||||
&.ant-alert {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&.ant-alert-error {
|
||||
color: var(--danger-foreground);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
}
|
||||
|
||||
&.ant-alert-warning {
|
||||
color: var(--warning-foreground);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--warning-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--warning-background) 10%, transparent);
|
||||
}
|
||||
|
||||
&-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.retry-time {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&__description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&__select {
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
&__form-item {
|
||||
margin: 0;
|
||||
}
|
||||
&__include-all-regions-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
margin-bottom: 12px;
|
||||
&-label {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&__note {
|
||||
padding: 12px;
|
||||
color: var(--callout-primary-description);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.06px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
}
|
||||
&__submit-button {
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
ActiveViewEnum,
|
||||
IntegrationModalProps,
|
||||
ModalStateEnum,
|
||||
} from '../../../HeroSection/types';
|
||||
import { RegionForm } from '../RegionForm/RegionForm';
|
||||
} from '../types';
|
||||
import { RegionForm } from './RegionForm';
|
||||
|
||||
import './CloudAccountSetupModal.style.scss';
|
||||
|
||||
@@ -74,6 +74,8 @@ function CloudAccountSetupModal({
|
||||
isConnectionParamsLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
isLoading,
|
||||
isGeneratingUrl,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
@@ -5,7 +5,7 @@ import { ChevronDown } from 'lucide-react';
|
||||
import { Region } from 'utils/regions';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { RegionSelector } from './RegionForm/RegionSelector';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
|
||||
// Form section components
|
||||
function RegionDeploymentSection({
|
||||
@@ -3,18 +3,15 @@ import { Form } from 'antd';
|
||||
import { useGetAccount } from 'api/generated/services/cloudintegration';
|
||||
import cx from 'classnames';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import {
|
||||
ModalStateEnum,
|
||||
RegionFormProps,
|
||||
} from 'container/Integrations/HeroSection/types';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import AlertMessage from '../../AlertMessage';
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import {
|
||||
ComplianceNote,
|
||||
MonitoringRegionsSection,
|
||||
RegionDeploymentSection,
|
||||
} from '../IntegrateNowFormSections';
|
||||
} from './IntegrateNowFormSections';
|
||||
import RenderConnectionFields from './RenderConnectionParams';
|
||||
|
||||
export function RegionForm({
|
||||
@@ -79,6 +76,8 @@ export function RegionForm({
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<AlertMessage modalState={modalState} />
|
||||
|
||||
<div
|
||||
className={cx(`cloud-account-setup-form__content`, {
|
||||
disabled: isFormDisabled,
|
||||
@@ -101,10 +100,6 @@ export function RegionForm({
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="cloud-account-setup-form__alert">
|
||||
<AlertMessage modalState={modalState} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
.remove-integration-account-modal {
|
||||
&__cloud-provider {
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
background-color: var(--l1-background);
|
||||
border: 1px solid var(--l3-background);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
@@ -4,21 +4,16 @@ import { Modal } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useDisconnectAccount } from 'api/generated/services/cloudintegration';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import {
|
||||
INTEGRATION_TELEMETRY_EVENTS,
|
||||
INTEGRATION_TYPES,
|
||||
} from 'container/Integrations/constants';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Unlink } from 'lucide-react';
|
||||
|
||||
import './RemoveIntegrationAccount.scss';
|
||||
|
||||
function RemoveIntegrationAccount({
|
||||
cloudProvider,
|
||||
accountId,
|
||||
onRemoveIntegrationAccountSuccess,
|
||||
}: {
|
||||
cloudProvider: string;
|
||||
accountId: string;
|
||||
onRemoveIntegrationAccountSuccess: () => void;
|
||||
}): JSX.Element {
|
||||
@@ -44,13 +39,12 @@ function RemoveIntegrationAccount({
|
||||
},
|
||||
});
|
||||
const handleOk = (): void => {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATION_ACCOUNT_REMOVED, {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
|
||||
accountId,
|
||||
integration: cloudProvider,
|
||||
});
|
||||
disconnectAccount({
|
||||
pathParams: {
|
||||
cloudProvider,
|
||||
cloudProvider: 'aws',
|
||||
id: accountId,
|
||||
},
|
||||
});
|
||||
@@ -84,28 +78,13 @@ function RemoveIntegrationAccount({
|
||||
loading: isRemoveIntegrationLoading,
|
||||
}}
|
||||
>
|
||||
{cloudProvider === INTEGRATION_TYPES.AWS ? (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your Azure subscription within the next ~15 minutes
|
||||
(deployment stack named signoz-integration-telemetry will be deleted
|
||||
automatically). <br />
|
||||
<br />
|
||||
After that, you have to manually delete 'signoz-integration'
|
||||
deployment stack that was created while connecting this account (Takes ~20
|
||||
minutes to delete).
|
||||
</>
|
||||
)}
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
@@ -4,7 +4,7 @@ import { useListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../../mapCloudAccountFromDto';
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../mapAwsCloudAccountFromDto';
|
||||
import { CloudAccount } from '../types';
|
||||
|
||||
import './S3BucketsSelector.styles.scss';
|
||||
|
||||
@@ -12,14 +12,14 @@ import {
|
||||
useUpdateService,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
import {
|
||||
CloudintegrationtypesServiceConfigDTO,
|
||||
CloudintegrationtypesServiceDTO,
|
||||
ListServicesMetadata200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import ServiceDashboards from 'container/Integrations/CloudIntegration/ServiceDashboards/ServiceDashboards';
|
||||
import { IntegrationType, IServiceStatus } from 'container/Integrations/types';
|
||||
import ServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { IServiceStatus } from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Save, X } from 'lucide-react';
|
||||
|
||||
@@ -36,81 +36,7 @@ type ServiceDetailsData = CloudintegrationtypesServiceDTO & {
|
||||
status?: IServiceStatus;
|
||||
};
|
||||
|
||||
const EMPTY_FORM_VALUES: ServiceConfigFormValues = {
|
||||
logsEnabled: false,
|
||||
metricsEnabled: false,
|
||||
s3BucketsByRegion: {},
|
||||
};
|
||||
|
||||
function getInitialFormValues(
|
||||
type: IntegrationType,
|
||||
serviceDetailsData?: ServiceDetailsData,
|
||||
): ServiceConfigFormValues {
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
|
||||
return {
|
||||
logsEnabled: integrationConfig?.logs?.enabled || false,
|
||||
metricsEnabled: integrationConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion:
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws?.logs
|
||||
?.s3Buckets || {}
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
||||
function getServiceConfigPayload({
|
||||
type,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
metricsEnabled,
|
||||
isLogsSupported,
|
||||
isMetricsSupported,
|
||||
s3BucketsByRegion,
|
||||
}: {
|
||||
type: IntegrationType;
|
||||
serviceId: string;
|
||||
logsEnabled: boolean;
|
||||
metricsEnabled: boolean;
|
||||
isLogsSupported: boolean;
|
||||
isMetricsSupported: boolean;
|
||||
s3BucketsByRegion: Record<string, string[]>;
|
||||
}): CloudintegrationtypesServiceConfigDTO {
|
||||
if (type === IntegrationType.AWS_SERVICES) {
|
||||
return {
|
||||
aws: {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
s3Buckets:
|
||||
serviceId === 's3sync' && isLogsSupported ? s3BucketsByRegion : {},
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
azure: {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ServiceDetails({
|
||||
type,
|
||||
}: {
|
||||
type: IntegrationType;
|
||||
}): JSX.Element | null {
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
@@ -125,7 +51,7 @@ function ServiceDetails({
|
||||
isLoading: isServiceDetailsLoading,
|
||||
} = useGetService(
|
||||
{
|
||||
cloudProvider: type,
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
serviceId: serviceId || '',
|
||||
},
|
||||
{
|
||||
@@ -139,17 +65,10 @@ function ServiceDetails({
|
||||
},
|
||||
);
|
||||
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
const awsConfig = serviceDetailsData?.cloudIntegrationService?.config?.aws;
|
||||
const isServiceEnabledInPersistedConfig =
|
||||
Boolean(integrationConfig?.logs?.enabled) ||
|
||||
Boolean(integrationConfig?.metrics?.enabled);
|
||||
Boolean(awsConfig?.logs?.enabled) || Boolean(awsConfig?.metrics?.enabled);
|
||||
const serviceDetailsId = serviceDetailsData?.id;
|
||||
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
|
||||
const isMetricsSupported =
|
||||
serviceDetailsData?.supportedSignals?.metrics || false;
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -158,31 +77,43 @@ function ServiceDetails({
|
||||
watch,
|
||||
formState: { isDirty },
|
||||
} = useForm<ServiceConfigFormValues>({
|
||||
defaultValues: getInitialFormValues(type, serviceDetailsData),
|
||||
defaultValues: {
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
|
||||
},
|
||||
});
|
||||
|
||||
const resetToConfig = useCallback((): void => {
|
||||
reset(getInitialFormValues(type, serviceDetailsData));
|
||||
}, [reset, serviceDetailsData, type]);
|
||||
const resetToAwsConfig = useCallback((): void => {
|
||||
reset({
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
|
||||
});
|
||||
}, [awsConfig, reset]);
|
||||
|
||||
// Ensure form state does not leak across service switches while new details load.
|
||||
useEffect(() => {
|
||||
reset(EMPTY_FORM_VALUES);
|
||||
reset({
|
||||
logsEnabled: false,
|
||||
metricsEnabled: false,
|
||||
s3BucketsByRegion: {},
|
||||
});
|
||||
}, [reset, serviceId]);
|
||||
|
||||
useEffect(() => {
|
||||
resetToConfig();
|
||||
}, [resetToConfig, serviceDetailsId]);
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig, serviceDetailsId]);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
useEffect(() => {
|
||||
if (serviceId) {
|
||||
logEvent(`${type} Integration: Service viewed`, {
|
||||
logEvent('AWS Integration: Service viewed', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
}
|
||||
}, [cloudAccountId, serviceId, type]);
|
||||
}, [cloudAccountId, serviceId]);
|
||||
|
||||
const { mutate: updateService, isLoading: isUpdatingServiceConfig } =
|
||||
useUpdateService();
|
||||
@@ -190,8 +121,8 @@ function ServiceDetails({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleDiscard = useCallback((): void => {
|
||||
resetToConfig();
|
||||
}, [resetToConfig]);
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: ServiceConfigFormValues): Promise<void> => {
|
||||
@@ -210,25 +141,25 @@ function ServiceDetails({
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceConfigPayload = getServiceConfigPayload({
|
||||
type,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
metricsEnabled,
|
||||
isLogsSupported,
|
||||
isMetricsSupported,
|
||||
s3BucketsByRegion: normalizedS3BucketsByRegion,
|
||||
});
|
||||
|
||||
updateService(
|
||||
{
|
||||
pathParams: {
|
||||
cloudProvider: type,
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
id: cloudAccountId,
|
||||
serviceId,
|
||||
},
|
||||
data: {
|
||||
config: serviceConfigPayload,
|
||||
config: {
|
||||
aws: {
|
||||
logs: {
|
||||
enabled: logsEnabled,
|
||||
s3Buckets: normalizedS3BucketsByRegion,
|
||||
},
|
||||
metrics: {
|
||||
enabled: metricsEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -239,7 +170,7 @@ function ServiceDetails({
|
||||
|
||||
const servicesListQueryKey = getListServicesMetadataQueryKey(
|
||||
{
|
||||
cloudProvider: type,
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
@@ -272,7 +203,7 @@ function ServiceDetails({
|
||||
invalidateGetService(
|
||||
queryClient,
|
||||
{
|
||||
cloudProvider: type,
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
serviceId,
|
||||
},
|
||||
{
|
||||
@@ -283,14 +214,14 @@ function ServiceDetails({
|
||||
invalidateListServicesMetadata(
|
||||
queryClient,
|
||||
{
|
||||
cloudProvider: type,
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
},
|
||||
);
|
||||
|
||||
logEvent(`${type} Integration: Service settings saved`, {
|
||||
logEvent('AWS Integration: Service settings saved', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
@@ -310,16 +241,7 @@ function ServiceDetails({
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
},
|
||||
[
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
updateService,
|
||||
queryClient,
|
||||
reset,
|
||||
type,
|
||||
isLogsSupported,
|
||||
isMetricsSupported,
|
||||
],
|
||||
[serviceId, cloudAccountId, updateService, queryClient, reset],
|
||||
);
|
||||
|
||||
if (isServiceDetailsLoading) {
|
||||
@@ -340,6 +262,10 @@ function ServiceDetails({
|
||||
const logsEnabled = watch('logsEnabled');
|
||||
const s3BucketsByRegion = watch('s3BucketsByRegion');
|
||||
|
||||
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
|
||||
const isMetricsSupported =
|
||||
serviceDetailsData?.supportedSignals?.metrics || false;
|
||||
|
||||
const hasUnsavedChanges = isDirty;
|
||||
|
||||
const isS3SyncBucketsMissing =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import cx from 'classnames';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
|
||||
import { Service } from './AmazonWebServices/types';
|
||||
import { Service } from './types';
|
||||
|
||||
function ServiceItem({
|
||||
service,
|
||||
@@ -4,20 +4,15 @@ import { Skeleton } from 'antd';
|
||||
import { useListServicesMetadata } from 'api/generated/services/cloudintegration';
|
||||
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import emptyStateIconUrl from '@/assets/Icons/emptyState.svg';
|
||||
|
||||
interface ServicesListProps {
|
||||
cloudAccountId: string;
|
||||
type: IntegrationType;
|
||||
}
|
||||
|
||||
function ServicesList({
|
||||
cloudAccountId,
|
||||
type,
|
||||
}: ServicesListProps): JSX.Element {
|
||||
function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const hasValidCloudAccountId = Boolean(cloudAccountId);
|
||||
@@ -27,7 +22,7 @@ function ServicesList({
|
||||
|
||||
const { data: servicesMetadata, isLoading } = useListServicesMetadata(
|
||||
{
|
||||
cloudProvider: type,
|
||||
cloudProvider: 'aws',
|
||||
},
|
||||
serviceQueryParams,
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServiceDetails from './ServiceDetails/ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection />
|
||||
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesList cloudAccountId={cloudAccountId} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -29,7 +29,7 @@ jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
|
||||
MarkdownRenderer: (): JSX.Element => <div data-testid="markdown-renderer" />,
|
||||
}));
|
||||
jest.mock(
|
||||
'container/Integrations/CloudIntegration/ServiceDashboards/ServiceDashboards',
|
||||
'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="service-dashboards" />,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import ServiceDetails from '../ServiceDetails/ServiceDetails';
|
||||
@@ -12,11 +11,10 @@ import { accountsResponse } from './mockData';
|
||||
const renderServiceDetails = (
|
||||
_initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
_serviceId = 's3sync',
|
||||
type: IntegrationType = IntegrationType.AWS_SERVICES,
|
||||
): RenderResult =>
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ServiceDetails type={type} />
|
||||
<ServiceDetails />
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { CloudAccount } from './types';
|
||||
|
||||
export function mapAccountDtoToAwsCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): CloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
regions: account.config?.aws?.regions ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { Button, Callout, DrawerWrapper, Tabs } from '@signozhq/ui';
|
||||
import { Form, Select, Spin } from 'antd';
|
||||
import { useGetAccount } from 'api/generated/services/cloudintegration';
|
||||
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import CodeBlock from 'components/CodeBlock/CodeBlock';
|
||||
import {
|
||||
AZURE_REGIONS,
|
||||
INTEGRATION_TYPES,
|
||||
} from 'container/Integrations/constants';
|
||||
import {
|
||||
IntegrationModalProps,
|
||||
ModalStateEnum,
|
||||
} from 'container/Integrations/HeroSection/types';
|
||||
import { LoaderCircle, SquareArrowOutUpRight } from 'lucide-react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { useIntegrationModal } from '../../../../../hooks/integration/azure/useIntegrationModal';
|
||||
import RenderConnectionFields from '../../AmazonWebServices/RegionForm/RenderConnectionParams';
|
||||
|
||||
import '../../AmazonWebServices/AddNewAccount/CloudAccountSetupModal.style.scss';
|
||||
|
||||
const AZURE_CLI_DESC =
|
||||
'Paste the following command if you have Azure CLI setup locally on your machine or use BASH CloudShell on Azure portal with above mentioned permissions.';
|
||||
const AZURE_POWERSHELL_DESC =
|
||||
'Paste the following command in PowerShell CloudShell on Azure portal, you can switch to PowerShell on Azure portal.';
|
||||
|
||||
function CloudAccountSetupModal({
|
||||
onClose,
|
||||
}: IntegrationModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
modalState,
|
||||
isLoading,
|
||||
accountId,
|
||||
connectionCommands,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
} = useIntegrationModal({ onClose });
|
||||
|
||||
const startTimeRef = useRef(Date.now());
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 10 * 60 * 1000;
|
||||
|
||||
const [isHowItWorksOpen, setIsHowItWorksOpen] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('cli');
|
||||
|
||||
useGetAccount(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
id: accountId ?? '',
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: Boolean(accountId) && modalState === ModalStateEnum.WAITING,
|
||||
refetchInterval,
|
||||
select: (response): CloudintegrationtypesAccountDTO => response.data,
|
||||
onSuccess: (account) => {
|
||||
const isConnected =
|
||||
Boolean(account.providerAccountId) && account.removedAt === null;
|
||||
|
||||
if (isConnected) {
|
||||
handleConnectionSuccess({
|
||||
cloudAccountId: account.providerAccountId ?? account.id,
|
||||
status: account.agentReport,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
handleConnectionTimeout({ id: accountId });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
handleConnectionError();
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const renderAlert = useCallback((): JSX.Element | null => {
|
||||
if (modalState === ModalStateEnum.WAITING) {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__alert">
|
||||
<Callout
|
||||
title={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<Spin
|
||||
indicator={
|
||||
<LoaderCircle
|
||||
size={14}
|
||||
className="anticon anticon-loading anticon-spin ant-spin-dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
Waiting for Azure account connection, retrying in{' '}
|
||||
<span className="retry-time">10</span> secs...
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (modalState === ModalStateEnum.ERROR) {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__alert">
|
||||
<Callout
|
||||
title={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
We couldn't establish a connection to your Azure account. Please
|
||||
try again
|
||||
</div>
|
||||
}
|
||||
type="error"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [modalState]);
|
||||
|
||||
const footer = (
|
||||
<div className="cloud-account-setup-modal__footer">
|
||||
{modalState === ModalStateEnum.FORM && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefix={<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
>
|
||||
Generate Azure Setup Commands
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
className="cloud-account-setup-modal"
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
title="Add Azure Account"
|
||||
width="wide"
|
||||
footer={footer}
|
||||
>
|
||||
<div className="cloud-account-setup-modal__content">
|
||||
<div className="cloud-account-setup-prerequisites">
|
||||
<div className="cloud-account-setup-prerequisites__title">
|
||||
Prerequisites
|
||||
</div>
|
||||
|
||||
<ul className="cloud-account-setup-prerequisites__list">
|
||||
<li className="cloud-account-setup-prerequisites__list-item">
|
||||
<span className="cloud-account-setup-prerequisites__list-item-bullet">
|
||||
—
|
||||
</span>{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-text">
|
||||
Ensure that you're logged in to the Azure workspace which you want
|
||||
to monitor.
|
||||
</span>
|
||||
</li>
|
||||
<li className="cloud-account-setup-prerequisites__list-item">
|
||||
<span className="cloud-account-setup-prerequisites__list-item-bullet">
|
||||
—
|
||||
</span>{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-text">
|
||||
Ensure that you either have the{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-highlight">
|
||||
Owner
|
||||
</span>{' '}
|
||||
role OR
|
||||
</span>
|
||||
</li>
|
||||
<li className="cloud-account-setup-prerequisites__list-item">
|
||||
<span className="cloud-account-setup-prerequisites__list-item-bullet">
|
||||
—
|
||||
</span>{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-text">
|
||||
Both the{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-highlight">
|
||||
Contributor
|
||||
</span>{' '}
|
||||
and{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-highlight">
|
||||
user access admin
|
||||
</span>{' '}
|
||||
roles
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="cloud-account-setup-how-it-works-accordion">
|
||||
<div
|
||||
className={`cloud-account-setup-how-it-works-accordion__title ${
|
||||
isHowItWorksOpen ? 'open' : ''
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
onClick={(): void => setIsHowItWorksOpen(!isHowItWorksOpen)}
|
||||
prefix={isHowItWorksOpen ? <ChevronDown /> : <ChevronRight />}
|
||||
/>
|
||||
|
||||
<span className="cloud-account-setup-how-it-works-accordion__title-text">
|
||||
How it works?
|
||||
</span>
|
||||
</div>
|
||||
{isHowItWorksOpen && (
|
||||
<div className="cloud-account-setup-how-it-works-accordion__description">
|
||||
<div className="cloud-account-setup-how-it-works-accordion__description-item">
|
||||
SigNoz will create new resource-group to manage the resources required
|
||||
for this integration. The following steps will create a User-Assigned
|
||||
Managed Identity with the necessary permissions and follows the
|
||||
Principle of Least Privilege.
|
||||
</div>
|
||||
<div className="cloud-account-setup-how-it-works__description-item">
|
||||
Once the Integration template is deployed, you can enable the services
|
||||
you want to monitor right here in Signoz dashboard.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
className="cloud-account-setup-form"
|
||||
layout="vertical"
|
||||
initialValues={{ resourceGroups: [] }}
|
||||
>
|
||||
<div className="cloud-account-setup-form__content">
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
<div className="cloud-account-setup-form__title">
|
||||
Where should we deploy the SigNoz collector resources?
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__description">
|
||||
Choose the Azure region for deployment.
|
||||
</div>
|
||||
<Form.Item
|
||||
name="region"
|
||||
rules={[{ required: true, message: 'Please select a region' }]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<Select
|
||||
placeholder="e.g. East US"
|
||||
options={AZURE_REGIONS.map((region) => ({
|
||||
label: `${region.label} (${region.value})`,
|
||||
value: region.value,
|
||||
}))}
|
||||
getPopupContainer={popupContainer}
|
||||
disabled={modalState === ModalStateEnum.WAITING}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
<div className="cloud-account-setup-form__title">
|
||||
Which resource groups do you want to monitor?
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__description">
|
||||
Add one or more Azure resource group names.
|
||||
</div>
|
||||
<Form.Item
|
||||
name="resourceGroups"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: 'Please add at least one resource group',
|
||||
},
|
||||
]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="e.g. prod-platform-rg"
|
||||
tokenSeparators={[',']}
|
||||
disabled={modalState === ModalStateEnum.WAITING}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<RenderConnectionFields
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
connectionParams={connectionParams}
|
||||
isFormDisabled={modalState === ModalStateEnum.WAITING}
|
||||
/>
|
||||
|
||||
{connectionCommands && (
|
||||
<div className="cloud-account-setup-form__code-block-tabs-container">
|
||||
<div className="cloud-account-setup-form__code-block-tabs-header">
|
||||
<div className="cloud-account-setup-form__code-block-tabs-header-title">
|
||||
Deploy Agent
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__code-block-tabs-header-description">
|
||||
{activeTab === 'cli' ? AZURE_CLI_DESC : AZURE_POWERSHELL_DESC}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
className="cloud-account-setup-form__code-block-tabs"
|
||||
items={[
|
||||
{
|
||||
key: 'cli',
|
||||
label: 'CLI',
|
||||
children: <CodeBlock code={connectionCommands?.cliCommand || ''} />,
|
||||
},
|
||||
{
|
||||
key: 'powershell',
|
||||
label: 'PowerShell',
|
||||
children: (
|
||||
<CodeBlock
|
||||
code={connectionCommands?.cloudPowerShellCommand || ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={(key): void => setActiveTab(key)}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderAlert()}
|
||||
|
||||
{modalState === ModalStateEnum.WAITING && (
|
||||
<div className="cloud-account-setup-status-message">
|
||||
After running the command, return here and wait for automatic connection
|
||||
detection.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudAccountSetupModal;
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button, DrawerWrapper } from '@signozhq/ui';
|
||||
import { Form, Select } from 'antd';
|
||||
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
import { useAccountSettingsModal } from '../../../../../hooks/integration/azure/useAccountSettingsModal';
|
||||
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
|
||||
|
||||
import '../../AmazonWebServices/EditAccount/AccountSettingsModal.style.scss';
|
||||
|
||||
interface AccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
resourceGroups,
|
||||
isSaveDisabled,
|
||||
setResourceGroups,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const azureConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
className="account-settings-modal"
|
||||
title="Account Settings"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
width="wide"
|
||||
footer={
|
||||
<div className="account-settings-modal__footer">
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={(): void => {
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
});
|
||||
setActiveAccount(null);
|
||||
handleClose();
|
||||
}}
|
||||
cloudProvider={INTEGRATION_TYPES.AZURE}
|
||||
/>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
disabled={isSaveDisabled}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
prefix={<Save size={14} />}
|
||||
>
|
||||
Update Changes
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
resourceGroups: azureConfig?.resource_groups || [],
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
Azure Subscription:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.providerAccountId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{azureConfig?.deployment_region && (
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Deployment region
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
{azureConfig.deployment_region}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Resource groups
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
Update the resource groups that should be monitored.
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="resourceGroups"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: 'Please add at least one resource group',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={resourceGroups}
|
||||
onChange={(values): void => {
|
||||
setResourceGroups(values);
|
||||
form.setFieldValue('resourceGroups', values);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsModal;
|
||||
@@ -1,15 +1,16 @@
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
|
||||
import AWSTabs from './AmazonWebServices/ServicesTabs';
|
||||
import Header from './Header/Header';
|
||||
import ServicesTabs from './ServiceTabs/ServicesTabs';
|
||||
|
||||
import './CloudIntegration.styles.scss';
|
||||
|
||||
const CloudIntegration = ({ type }: { type: IntegrationType }): JSX.Element => {
|
||||
return (
|
||||
<div className="cloud-integration-container">
|
||||
<Header type={type} />
|
||||
<ServicesTabs type={type} />
|
||||
<Header title={type} />
|
||||
|
||||
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Blocks, LifeBuoy } from 'lucide-react';
|
||||
|
||||
import './Header.styles.scss';
|
||||
|
||||
function Header({ type }: { type: IntegrationType }): JSX.Element {
|
||||
function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-header">
|
||||
<div className="cloud-header__navigation">
|
||||
@@ -25,30 +25,27 @@ function Header({ type }: { type: IntegrationType }): JSX.Element {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div className="cloud-header__breadcrumb-title">{type}</div>,
|
||||
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{type === IntegrationType.AWS_SERVICES && (
|
||||
<div className="cloud-header__actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
prefix={<LifeBuoy size={12} />}
|
||||
>
|
||||
Get Help
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="cloud-header__actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
prefix={<LifeBuoy size={12} />}
|
||||
>
|
||||
Get Help
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import HeroSection from '../../HeroSection/HeroSection';
|
||||
import ServiceDetails from '../AmazonWebServices/ServiceDetails/ServiceDetails';
|
||||
import ServicesList from '../ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
function ServicesTabs({ type }: { type: IntegrationType }): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection type={type} />
|
||||
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesList cloudAccountId={cloudAccountId} type={type} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails type={type} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { CloudAccount as IntegrationCloudAccount } from 'container/Integrations/types';
|
||||
|
||||
import { CloudAccount as AwsCloudAccount } from './AmazonWebServices/types';
|
||||
|
||||
export function mapAccountDtoToAwsCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): AwsCloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
regions: account.config?.aws?.regions ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAccountDtoToAzureCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): IntegrationCloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
deployment_region: account.config?.azure?.deploymentRegion ?? '',
|
||||
resource_groups: account.config?.azure?.resourceGroups ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
@@ -1,32 +1,5 @@
|
||||
import { ONE_CLICK_INTEGRATIONS } from '../constants';
|
||||
import { IntegrationType } from '../types';
|
||||
|
||||
export const getAccountById = <T extends { cloud_account_id: string }>(
|
||||
accounts: T[],
|
||||
accountId: string,
|
||||
): T | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
|
||||
interface IntegrationMetadata {
|
||||
title: string;
|
||||
description: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export const getIntegrationMetadata = (
|
||||
type: IntegrationType,
|
||||
): IntegrationMetadata => {
|
||||
const integration = ONE_CLICK_INTEGRATIONS.find(
|
||||
(integration) => integration.id === type,
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
return { title: '', description: '', logo: '' };
|
||||
}
|
||||
|
||||
return {
|
||||
title: integration.title,
|
||||
description: integration.description,
|
||||
logo: integration.icon,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
|
||||
import AccountActions from '../CloudIntegration/AmazonWebServices/AccountActions/AccountActions';
|
||||
import { getIntegrationMetadata } from '../CloudIntegration/utils';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection({ type }: { type: IntegrationType }): JSX.Element {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
logo: integrationLogo,
|
||||
} = getIntegrationMetadata(type);
|
||||
|
||||
return (
|
||||
<div className="hero-section">
|
||||
<div className="hero-section__details">
|
||||
<div className="hero-section__details-header">
|
||||
<div className="hero-section__icon">
|
||||
<img src={integrationLogo} alt={type} />
|
||||
</div>
|
||||
|
||||
<div className="hero-section__details-title">{title}</div>
|
||||
</div>
|
||||
<div className="hero-section__details-description">{description}</div>
|
||||
</div>
|
||||
|
||||
<AccountActions type={type} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -9,6 +9,53 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
gap: 15px;
|
||||
|
||||
.error-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.retry-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contact-support {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.text {
|
||||
color: var(--callout-primary-description);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state-svg {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-integration-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -280,36 +327,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
gap: 15px;
|
||||
|
||||
.error-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-state-svg {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remove-integration-modal {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Flex, Skeleton, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -54,19 +55,8 @@ function IntegrationDetailPage(): JSX.Element {
|
||||
),
|
||||
);
|
||||
|
||||
if (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
) {
|
||||
return (
|
||||
<CloudIntegration
|
||||
type={
|
||||
integrationId === INTEGRATION_TYPES.AWS
|
||||
? IntegrationType.AWS_SERVICES
|
||||
: IntegrationType.AZURE_SERVICES
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (integrationId === INTEGRATION_TYPES.AWS) {
|
||||
return <CloudIntegration type={IntegrationType.AWS_SERVICES} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -95,20 +85,20 @@ function IntegrationDetailPage(): JSX.Element {
|
||||
<div className="error-btns">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
color="primary"
|
||||
onClick={(): Promise<any> => refetch()}
|
||||
prefix={<RotateCw size={14} />}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
<div
|
||||
className="contact-support"
|
||||
onClick={(): void => handleContactSupport(isCloudUserVal)}
|
||||
suffix={<MoveUpRight size={12} />}
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
<Typography.Link className="text">Contact Support </Typography.Link>
|
||||
|
||||
<MoveUpRight size={14} color={Color.BG_ROBIN_400} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,6 @@ function OneClickIntegrations(props: OneClickIntegrationsProps): JSX.Element {
|
||||
if (!query) {
|
||||
return ONE_CLICK_INTEGRATIONS;
|
||||
}
|
||||
|
||||
return ONE_CLICK_INTEGRATIONS.filter(
|
||||
(integration) =>
|
||||
integration.title.toLowerCase().includes(query) ||
|
||||
|
||||
@@ -14,8 +14,8 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
'Integrations Detail Page: Clicked remove Integration button for integration',
|
||||
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
|
||||
'Integrations Detail Page: Navigated to configure an integration',
|
||||
INTEGRATION_ACCOUNT_REMOVED:
|
||||
'Integration Detail page: Clicked remove Integration button for integration',
|
||||
AWS_INTEGRATION_ACCOUNT_REMOVED:
|
||||
'AWS Integration Detail page: Clicked remove Integration button for integration',
|
||||
};
|
||||
|
||||
export const INTEGRATION_TYPES = {
|
||||
@@ -53,7 +53,7 @@ export const AZURE_INTEGRATION = {
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION, AZURE_INTEGRATION];
|
||||
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION];
|
||||
|
||||
export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{
|
||||
@@ -81,7 +81,6 @@ export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{ label: 'Central India', value: 'centralindia', geography: 'India' },
|
||||
{ label: 'Central US', value: 'centralus', geography: 'United States' },
|
||||
{ label: 'Chile Central', value: 'chilecentral', geography: 'Chile' },
|
||||
{ label: 'Denmark East', value: 'denmarkeast', geography: 'Denmark' },
|
||||
{ label: 'East Asia', value: 'eastasia', geography: 'Asia Pacific' },
|
||||
{ label: 'East US', value: 'eastus', geography: 'United States' },
|
||||
{ label: 'East US 2', value: 'eastus2', geography: 'United States' },
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
} from './CloudIntegration/AmazonWebServices/types';
|
||||
|
||||
export enum IntegrationType {
|
||||
AWS_SERVICES = 'aws',
|
||||
AZURE_SERVICES = 'azure',
|
||||
AWS_SERVICES = 'aws-services',
|
||||
AZURE_SERVICES = 'azure-services',
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
@@ -89,7 +89,6 @@ export interface CloudAccount {
|
||||
cloud_account_id: string;
|
||||
config: AzureCloudAccountConfig | AWSCloudAccountConfig;
|
||||
status: AccountStatus | IServiceStatus;
|
||||
providerAccountId: string;
|
||||
}
|
||||
|
||||
export interface AzureCloudAccountConfig {
|
||||
|
||||
@@ -196,7 +196,6 @@ export const getUplotClickData = ({
|
||||
coord: { x: number; y: number };
|
||||
record: { queryName: string; filters: FilterData[] };
|
||||
label: string | React.ReactNode;
|
||||
seriesColor?: string;
|
||||
} | null => {
|
||||
if (!queryData?.queryName || !metric) {
|
||||
return null;
|
||||
@@ -209,8 +208,6 @@ export const getUplotClickData = ({
|
||||
|
||||
// Generate label from focusedSeries data
|
||||
let label: string | React.ReactNode = '';
|
||||
const seriesColor = focusedSeries?.color;
|
||||
|
||||
if (focusedSeries && focusedSeries.seriesName) {
|
||||
label = (
|
||||
<span style={{ color: focusedSeries.color }}>
|
||||
@@ -226,7 +223,6 @@ export const getUplotClickData = ({
|
||||
},
|
||||
record,
|
||||
label,
|
||||
seriesColor,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -241,7 +237,6 @@ export const getPieChartClickData = (
|
||||
queryName: string;
|
||||
filters: FilterData[];
|
||||
label: string | React.ReactNode;
|
||||
seriesColor?: string;
|
||||
} | null => {
|
||||
const { metric, queryName } = arc.data.record;
|
||||
if (!queryName || !metric) {
|
||||
@@ -253,7 +248,6 @@ export const getPieChartClickData = (
|
||||
queryName,
|
||||
filters: getFiltersFromMetric(metric), // TODO: add where clause query as well.
|
||||
label,
|
||||
seriesColor: arc.data.color,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface AggregateData {
|
||||
endTime: number;
|
||||
};
|
||||
label?: string | React.ReactNode;
|
||||
seriesColor?: string;
|
||||
}
|
||||
|
||||
const useAggregateDrilldown = ({
|
||||
|
||||
@@ -228,13 +228,7 @@ const useBaseAggregateOptions = ({
|
||||
return (
|
||||
<ContextMenu.Item
|
||||
key={key}
|
||||
icon={
|
||||
isLoading ? (
|
||||
<LoadingOutlined spin />
|
||||
) : (
|
||||
<span style={{ color: aggregateData?.seriesColor }}>{icon}</span>
|
||||
)
|
||||
}
|
||||
icon={isLoading ? <LoadingOutlined spin /> : icon}
|
||||
onClick={(): void => onClick()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
||||
@@ -7,13 +7,6 @@ import {
|
||||
GetIntegrationStatusProps,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
export function isOneClickIntegration(integrationId: string): boolean {
|
||||
return (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
);
|
||||
}
|
||||
|
||||
export const useGetIntegrationStatus = ({
|
||||
integrationId,
|
||||
}: GetIntegrationPayloadProps): UseQueryResult<
|
||||
@@ -27,5 +20,5 @@ export const useGetIntegrationStatus = ({
|
||||
enabled:
|
||||
!!integrationId &&
|
||||
integrationId !== '' &&
|
||||
!isOneClickIntegration(integrationId),
|
||||
integrationId !== INTEGRATION_TYPES.AWS,
|
||||
});
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
CloudintegrationtypesCredentialsDTO,
|
||||
CloudintegrationtypesPostableAccountDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import {
|
||||
ActiveViewEnum,
|
||||
ModalStateEnum,
|
||||
} from 'container/Integrations/HeroSection/types';
|
||||
} from 'container/Integrations/CloudIntegration/AmazonWebServices/HeroSection/types';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Form } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { useUpdateAccount } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseAccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
interface UseAccountSettingsModal {
|
||||
form: FormInstance;
|
||||
isLoading: boolean;
|
||||
resourceGroups: string[];
|
||||
isSaveDisabled: boolean;
|
||||
setResourceGroups: Dispatch<SetStateAction<string[]>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export function useAccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: UseAccountSettingsModalProps): UseAccountSettingsModal {
|
||||
const [form] = Form.useForm();
|
||||
const { mutate: updateAccount, isLoading } = useUpdateAccount();
|
||||
const accountConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
const [resourceGroups, setResourceGroups] = useState<string[]>(
|
||||
accountConfig?.resource_groups || [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
region: accountConfig.deployment_region,
|
||||
resourceGroups: accountConfig.resource_groups,
|
||||
});
|
||||
setResourceGroups(accountConfig.resource_groups);
|
||||
}, [accountConfig, form]);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
updateAccount(
|
||||
{
|
||||
pathParams: {
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
id: account?.id || '',
|
||||
},
|
||||
data: {
|
||||
config: {
|
||||
azure: {
|
||||
resourceGroups: values.resourceGroups || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
const nextConfig = {
|
||||
deployment_region: accountConfig?.deployment_region || '',
|
||||
resource_groups: values.resourceGroups || [],
|
||||
};
|
||||
|
||||
setActiveAccount({
|
||||
...account,
|
||||
config: nextConfig,
|
||||
});
|
||||
onClose();
|
||||
|
||||
toast.success('Account settings updated successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
|
||||
logEvent('Azure Integration: Account settings updated', {
|
||||
cloudAccountId: account.cloud_account_id,
|
||||
deploymentRegion: nextConfig.deployment_region,
|
||||
resourceGroups: nextConfig.resource_groups,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to update account settings', {
|
||||
description: error?.message,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
}, [form, updateAccount, account, setActiveAccount, onClose]);
|
||||
|
||||
const isSaveDisabled = useMemo(() => {
|
||||
if (!accountConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const formResourceGroups = resourceGroups || [];
|
||||
|
||||
return isEqual(
|
||||
[...formResourceGroups].sort(),
|
||||
[...accountConfig.resource_groups].sort(),
|
||||
);
|
||||
}, [accountConfig, resourceGroups, form]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading,
|
||||
resourceGroups,
|
||||
isSaveDisabled,
|
||||
setResourceGroups,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
};
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Form, FormInstance } from 'antd';
|
||||
import {
|
||||
CreateAccountMutationResult,
|
||||
GetConnectionCredentialsQueryResult,
|
||||
invalidateListAccounts,
|
||||
useCreateAccount,
|
||||
useGetConnectionCredentials,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
import {
|
||||
CloudintegrationtypesCredentialsDTO,
|
||||
CloudintegrationtypesPostableAccountDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { ModalStateEnum } from 'container/Integrations/HeroSection/types';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseIntegrationModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface UseAzureIntegrationModal {
|
||||
form: FormInstance;
|
||||
modalState: ModalStateEnum;
|
||||
isLoading: boolean;
|
||||
accountId?: string;
|
||||
connectionCommands: {
|
||||
cliCommand: string;
|
||||
cloudPowerShellCommand: string;
|
||||
} | null;
|
||||
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO;
|
||||
isConnectionParamsLoading: boolean;
|
||||
handleConnectionSuccess: (payload: {
|
||||
cloudAccountId: string;
|
||||
status?: unknown;
|
||||
}) => void;
|
||||
handleConnectionTimeout: (payload: { id?: string }) => void;
|
||||
handleConnectionError: () => void;
|
||||
}
|
||||
|
||||
export function useIntegrationModal({
|
||||
onClose,
|
||||
}: UseIntegrationModalProps): UseAzureIntegrationModal {
|
||||
const queryClient = useQueryClient();
|
||||
const [form] = Form.useForm();
|
||||
const [modalState, setModalState] = useState<ModalStateEnum>(
|
||||
ModalStateEnum.FORM,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [accountId, setAccountId] = useState<string | undefined>(undefined);
|
||||
const [connectionCommands, setConnectionCommands] = useState<{
|
||||
cliCommand: string;
|
||||
cloudPowerShellCommand: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setModalState(ModalStateEnum.FORM);
|
||||
setConnectionCommands(null);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const handleConnectionSuccess = useCallback(
|
||||
(payload: { cloudAccountId: string; status?: unknown }): void => {
|
||||
logEvent('Azure Integration: Account connected', {
|
||||
cloudAccountId: payload.cloudAccountId,
|
||||
status: payload.status,
|
||||
});
|
||||
toast.success('Azure account connected successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
[handleClose, queryClient],
|
||||
);
|
||||
|
||||
const handleConnectionTimeout = useCallback(
|
||||
(payload: { id?: string }): void => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
logEvent('Azure Integration: Account connection attempt timed out', {
|
||||
id: payload.id,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleConnectionError = useCallback((): void => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
}, []);
|
||||
|
||||
const { mutate: createAccount } = useCreateAccount();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { data: connectionParams, isLoading: isConnectionParamsLoading } =
|
||||
useGetConnectionCredentials<GetConnectionCredentialsQueryResult>(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
onError: handleError,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const values = await form.validateFields();
|
||||
|
||||
const payload: CloudintegrationtypesPostableAccountDTO = {
|
||||
config: {
|
||||
azure: {
|
||||
deploymentRegion: values.region,
|
||||
resourceGroups: values.resourceGroups || [],
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
ingestionUrl: connectionParams?.data?.ingestionUrl || values.ingestionUrl,
|
||||
ingestionKey: connectionParams?.data?.ingestionKey || values.ingestionKey,
|
||||
sigNozApiUrl: connectionParams?.data?.sigNozApiUrl || values.sigNozApiUrl,
|
||||
sigNozApiKey: connectionParams?.data?.sigNozApiKey || values.sigNozApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
createAccount(
|
||||
{
|
||||
pathParams: { cloudProvider: INTEGRATION_TYPES.AZURE },
|
||||
data: payload,
|
||||
},
|
||||
{
|
||||
onSuccess: (response: CreateAccountMutationResult) => {
|
||||
const nextAccountId = response.data.id;
|
||||
const artifact = response.data.connectionArtifact.azure;
|
||||
|
||||
logEvent('Azure Integration: Account connection commands generated', {
|
||||
id: nextAccountId,
|
||||
});
|
||||
|
||||
setConnectionCommands({
|
||||
cliCommand: artifact?.cliCommand || '',
|
||||
cloudPowerShellCommand: artifact?.cloudPowerShellCommand || '',
|
||||
});
|
||||
setModalState(ModalStateEnum.WAITING);
|
||||
setAccountId(nextAccountId);
|
||||
},
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
toast.error('Failed to create account connection', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [form, connectionParams, createAccount]);
|
||||
|
||||
return {
|
||||
form,
|
||||
modalState,
|
||||
isLoading,
|
||||
accountId,
|
||||
connectionCommands,
|
||||
setModalState,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
connectionParams: connectionParams?.data as
|
||||
| CloudintegrationtypesCredentialsDTO
|
||||
| undefined,
|
||||
isConnectionParamsLoading,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--muted-foreground);
|
||||
color: var(--foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
@@ -20,10 +20,13 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover,
|
||||
&:hover {
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: var(--l2-background-hover);
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@@ -44,8 +47,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--danger-background);
|
||||
color: var(--l1-foreground);
|
||||
background-color: var(--bg-cherry-100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,24 +74,73 @@
|
||||
}
|
||||
|
||||
.context-menu-header {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
// Target the popover inner specifically for context menu
|
||||
.context-menu .ant-popover-inner {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
max-width: 300px;
|
||||
background: var(--l2-background) !important;
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
padding: 12px 8px !important;
|
||||
// max-height: 254px !important;
|
||||
max-width: 300px !important;
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
.darkMode {
|
||||
.context-menu-item {
|
||||
color: var(--muted-foreground);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--l2-background);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--bg-cherry-400);
|
||||
|
||||
.icon {
|
||||
color: var(--bg-cherry-400);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--danger-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-header {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
// Set the menu popover background
|
||||
.context-menu .ant-popover-inner {
|
||||
background: var(--l1-background) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu backdrop overlay
|
||||
.context-menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
|
||||
// Prevent any pointer events from reaching elements behind
|
||||
pointer-events: auto;
|
||||
|
||||
// Ensure it covers the entire viewport including any scrollable areas
|
||||
position: fixed !important;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
@@ -51,6 +52,7 @@ type provider struct {
|
||||
flaggerHandler flagger.Handler
|
||||
dashboardModule dashboard.Module
|
||||
dashboardHandler dashboard.Handler
|
||||
systemDashboardHandler systemdashboard.Handler
|
||||
metricsExplorerHandler metricsexplorer.Handler
|
||||
infraMonitoringHandler inframonitoring.Handler
|
||||
gatewayHandler gateway.Handler
|
||||
@@ -82,6 +84,7 @@ func NewFactory(
|
||||
flaggerHandler flagger.Handler,
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
systemDashboardHandler systemdashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.Handler,
|
||||
infraMonitoringHandler inframonitoring.Handler,
|
||||
gatewayHandler gateway.Handler,
|
||||
@@ -116,6 +119,7 @@ func NewFactory(
|
||||
flaggerHandler,
|
||||
dashboardModule,
|
||||
dashboardHandler,
|
||||
systemDashboardHandler,
|
||||
metricsExplorerHandler,
|
||||
infraMonitoringHandler,
|
||||
gatewayHandler,
|
||||
@@ -152,6 +156,7 @@ func newProvider(
|
||||
flaggerHandler flagger.Handler,
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
systemDashboardHandler systemdashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.Handler,
|
||||
infraMonitoringHandler inframonitoring.Handler,
|
||||
gatewayHandler gateway.Handler,
|
||||
@@ -186,6 +191,7 @@ func newProvider(
|
||||
flaggerHandler: flaggerHandler,
|
||||
dashboardModule: dashboardModule,
|
||||
dashboardHandler: dashboardHandler,
|
||||
systemDashboardHandler: systemDashboardHandler,
|
||||
metricsExplorerHandler: metricsExplorerHandler,
|
||||
infraMonitoringHandler: infraMonitoringHandler,
|
||||
gatewayHandler: gatewayHandler,
|
||||
@@ -254,6 +260,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addSystemDashboardRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addMetricsExplorerRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
66
pkg/apiserver/signozapiserver/systemdashboard.go
Normal file
66
pkg/apiserver/signozapiserver/systemdashboard.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
)
|
||||
|
||||
func (provider *provider) addSystemDashboardRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/system/{source}/dashboard", handler.New(provider.authZ.ViewAccess(provider.systemDashboardHandler.Get), handler.OpenAPIDef{
|
||||
ID: "GetSystemDashboard",
|
||||
Tags: []string{"system-dashboard"},
|
||||
Summary: "Get system dashboard",
|
||||
Description: "This endpoint returns the system dashboard for the callers org keyed by source (e.g. ai-o11y-overview).",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettableSystemDashboard),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/system/{source}/dashboard", handler.New(provider.authZ.EditAccess(provider.systemDashboardHandler.Update), handler.OpenAPIDef{
|
||||
ID: "UpdateSystemDashboard",
|
||||
Tags: []string{"system-dashboard"},
|
||||
Summary: "Update system dashboard",
|
||||
Description: "This endpoint replaces the system dashboard for the callers org with the provided payload.",
|
||||
Request: new(dashboardtypes.UpdatableSystemDashboard),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableSystemDashboard),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/system/{source}/dashboard/reset", handler.New(provider.authZ.EditAccess(provider.systemDashboardHandler.Reset), handler.OpenAPIDef{
|
||||
ID: "ResetSystemDashboard",
|
||||
Tags: []string{"system-dashboard"},
|
||||
Summary: "Reset system dashboard to defaults",
|
||||
Description: "This endpoint drops any customisation to the system dashboard, writes the defaults back, and returns the freshly seeded dashboard.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettableSystemDashboard),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func newConfig() factory.Config {
|
||||
Agent: AgentConfig{
|
||||
// we will maintain the latest version of cloud integration agent from here,
|
||||
// till we automate it externally or figure out a way to validate it.
|
||||
Version: "v0.0.10",
|
||||
Version: "v0.0.9",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func NewStore(sqlstore sqlstore.SQLStore) dashboardtypes.Store {
|
||||
func (store *store) Create(ctx context.Context, storabledashboard *dashboardtypes.StorableDashboard) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(storabledashboard).
|
||||
Exec(ctx)
|
||||
@@ -55,6 +55,7 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
|
||||
Model(storableDashboard).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", "").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
|
||||
@@ -63,6 +64,23 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
|
||||
return storableDashboard, nil
|
||||
}
|
||||
|
||||
func (store *store) GetBySource(ctx context.Context, orgID valuer.UUID, source string) (*dashboardtypes.StorableDashboard, error) {
|
||||
storableDashboard := new(dashboardtypes.StorableDashboard)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(storableDashboard).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", source).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "system dashboard with source %s doesn't exist", source)
|
||||
}
|
||||
|
||||
return storableDashboard, nil
|
||||
}
|
||||
|
||||
func (store *store) GetPublic(ctx context.Context, dashboardID string) (*dashboardtypes.StorablePublicDashboard, error) {
|
||||
storable := new(dashboardtypes.StorablePublicDashboard)
|
||||
err := store.
|
||||
@@ -124,6 +142,7 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardty
|
||||
NewSelect().
|
||||
Model(&storableDashboards).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", "").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -158,6 +177,7 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashb
|
||||
Model(storableDashboard).
|
||||
WherePK().
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", "").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", storableDashboard.ID)
|
||||
@@ -166,6 +186,27 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashb
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateBySource updates the org's system dashboard for the given source.(org_id, source)
|
||||
// Delete drops and re-creates the row with a new id, last id from a prior Get should not cause this update to match zero rows.
|
||||
func (store *store) UpdateBySource(ctx context.Context, orgID valuer.UUID, source string, storableDashboard *dashboardtypes.StorableDashboard) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model(storableDashboard).
|
||||
Set("data = ?", storableDashboard.Data).
|
||||
Set("updated_by = ?", storableDashboard.UpdatedBy).
|
||||
Set("updated_at = ?", storableDashboard.UpdatedAt).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", source).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "system dashboard with source %s doesn't exist", source)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) UpdatePublic(ctx context.Context, storable *dashboardtypes.StorablePublicDashboard) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
@@ -189,6 +230,7 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
|
||||
Model(new(dashboardtypes.StorableDashboard)).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", "").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
|
||||
@@ -197,6 +239,22 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) DeleteBySource(ctx context.Context, orgID valuer.UUID, source string) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(dashboardtypes.StorableDashboard)).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", source).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "system dashboard with source %s doesn't exist", source)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) DeletePublic(ctx context.Context, dashboardID string) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
|
||||
@@ -6,18 +6,20 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type setter struct {
|
||||
store types.OrganizationStore
|
||||
alertmanager alertmanager.Alertmanager
|
||||
quickfilter quickfilter.Module
|
||||
store types.OrganizationStore
|
||||
alertmanager alertmanager.Alertmanager
|
||||
quickfilter quickfilter.Module
|
||||
systemDashboard systemdashboard.Module
|
||||
}
|
||||
|
||||
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
|
||||
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
|
||||
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, systemDashboard systemdashboard.Module) organization.Setter {
|
||||
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, systemDashboard: systemDashboard}
|
||||
}
|
||||
|
||||
func (module *setter) Create(ctx context.Context, organization *types.Organization, createManagedRoles func(context.Context, valuer.UUID) error) error {
|
||||
@@ -33,6 +35,10 @@ func (module *setter) Create(ctx context.Context, organization *types.Organizati
|
||||
return err
|
||||
}
|
||||
|
||||
if err := module.systemDashboard.SetDefaultConfig(ctx, organization.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := createManagedRoles(ctx, organization.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
138
pkg/modules/systemdashboard/implsystemdashboard/handler.go
Normal file
138
pkg/modules/systemdashboard/implsystemdashboard/handler.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package implsystemdashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module systemdashboard.Module
|
||||
}
|
||||
|
||||
func NewHandler(module systemdashboard.Module) systemdashboard.Handler {
|
||||
return &handler{module: module}
|
||||
}
|
||||
|
||||
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
source, err := parseSource(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Get(ctx, orgID, source)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableSystemDashboardFromDashboard(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
source, err := parseSource(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.UpdatableSystemDashboard{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Data == nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "data is required"))
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Update(ctx, orgID, source, claims.Email, req.Data)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableSystemDashboardFromDashboard(dashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) Reset(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
source, err := parseSource(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Reset(ctx, orgID, source)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewGettableSystemDashboardFromDashboard(dashboard))
|
||||
}
|
||||
|
||||
func parseSource(r *http.Request) (dashboardtypes.Source, error) {
|
||||
source := mux.Vars(r)["source"]
|
||||
if source == "" {
|
||||
return dashboardtypes.Source{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "source is missing in the path")
|
||||
}
|
||||
|
||||
return dashboardtypes.NewSource(source)
|
||||
}
|
||||
120
pkg/modules/systemdashboard/implsystemdashboard/module.go
Normal file
120
pkg/modules/systemdashboard/implsystemdashboard/module.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package implsystemdashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store dashboardtypes.Store
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store) systemdashboard.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error) {
|
||||
storableDashboard, err := module.store.GetBySource(ctx, orgID, source.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
|
||||
}
|
||||
|
||||
// Update applies the new payload as last-writer-wins. The Get and Update run inside one transaction so a
|
||||
// concurrent Reset cannot interleave and leave the response with a stale id from before the reset.
|
||||
func (module *module) Update(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source, updatedBy string, data dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||
var updated *dashboardtypes.Dashboard
|
||||
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
storableDashboard, err := module.store.GetBySource(ctx, orgID, source.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storableDashboard.Data = data
|
||||
storableDashboard.UpdatedBy = updatedBy
|
||||
storableDashboard.UpdatedAt = time.Now()
|
||||
|
||||
if err := module.store.UpdateBySource(ctx, orgID, source.StringValue(), storableDashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updated = dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// Reset drops the org's customized system dashboard for the given source and writes the default back in a single transaction. Returns the freshly seeded dashboard so callers can render it without a follow-up Get. A NotFound from the delete is treated as "nothing to drop, proceed with seed" so Reset is self-healing when a row was lost (e.g. hand-deleted in SQL or a prior partial seed).
|
||||
func (module *module) Reset(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error) {
|
||||
var resetDashboard *dashboardtypes.Dashboard
|
||||
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.DeleteBySource(ctx, orgID, source.StringValue()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultDashboard, err := dashboardtypes.NewDefaultSystemDashboard(orgID, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storable, err := dashboardtypes.NewStorableDashboardFromDashboard(defaultDashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := module.store.Create(ctx, storable); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resetDashboard = defaultDashboard
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resetDashboard, nil
|
||||
}
|
||||
|
||||
func (module *module) SetDefaultConfig(ctx context.Context, orgID valuer.UUID) error {
|
||||
for _, source := range dashboardtypes.AllSources {
|
||||
if err := module.setDefaultForSource(ctx, orgID, source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) setDefaultForSource(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) error {
|
||||
existing, err := module.store.GetBySource(ctx, orgID, source.StringValue())
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
if existing != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dashboard, err := dashboardtypes.NewDefaultSystemDashboard(orgID, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.Create(ctx, storableDashboard)
|
||||
}
|
||||
25
pkg/modules/systemdashboard/systemdashboard.go
Normal file
25
pkg/modules/systemdashboard/systemdashboard.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package systemdashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
Get(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error)
|
||||
Update(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source, updatedBy string, data dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error)
|
||||
Reset(ctx context.Context, orgID valuer.UUID, source dashboardtypes.Source) (*dashboardtypes.Dashboard, error)
|
||||
SetDefaultConfig(ctx context.Context, orgID valuer.UUID) error
|
||||
}
|
||||
|
||||
// Handler defines the HTTP handler interface for system dashboard endpoints.
|
||||
// /api/v1/system/{source}/dashboard — Get / Update
|
||||
// /api/v1/system/{source}/dashboard/reset — Reset.
|
||||
type Handler interface {
|
||||
Get(http.ResponseWriter, *http.Request)
|
||||
Update(http.ResponseWriter, *http.Request)
|
||||
Reset(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
@@ -40,6 +40,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper/implspanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard/implsystemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
@@ -55,6 +57,7 @@ type Handlers struct {
|
||||
SavedView savedview.Handler
|
||||
Apdex apdex.Handler
|
||||
Dashboard dashboard.Handler
|
||||
SystemDashboard systemdashboard.Handler
|
||||
QuickFilter quickfilter.Handler
|
||||
TraceFunnel tracefunnel.Handler
|
||||
RawDataExport rawdataexport.Handler
|
||||
@@ -99,6 +102,7 @@ func NewHandlers(
|
||||
SavedView: implsavedview.NewHandler(modules.SavedView),
|
||||
Apdex: implapdex.NewHandler(modules.Apdex),
|
||||
Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings, authz),
|
||||
SystemDashboard: implsystemdashboard.NewHandler(modules.SystemDashboard),
|
||||
QuickFilter: implquickfilter.NewHandler(modules.QuickFilter),
|
||||
TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel),
|
||||
RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport),
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
@@ -40,6 +41,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/session/implsession"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard/implsystemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
@@ -66,6 +69,7 @@ type Modules struct {
|
||||
SavedView savedview.Module
|
||||
Apdex apdex.Module
|
||||
Dashboard dashboard.Module
|
||||
SystemDashboard systemdashboard.Module
|
||||
QuickFilter quickfilter.Module
|
||||
TraceFunnel tracefunnel.Module
|
||||
RawDataExport rawdataexport.Module
|
||||
@@ -106,7 +110,8 @@ func NewModules(
|
||||
fl flagger.Flagger,
|
||||
) Modules {
|
||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
||||
systemDashboard := implsystemdashboard.NewModule(impldashboard.NewStore(sqlstore))
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter, systemDashboard)
|
||||
userSetter := impluser.NewSetter(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, userRoleStore, userGetter)
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
|
||||
@@ -117,6 +122,7 @@ func NewModules(
|
||||
SavedView: implsavedview.NewModule(sqlstore),
|
||||
Apdex: implapdex.NewModule(sqlstore),
|
||||
Dashboard: dashboard,
|
||||
SystemDashboard: systemDashboard,
|
||||
UserSetter: userSetter,
|
||||
UserGetter: userGetter,
|
||||
QuickFilter: quickfilter,
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
@@ -63,6 +64,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ flagger.Handler }{},
|
||||
struct{ dashboard.Module }{},
|
||||
struct{ dashboard.Handler }{},
|
||||
struct{ systemdashboard.Handler }{},
|
||||
struct{ metricsexplorer.Handler }{},
|
||||
struct{ inframonitoring.Handler }{},
|
||||
struct{ gateway.Handler }{},
|
||||
|
||||
@@ -195,6 +195,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewServiceAccountAuthzactory(sqlstore),
|
||||
sqlmigration.NewDropUserDeletedAtFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateAWSAllRegionsFactory(sqlstore),
|
||||
sqlmigration.NewAddSystemDashboardFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -269,6 +270,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.FlaggerHandler,
|
||||
modules.Dashboard,
|
||||
handlers.Dashboard,
|
||||
handlers.SystemDashboard,
|
||||
handlers.MetricsExplorer,
|
||||
handlers.InfraMonitoring,
|
||||
handlers.GatewayHandler,
|
||||
|
||||
120
pkg/sqlmigration/078_add_system_dashboard.go
Normal file
120
pkg/sqlmigration/078_add_system_dashboard.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type addSystemDashboard struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddSystemDashboardFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_system_dashboard"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addSystemDashboard{sqlstore: sqlstore, sqlschema: sqlschema}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addSystemDashboard) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addSystemDashboard) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
exists, err := migration.sqlstore.Dialect().ColumnExists(ctx, tx, "dashboard", "source")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("dashboard"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
column := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("source"),
|
||||
DataType: sqlschema.DataTypeText,
|
||||
Nullable: false,
|
||||
}
|
||||
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, "")
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var orgIDs []string
|
||||
if err := tx.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rawOrgID := range orgIDs {
|
||||
orgID, err := valuer.NewUUID(rawOrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, source := range dashboardtypes.AllSources {
|
||||
count, err := tx.NewSelect().
|
||||
Model((*dashboardtypes.StorableDashboard)(nil)).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("source = ?", source.StringValue()).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard, err := dashboardtypes.NewDefaultSystemDashboard(orgID, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storable, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewInsert().Model(storable).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addSystemDashboard) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -413,21 +413,18 @@ func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFro
|
||||
}
|
||||
|
||||
func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFromCTE string) (*qbtypes.Statement, error) {
|
||||
keySelectors := b.getKeySelectors()
|
||||
for _, field := range b.operator.SelectFields {
|
||||
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: field.FieldContext,
|
||||
FieldDataType: field.FieldDataType,
|
||||
})
|
||||
}
|
||||
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
// Select core fields
|
||||
sb.Select(
|
||||
"timestamp",
|
||||
"trace_id",
|
||||
"span_id",
|
||||
"name",
|
||||
"duration_nano",
|
||||
"parent_span_id",
|
||||
)
|
||||
|
||||
coreFields := []string{"trace_id", "span_id", "name", "duration_nano", "parent_span_id"}
|
||||
selectedFields := map[string]bool{
|
||||
"timestamp": true,
|
||||
"trace_id": true,
|
||||
@@ -437,15 +434,23 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
"parent_span_id": true,
|
||||
}
|
||||
|
||||
// Inner SELECT reads from the CTE and renames timestamp→ts.
|
||||
// This breaks the `ORDER BY col AS `col`` pattern that triggers a
|
||||
// CH 25.12.5 distributed-analyzer regression (NOT_FOUND_COLUMN_IN_BLOCK /
|
||||
// timestamp renamed to timestamp_0). See ClickHouse/ClickHouse#103508.
|
||||
innerSB := sqlbuilder.NewSelectBuilder()
|
||||
innerSB.Select("timestamp AS ts")
|
||||
innerSB.SelectMore(coreFields...)
|
||||
// Get keys for selectFields
|
||||
keySelectors := b.getKeySelectors()
|
||||
for _, field := range b.operator.SelectFields {
|
||||
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
||||
Name: field.Name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: field.FieldContext,
|
||||
FieldDataType: field.FieldDataType,
|
||||
})
|
||||
}
|
||||
|
||||
var additionalSelectedFields []string
|
||||
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add selectFields using ColumnExpressionFor since we now have all base table columns
|
||||
for _, field := range b.operator.SelectFields {
|
||||
if selectedFields[field.Name] {
|
||||
continue
|
||||
@@ -456,60 +461,41 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
slog.String("field", field.Name), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
innerSB.SelectMore(colExpr)
|
||||
sb.SelectMore(colExpr)
|
||||
selectedFields[field.Name] = true
|
||||
additionalSelectedFields = append(additionalSelectedFields, field.Name)
|
||||
}
|
||||
|
||||
// Also expose any explicit ORDER BY fields that aren't already selected,
|
||||
// so the outer query can reference them by alias name.
|
||||
sb.From(selectFromCTE)
|
||||
|
||||
// Add order by support using ColumnExpressionFor
|
||||
orderApplied := false
|
||||
for _, orderBy := range b.operator.Order {
|
||||
if selectedFields[orderBy.Key.Name] {
|
||||
continue
|
||||
}
|
||||
colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(ctx, b.start, b.end, &orderBy.Key.TelemetryFieldKey, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
innerSB.SelectMore(colExpr)
|
||||
selectedFields[orderBy.Key.Name] = true
|
||||
sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue()))
|
||||
orderApplied = true
|
||||
}
|
||||
|
||||
innerSB.From(selectFromCTE)
|
||||
innerSQL, innerArgs := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Outer SELECT reads from the inner subquery and re-exposes timestamp via
|
||||
// the ts alias. ORDER BY uses the alias name directly — no AS-alias in the
|
||||
// ORDER BY position — which is the pattern that avoids the CH regression.
|
||||
outerSB := sqlbuilder.NewSelectBuilder()
|
||||
outerSB.Select("ts AS timestamp")
|
||||
outerSB.SelectMore(coreFields...)
|
||||
for _, name := range additionalSelectedFields {
|
||||
outerSB.SelectMore(fmt.Sprintf("`%s`", name))
|
||||
}
|
||||
outerSB.From(fmt.Sprintf("(%s) AS t", innerSQL))
|
||||
|
||||
if len(b.operator.Order) > 0 {
|
||||
for _, orderBy := range b.operator.Order {
|
||||
outerSB.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
||||
}
|
||||
} else {
|
||||
outerSB.OrderBy("timestamp DESC")
|
||||
if !orderApplied {
|
||||
sb.OrderBy("timestamp DESC")
|
||||
}
|
||||
|
||||
if b.operator.Limit > 0 {
|
||||
outerSB.Limit(b.operator.Limit)
|
||||
sb.Limit(b.operator.Limit)
|
||||
} else {
|
||||
outerSB.Limit(100)
|
||||
}
|
||||
if b.operator.Offset > 0 {
|
||||
outerSB.Offset(b.operator.Offset)
|
||||
sb.Limit(100)
|
||||
}
|
||||
|
||||
outerSQL, outerArgs := outerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
if b.operator.Offset > 0 {
|
||||
sb.Offset(b.operator.Offset)
|
||||
}
|
||||
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{
|
||||
Query: outerSQL,
|
||||
Args: append(innerArgs, outerArgs...),
|
||||
Query: sql,
|
||||
Args: args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT ts AS timestamp, trace_id, span_id, name, duration_nano, parent_span_id, `service.name` FROM (SELECT timestamp AS ts, trace_id, span_id, name, duration_nano, parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name` FROM A_DIR_DESC_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name` FROM A_DIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -104,7 +104,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT ts AS timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM (SELECT timestamp AS ts, trace_id, span_id, name, duration_nano, parent_span_id FROM A_INDIR_DESC_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -141,7 +141,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT ts AS timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM (SELECT timestamp AS ts, trace_id, span_id, name, duration_nano, parent_span_id FROM A_AND_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 15},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -178,7 +178,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT ts AS timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM (SELECT timestamp AS ts, trace_id, span_id, name, duration_nano, parent_span_id FROM A_OR_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 20},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -215,7 +215,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT ts AS timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM (SELECT timestamp AS ts, trace_id, span_id, name, duration_nano, parent_span_id FROM A_not_B) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -380,72 +380,11 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT ts AS timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM (SELECT timestamp AS ts, trace_id, span_id, name, duration_nano, parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D) AS t ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
// order-by field (http.request.method) is not present in SelectFields;
|
||||
// it must be included in the inner SELECT so the outer ORDER BY can
|
||||
// reference it by alias, but must NOT appear in the outer SELECT list.
|
||||
name: "order by field not in select fields",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
operator: qbtypes.QueryBuilderTraceOperator{
|
||||
Expression: "A => B",
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
||||
{
|
||||
Name: "service.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
Order: []qbtypes.OrderBy{
|
||||
{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "http.request.method",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
compositeQuery: &qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'frontend'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'backend'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
// http.request.method is in the inner SELECT (so ORDER BY can reach it)
|
||||
// but is absent from the outer SELECT column list — only the ORDER BY clause references it.
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT ts AS timestamp, trace_id, span_id, name, duration_nano, parent_span_id, `service.name` FROM (SELECT timestamp AS ts, trace_id, span_id, name, duration_nano, parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, attributes_string['http.request.method'] AS `http.request.method` FROM A_DIR_DESC_B) AS t ORDER BY `http.request.method` desc LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
|
||||
107
pkg/types/dashboardtypes/ai_o11y_overview.json
Normal file
107
pkg/types/dashboardtypes/ai_o11y_overview.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"title": "AI Observability Overview",
|
||||
"description": "AI / LLM observability overview — cost, tokens, latency, errors, RED for tool calls, and time to first token. Scoped by model, environment and service (apply via the variable bar).",
|
||||
"tags": ["ai", "llm", "genai", "overview"],
|
||||
"version": "v5",
|
||||
"variables": {
|
||||
"model": {
|
||||
"id": "a1000000-0000-0000-0000-000000000001",
|
||||
"name": "model",
|
||||
"key": "model",
|
||||
"description": "LLM model",
|
||||
"type": "QUERY",
|
||||
"sort": "ASC",
|
||||
"multiSelect": true,
|
||||
"showALLOption": true,
|
||||
"allSelected": true,
|
||||
"queryValue": "SELECT DISTINCT attributes_string['gen_ai.request.model'] AS model FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(attributes_string, 'gen_ai.request.model') AND timestamp >= now() - INTERVAL 1 DAY",
|
||||
"customValue": "",
|
||||
"textboxValue": "",
|
||||
"selectedValue": [],
|
||||
"order": 0,
|
||||
"modificationUUID": "a1000000-0000-0000-0000-000000000011"
|
||||
},
|
||||
"environment": {
|
||||
"id": "a1000000-0000-0000-0000-000000000002",
|
||||
"name": "environment",
|
||||
"key": "environment",
|
||||
"description": "Deployment environment",
|
||||
"type": "QUERY",
|
||||
"sort": "ASC",
|
||||
"multiSelect": true,
|
||||
"showALLOption": true,
|
||||
"allSelected": true,
|
||||
"queryValue": "SELECT DISTINCT resources_string['deployment.environment'] AS environment FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'deployment.environment') AND timestamp >= now() - INTERVAL 1 DAY",
|
||||
"customValue": "",
|
||||
"textboxValue": "",
|
||||
"selectedValue": [],
|
||||
"order": 1,
|
||||
"modificationUUID": "a1000000-0000-0000-0000-000000000012"
|
||||
},
|
||||
"service_name": {
|
||||
"id": "a1000000-0000-0000-0000-000000000003",
|
||||
"name": "service_name",
|
||||
"key": "service_name",
|
||||
"description": "Service name",
|
||||
"type": "QUERY",
|
||||
"sort": "ASC",
|
||||
"multiSelect": true,
|
||||
"showALLOption": true,
|
||||
"allSelected": true,
|
||||
"queryValue": "SELECT DISTINCT resources_string['service.name'] AS service_name FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'service.name') AND timestamp >= now() - INTERVAL 1 DAY",
|
||||
"customValue": "",
|
||||
"textboxValue": "",
|
||||
"selectedValue": [],
|
||||
"order": 2,
|
||||
"modificationUUID": "a1000000-0000-0000-0000-000000000013"
|
||||
}
|
||||
},
|
||||
"layout": [
|
||||
{"i": "11111111-1111-1111-1111-111111111111", "x": 0, "y": 0, "w": 3, "h": 3, "moved": false, "static": false},
|
||||
{"i": "22222222-2222-2222-2222-222222222222", "x": 3, "y": 0, "w": 3, "h": 3, "moved": false, "static": false},
|
||||
{"i": "33333333-3333-3333-3333-333333333333", "x": 6, "y": 0, "w": 2, "h": 3, "moved": false, "static": false},
|
||||
{"i": "44444444-4444-4444-4444-444444444444", "x": 8, "y": 0, "w": 2, "h": 3, "moved": false, "static": false},
|
||||
{"i": "55555555-5555-5555-5555-555555555555", "x": 10, "y": 0, "w": 2, "h": 3, "moved": false, "static": false},
|
||||
{"i": "66666666-6666-6666-6666-666666666666", "x": 0, "y": 3, "w": 6, "h": 4, "moved": false, "static": false},
|
||||
{"i": "77777777-7777-7777-7777-777777777777", "x": 6, "y": 3, "w": 6, "h": 4, "moved": false, "static": false},
|
||||
{"i": "88888888-8888-8888-8888-888888888888", "x": 0, "y": 7, "w": 6, "h": 4, "moved": false, "static": false},
|
||||
{"i": "99999999-9999-9999-9999-999999999999", "x": 6, "y": 7, "w": 6, "h": 4, "moved": false, "static": false},
|
||||
{"i": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "x": 0, "y": 11, "w": 4, "h": 4, "moved": false, "static": false},
|
||||
{"i": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "x": 4, "y": 11, "w": 4, "h": 4, "moved": false, "static": false},
|
||||
{"i": "cccccccc-cccc-cccc-cccc-cccccccccccc", "x": 8, "y": 11, "w": 4, "h": 4, "moved": false, "static": false},
|
||||
{"i": "dddddddd-dddd-dddd-dddd-dddddddddddd", "x": 0, "y": 15, "w": 4, "h": 4, "moved": false, "static": false},
|
||||
{"i": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "x": 4, "y": 15, "w": 4, "h": 4, "moved": false, "static": false},
|
||||
{"i": "ffffffff-ffff-ffff-ffff-ffffffffffff", "x": 8, "y": 15, "w": 4, "h": 4, "moved": false, "static": false}
|
||||
],
|
||||
"widgets": [
|
||||
{"id": "11111111-1111-1111-1111-111111111111", "title": "Total cost", "description": "Total LLM cost across all calls. Requires gen_ai.usage.cost attribute.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "none", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q1111111-1111-1111-1111-111111111111", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.cost)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "sum"}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "22222222-2222-2222-2222-222222222222", "title": "Total tokens", "description": "Sum of input + output tokens.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "short", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q2222222-2222-2222-2222-222222222222", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "sum"}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "33333333-3333-3333-3333-333333333333", "title": "Avg latency (p95)", "description": "p95 latency of LLM spans.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ns", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q3333333-3333-3333-3333-333333333333", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(duration_nano)"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "avg"}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "44444444-4444-4444-4444-444444444444", "title": "Error rate", "description": "Error rate as a percentage of total LLM calls.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "percent", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q4444444-4444-4444-4444-444444444444", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "countIf(has_error = true) * 100 / count()"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "avg"}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "55555555-5555-5555-5555-555555555555", "title": "TTFT (p95)", "description": "p95 time to first token.", "panelTypes": "value", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q5555555-5555-5555-5555-555555555555", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(gen_ai.server.ttft)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "", "disabled": false, "having": {"expression": ""}, "limit": null, "reduceTo": "avg"}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "66666666-6666-6666-6666-666666666666", "title": "Cost over time", "description": "Cost by model over time.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "none", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q6666666-6666-6666-6666-666666666666", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.cost)"}], "filter": {"expression": ""}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "77777777-7777-7777-7777-777777777777", "title": "Token usage over time", "description": "Input vs output tokens over time.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": true, "fillSpans": false, "yAxisUnit": "short", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q7777777-7777-7777-7777-777777777777", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.input_tokens)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "Input", "disabled": false, "having": {"expression": ""}, "limit": null}, {"queryName": "B", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "sum(gen_ai.usage.output_tokens)"}], "filter": {"expression": ""}, "groupBy": [], "expression": "B", "orderBy": [], "legend": "Output", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "88888888-8888-8888-8888-888888888888", "title": "LLM call latency percentiles", "description": "p50, p90, p95, p99 latency by model.", "panelTypes": "table", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ns", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q8888888-8888-8888-8888-888888888888", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p50(duration_nano)"}, {"expression": "p90(duration_nano)"}, {"expression": "p95(duration_nano)"}, {"expression": "p99(duration_nano)"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "99999999-9999-9999-9999-999999999999", "title": "LLM call latency over time", "description": "p95 latency trend by model.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ns", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "q9999999-9999-9999-9999-999999999999", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(duration_nano)"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "title": "Error count", "description": "Errors grouped by error type.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": true, "fillSpans": false, "yAxisUnit": "short", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "count()"}], "filter": {"expression": "has_error = true AND gen_ai.system != ''"}, "groupBy": [{"key": "gen_ai.error.type", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.error.type}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "title": "Time to first token", "description": "p95 TTFT by model.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ms", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p95(gen_ai.server.ttft)"}], "filter": {"expression": ""}, "groupBy": [{"key": "gen_ai.request.model", "dataType": "string", "type": "tag", "isColumn": false, "isJSON": false}], "expression": "A", "orderBy": [], "legend": "{{gen_ai.request.model}}", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "title": "Top 10 span names", "description": "Top span names by count across GenAI spans.", "panelTypes": "table", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "none", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qccccccc-cccc-cccc-cccc-cccccccccccc", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "count()"}], "filter": {"expression": "gen_ai.system != ''"}, "groupBy": [{"key": "name", "dataType": "string", "type": "tag", "isColumn": true, "isJSON": false}], "expression": "A", "orderBy": [{"columnName": "count()", "order": "desc"}], "legend": "{{name}}", "disabled": false, "having": {"expression": ""}, "limit": 10}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "dddddddd-dddd-dddd-dddd-dddddddddddd", "title": "Tool call rate", "description": "Tool call rate per second.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "reqps", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qddddddd-dddd-dddd-dddd-dddddddddddd", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "rate()"}], "filter": {"expression": "name = 'execute_tool'"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "req/s", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "title": "Tool error rate", "description": "Percentage of tool calls that errored.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "percent", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "countIf(has_error = true) * 100 / count()"}], "filter": {"expression": "name = 'execute_tool'"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "error %", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}},
|
||||
|
||||
{"id": "ffffffff-ffff-ffff-ffff-ffffffffffff", "title": "Tool duration (p50)", "description": "Median tool call duration.", "panelTypes": "graph", "nullZeroValues": "zero", "opacity": "1", "isStacked": false, "fillSpans": false, "yAxisUnit": "ns", "timePreferance": "GLOBAL_TIME", "softMax": null, "softMin": null, "thresholds": [], "selectedLogFields": [], "selectedTracesFields": [], "query": {"queryType": "builder", "promql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "clickhouse_sql": [{"disabled": false, "legend": "", "name": "A", "query": ""}], "id": "qfffffff-ffff-ffff-ffff-ffffffffffff", "builder": {"queryData": [{"queryName": "A", "stepInterval": 60, "dataSource": "traces", "aggregations": [{"expression": "p50(duration_nano)"}], "filter": {"expression": "name = 'execute_tool'"}, "groupBy": [], "expression": "A", "orderBy": [], "legend": "p50", "disabled": false, "having": {"expression": ""}, "limit": null}], "queryFormulas": [], "queryTraceOperator": []}}}
|
||||
]
|
||||
}
|
||||
@@ -37,6 +37,7 @@ type StorableDashboard struct {
|
||||
Data StorableDashboardData `bun:"data,type:text,notnull"`
|
||||
Locked bool `bun:"locked,notnull,default:false"`
|
||||
OrgID valuer.UUID `bun:"org_id,notnull"`
|
||||
Source string `bun:"source,type:text,notnull"`
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
@@ -47,6 +48,7 @@ type Dashboard struct {
|
||||
Data StorableDashboardData `json:"data"`
|
||||
Locked bool `json:"locked"`
|
||||
OrgID valuer.UUID `json:"org_id"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type LockUnlockDashboard struct {
|
||||
@@ -86,6 +88,7 @@ func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard
|
||||
OrgID: dashboard.OrgID,
|
||||
Data: dashboard.Data,
|
||||
Locked: dashboard.Locked,
|
||||
Source: dashboard.Source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -108,6 +111,30 @@ func NewDashboard(orgID valuer.UUID, createdBy string, storableDashboardData Sto
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewSystemDashboard builds a Dashboard owned by the system identified by source (e.g. "ai-o11y-overview").
|
||||
func NewSystemDashboard(orgID valuer.UUID, source string, data StorableDashboardData) (*Dashboard, error) {
|
||||
if source == "" {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "source is required for a system dashboard")
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
|
||||
return &Dashboard{
|
||||
ID: valuer.GenerateUUID().StringValue(),
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: currentTime,
|
||||
UpdatedAt: currentTime,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: "system",
|
||||
UpdatedBy: "system",
|
||||
},
|
||||
OrgID: orgID,
|
||||
Data: data,
|
||||
Source: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) *Dashboard {
|
||||
return &Dashboard{
|
||||
ID: storableDashboard.ID.StringValue(),
|
||||
@@ -122,6 +149,7 @@ func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) *Da
|
||||
OrgID: storableDashboard.OrgID,
|
||||
Data: storableDashboard.Data,
|
||||
Locked: storableDashboard.Locked,
|
||||
Source: storableDashboard.Source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +182,7 @@ func NewGettableDashboardFromDashboard(dashboard *Dashboard) (*GettableDashboard
|
||||
OrgID: dashboard.OrgID,
|
||||
Data: dashboard.Data,
|
||||
Locked: dashboard.Locked,
|
||||
Source: dashboard.Source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
30
pkg/types/dashboardtypes/defaults.go
Normal file
30
pkg/types/dashboardtypes/defaults.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
//go:embed ai_o11y_overview.json
|
||||
var aiO11yOverviewJSON []byte
|
||||
|
||||
func NewDefaultSystemDashboard(orgID valuer.UUID, source Source) (*Dashboard, error) {
|
||||
switch source {
|
||||
case SourceAIO11yOverview:
|
||||
return newDefaultAIO11yOverview(orgID)
|
||||
default:
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "no defaults registered for system dashboard source %s", source.StringValue())
|
||||
}
|
||||
}
|
||||
|
||||
func newDefaultAIO11yOverview(orgID valuer.UUID) (*Dashboard, error) {
|
||||
data := StorableDashboardData{}
|
||||
if err := json.Unmarshal(aiO11yOverviewJSON, &data); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal embedded ai-o11y-overview default")
|
||||
}
|
||||
|
||||
return NewSystemDashboard(orgID, SourceAIO11yOverview.StringValue(), data)
|
||||
}
|
||||
44
pkg/types/dashboardtypes/source.go
Normal file
44
pkg/types/dashboardtypes/source.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
SourceAIO11yOverview = Source{valuer.NewString("ai-o11y-overview")}
|
||||
)
|
||||
|
||||
var AllSources = []Source{
|
||||
SourceAIO11yOverview,
|
||||
}
|
||||
|
||||
func NewSource(s string) (Source, error) {
|
||||
switch s {
|
||||
case SourceAIO11yOverview.StringValue():
|
||||
return SourceAIO11yOverview, nil
|
||||
default:
|
||||
return Source{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid system dashboard source: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func (source *Source) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := NewSource(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*source = s
|
||||
return nil
|
||||
}
|
||||
@@ -13,6 +13,8 @@ type Store interface {
|
||||
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, error)
|
||||
|
||||
GetBySource(context.Context, valuer.UUID, string) (*StorableDashboard, error)
|
||||
|
||||
GetPublic(context.Context, string) (*StorablePublicDashboard, error)
|
||||
|
||||
GetDashboardByOrgsAndPublicID(context.Context, []string, string) (*StorableDashboard, error)
|
||||
@@ -25,10 +27,14 @@ type Store interface {
|
||||
|
||||
Update(context.Context, valuer.UUID, *StorableDashboard) error
|
||||
|
||||
UpdateBySource(context.Context, valuer.UUID, string, *StorableDashboard) error
|
||||
|
||||
UpdatePublic(context.Context, *StorablePublicDashboard) error
|
||||
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
DeleteBySource(context.Context, valuer.UUID, string) error
|
||||
|
||||
DeletePublic(context.Context, string) error
|
||||
|
||||
RunInTx(context.Context, func(context.Context) error) error
|
||||
|
||||
35
pkg/types/dashboardtypes/system_dashboard.go
Normal file
35
pkg/types/dashboardtypes/system_dashboard.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type GettableSystemDashboard struct {
|
||||
ID string `json:"id"`
|
||||
OrgID valuer.UUID `json:"org_id"`
|
||||
Source string `json:"source"`
|
||||
Data StorableDashboardData `json:"data"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
}
|
||||
|
||||
type UpdatableSystemDashboard struct {
|
||||
Data StorableDashboardData `json:"data"`
|
||||
}
|
||||
|
||||
func NewGettableSystemDashboardFromDashboard(dashboard *Dashboard) *GettableSystemDashboard {
|
||||
return &GettableSystemDashboard{
|
||||
ID: dashboard.ID,
|
||||
OrgID: dashboard.OrgID,
|
||||
Source: dashboard.Source,
|
||||
Data: dashboard.Data,
|
||||
CreatedAt: dashboard.CreatedAt,
|
||||
CreatedBy: dashboard.CreatedBy,
|
||||
UpdatedAt: dashboard.UpdatedAt,
|
||||
UpdatedBy: dashboard.UpdatedBy,
|
||||
}
|
||||
}
|
||||
3
tests/fixtures/querier.py
vendored
3
tests/fixtures/querier.py
vendored
@@ -72,7 +72,6 @@ class TraceOperatorQuery:
|
||||
return_spans_from: str
|
||||
limit: int | None = None
|
||||
order: list[OrderBy] | None = None
|
||||
select_fields: list[TelemetryFieldKey] | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
spec: dict[str, Any] = {
|
||||
@@ -84,8 +83,6 @@ class TraceOperatorQuery:
|
||||
spec["limit"] = self.limit
|
||||
if self.order:
|
||||
spec["order"] = [o.to_dict() if hasattr(o, "to_dict") else o for o in self.order]
|
||||
if self.select_fields:
|
||||
spec["selectFields"] = [f.to_dict() for f in self.select_fields]
|
||||
return {"type": "builder_trace_operator", "spec": spec}
|
||||
|
||||
|
||||
|
||||
@@ -530,13 +530,11 @@ def test_export_traces_with_composite_query_trace_operator(
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert a parent span and two child spans, all with an http.method attribute.
|
||||
Insert multiple traces with parent-child relationships.
|
||||
|
||||
Tests:
|
||||
1. Basic trace operator (A => B) returning parent spans, ordered by timestamp.
|
||||
2. Same operator with selectFields=[service.name] and order by http.method, which is
|
||||
NOT in selectFields — verifies the inner/outer subquery fix for the CH 25.12.5
|
||||
NOT_FOUND_COLUMN_IN_BLOCK regression (ORDER BY col AS `col` in a CTE shape).
|
||||
1. Export traces using trace operator in composite query (POST)
|
||||
2. Verify trace operator query works correctly
|
||||
"""
|
||||
parent_trace_id = TraceIdGenerator.trace_id()
|
||||
parent_span_id = TraceIdGenerator.span_id()
|
||||
@@ -557,8 +555,12 @@ def test_export_traces_with_composite_query_trace_operator(
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "parent-service"},
|
||||
attributes={"operation.type": "parent", "http.method": "GET"},
|
||||
resources={
|
||||
"service.name": "parent-service",
|
||||
},
|
||||
attributes={
|
||||
"operation.type": "parent",
|
||||
},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
@@ -570,8 +572,12 @@ def test_export_traces_with_composite_query_trace_operator(
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "parent-service"},
|
||||
attributes={"operation.type": "child", "http.method": "POST"},
|
||||
resources={
|
||||
"service.name": "parent-service",
|
||||
},
|
||||
attributes={
|
||||
"operation.type": "child",
|
||||
},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
@@ -583,23 +589,31 @@ def test_export_traces_with_composite_query_trace_operator(
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "parent-service"},
|
||||
attributes={"operation.type": "child", "http.method": "POST"},
|
||||
resources={
|
||||
"service.name": "parent-service",
|
||||
},
|
||||
attributes={
|
||||
"operation.type": "child",
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Calculate timestamps in nanoseconds
|
||||
start_ns = int((now - timedelta(minutes=5)).timestamp() * 1e9)
|
||||
end_ns = int(now.timestamp() * 1e9)
|
||||
|
||||
url = signoz.self.host_configs["8080"].get("/api/v1/export_raw_data?format=jsonl")
|
||||
# A: spans with operation.type = 'parent'
|
||||
query_a = BuilderQuery(
|
||||
signal="traces",
|
||||
name="A",
|
||||
limit=1000,
|
||||
filter_expression="operation.type = 'parent'",
|
||||
)
|
||||
|
||||
# B: spans with operation.type = 'child'
|
||||
query_b = BuilderQuery(
|
||||
signal="traces",
|
||||
name="B",
|
||||
@@ -607,50 +621,47 @@ def test_export_traces_with_composite_query_trace_operator(
|
||||
filter_expression="operation.type = 'child'",
|
||||
)
|
||||
|
||||
def export(operator: TraceOperatorQuery) -> list[dict]:
|
||||
body = QueryRangeRequest(
|
||||
start=start_ns,
|
||||
end=end_ns,
|
||||
queries=[query_a, query_b, operator],
|
||||
).to_dict()
|
||||
resp = requests.post(
|
||||
url,
|
||||
json=body,
|
||||
timeout=10,
|
||||
headers={"authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
assert resp.headers["Content-Type"] == "application/x-ndjson"
|
||||
return [json.loads(line) for line in resp.text.strip().split("\n") if line]
|
||||
|
||||
# Test 1: basic trace operator ordered by timestamp
|
||||
spans = export(
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=1000,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp", "string", "span"), "desc")],
|
||||
)
|
||||
# Trace operator: find traces where A has a direct descendant B
|
||||
query_c = TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=1000,
|
||||
order=[OrderBy(TelemetryFieldKey("timestamp", "string", "span"), "desc")],
|
||||
)
|
||||
assert len(spans) == 1
|
||||
assert all(s.get("trace_id") == parent_trace_id for s in spans)
|
||||
assert any(s.get("name") == "parent-operation" for s in spans)
|
||||
|
||||
# Test 2: order-by field (http.method) absent from selectFields
|
||||
spans = export(
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=1000,
|
||||
select_fields=[TelemetryFieldKey("service.name", "string", "resource")],
|
||||
order=[OrderBy(TelemetryFieldKey("http.method", "string", "tag"), "desc")],
|
||||
)
|
||||
body = QueryRangeRequest(
|
||||
start=start_ns,
|
||||
end=end_ns,
|
||||
queries=[query_a, query_b, query_c],
|
||||
).to_dict()
|
||||
|
||||
url = signoz.self.host_configs["8080"].get("/api/v1/export_raw_data?format=jsonl")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=body,
|
||||
timeout=10,
|
||||
headers={
|
||||
"authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
assert len(spans) >= 1
|
||||
assert all(s.get("trace_id") == parent_trace_id for s in spans)
|
||||
assert any(s.get("name") == "parent-operation" for s in spans)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.headers["Content-Type"] == "application/x-ndjson"
|
||||
|
||||
# Parse JSONL content
|
||||
jsonl_lines = response.text.strip().split("\n")
|
||||
assert len(jsonl_lines) == 1, f"Expected at least 1 line, got {len(jsonl_lines)}"
|
||||
|
||||
# Verify all returned spans belong to the matched trace
|
||||
json_objects = [json.loads(line) for line in jsonl_lines]
|
||||
trace_ids = [obj.get("trace_id") for obj in json_objects]
|
||||
assert all(tid == parent_trace_id for tid in trace_ids)
|
||||
|
||||
# Verify the parent span (returnSpansFrom = "A") is present
|
||||
span_names = [obj.get("name") for obj in json_objects]
|
||||
assert "parent-operation" in span_names
|
||||
|
||||
|
||||
def test_export_traces_with_select_fields(
|
||||
|
||||
Reference in New Issue
Block a user