mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-30 07:30:30 +01:00
Compare commits
4 Commits
issue_4203
...
adhoc/allo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a021ab4cb | ||
|
|
755390c4b5 | ||
|
|
adbd89aae9 | ||
|
|
b71de5b561 |
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.120.0
|
||||
image: signoz/signoz:v0.121.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.120.0
|
||||
image: signoz/signoz:v0.121.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.120.0}
|
||||
image: signoz/signoz:${VERSION:-v0.121.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.120.0}
|
||||
image: signoz/signoz:${VERSION:-v0.121.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -2632,6 +2632,158 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
LlmpricingruletypesGettablePricingRules:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
required:
|
||||
- items
|
||||
- total
|
||||
- offset
|
||||
- limit
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingCacheCosts:
|
||||
properties:
|
||||
mode:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleCacheMode'
|
||||
read:
|
||||
format: double
|
||||
type: number
|
||||
write:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- mode
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingRule:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
isOverride:
|
||||
type: boolean
|
||||
modelName:
|
||||
type: string
|
||||
modelPattern:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesStringSlice'
|
||||
orgId:
|
||||
type: string
|
||||
pricing:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
|
||||
provider:
|
||||
type: string
|
||||
sourceId:
|
||||
type: string
|
||||
syncedAt:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
unit:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- orgId
|
||||
- modelName
|
||||
- provider
|
||||
- modelPattern
|
||||
- unit
|
||||
- pricing
|
||||
- isOverride
|
||||
- enabled
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingRuleCacheMode:
|
||||
enum:
|
||||
- subtract
|
||||
- additive
|
||||
- unknown
|
||||
type: string
|
||||
LlmpricingruletypesLLMPricingRuleUnit:
|
||||
enum:
|
||||
- per_million_tokens
|
||||
type: string
|
||||
LlmpricingruletypesLLMRulePricing:
|
||||
properties:
|
||||
cache:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingCacheCosts'
|
||||
input:
|
||||
format: double
|
||||
type: number
|
||||
output:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- input
|
||||
- output
|
||||
type: object
|
||||
LlmpricingruletypesStringSlice:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
LlmpricingruletypesUpdatableLLMPricingRule:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
nullable: true
|
||||
type: string
|
||||
isOverride:
|
||||
nullable: true
|
||||
type: boolean
|
||||
modelName:
|
||||
type: string
|
||||
modelPattern:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
pricing:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
|
||||
provider:
|
||||
type: string
|
||||
sourceId:
|
||||
nullable: true
|
||||
type: string
|
||||
unit:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
|
||||
required:
|
||||
- modelName
|
||||
- provider
|
||||
- modelPattern
|
||||
- unit
|
||||
- pricing
|
||||
- enabled
|
||||
type: object
|
||||
LlmpricingruletypesUpdatableLLMPricingRules:
|
||||
properties:
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRule'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- rules
|
||||
type: object
|
||||
MetricsexplorertypesInspectMetricsRequest:
|
||||
properties:
|
||||
end:
|
||||
@@ -7675,6 +7827,218 @@ paths:
|
||||
summary: Create bulk invite
|
||||
tags:
|
||||
- users
|
||||
/api/v1/llm_pricing_rules:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns all LLM pricing rules for the authenticated org, with pagination.
|
||||
operationId: ListLLMPricingRules
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesGettablePricingRules'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List pricing rules
|
||||
tags:
|
||||
- llmpricingrules
|
||||
put:
|
||||
deprecated: false
|
||||
description: Single write endpoint used by both the user and the Zeus sync job.
|
||||
Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true)
|
||||
are fully preserved when the request does not provide isOverride; only synced_at
|
||||
is stamped.
|
||||
operationId: CreateOrUpdateLLMPricingRules
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRules'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create or update pricing rules
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/llm_pricing_rules/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Hard-deletes a pricing rule. If auto-synced, it will be recreated
|
||||
on the next sync cycle.
|
||||
operationId: DeleteLLMPricingRule
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a single LLM pricing rule by ID.
|
||||
operationId: GetLLMPricingRule
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
|
||||
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
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/logs/promote_paths:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.10",
|
||||
"@signozhq/ui": "0.0.12",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
399
frontend/src/api/generated/services/llmpricingrules/index.ts
Normal file
399
frontend/src/api/generated/services/llmpricingrules/index.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* ! 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 {
|
||||
DeleteLLMPricingRulePathParameters,
|
||||
GetLLMPricingRule200,
|
||||
GetLLMPricingRulePathParameters,
|
||||
ListLLMPricingRules200,
|
||||
ListLLMPricingRulesParams,
|
||||
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns all LLM pricing rules for the authenticated org, with pagination.
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
export const listLLMPricingRules = (
|
||||
params?: ListLLMPricingRulesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListLLMPricingRules200>({
|
||||
url: `/api/v1/llm_pricing_rules`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListLLMPricingRulesQueryKey = (
|
||||
params?: ListLLMPricingRulesParams,
|
||||
) => {
|
||||
return [`/api/v1/llm_pricing_rules`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListLLMPricingRulesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListLLMPricingRulesQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>
|
||||
> = ({ signal }) => listLLMPricingRules(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListLLMPricingRulesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>
|
||||
>;
|
||||
export type ListLLMPricingRulesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
|
||||
export function useListLLMPricingRules<
|
||||
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListLLMPricingRulesQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
export const invalidateListLLMPricingRules = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListLLMPricingRulesQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.
|
||||
* @summary Create or update pricing rules
|
||||
*/
|
||||
export const createOrUpdateLLMPricingRules = (
|
||||
llmpricingruletypesUpdatableLLMPricingRulesDTO: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/llm_pricing_rules`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: llmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateOrUpdateLLMPricingRulesMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createOrUpdateLLMPricingRules'];
|
||||
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 createOrUpdateLLMPricingRules>>,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createOrUpdateLLMPricingRules(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateOrUpdateLLMPricingRulesMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>
|
||||
>;
|
||||
export type CreateOrUpdateLLMPricingRulesMutationBody =
|
||||
BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>;
|
||||
export type CreateOrUpdateLLMPricingRulesMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create or update pricing rules
|
||||
*/
|
||||
export const useCreateOrUpdateLLMPricingRules = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions =
|
||||
getCreateOrUpdateLLMPricingRulesMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.
|
||||
* @summary Delete a pricing rule
|
||||
*/
|
||||
export const deleteLLMPricingRule = ({
|
||||
id,
|
||||
}: DeleteLLMPricingRulePathParameters) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/llm_pricing_rules/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteLLMPricingRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteLLMPricingRule'];
|
||||
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 deleteLLMPricingRule>>,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteLLMPricingRule(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteLLMPricingRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>
|
||||
>;
|
||||
|
||||
export type DeleteLLMPricingRuleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete a pricing rule
|
||||
*/
|
||||
export const useDeleteLLMPricingRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteLLMPricingRuleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a single LLM pricing rule by ID.
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
export const getLLMPricingRule = (
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetLLMPricingRule200>({
|
||||
url: `/api/v1/llm_pricing_rules/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetLLMPricingRuleQueryKey = ({
|
||||
id,
|
||||
}: GetLLMPricingRulePathParameters) => {
|
||||
return [`/api/v1/llm_pricing_rules/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetLLMPricingRuleQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetLLMPricingRuleQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>
|
||||
> = ({ signal }) => getLLMPricingRule({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetLLMPricingRuleQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>
|
||||
>;
|
||||
export type GetLLMPricingRuleQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
|
||||
export function useGetLLMPricingRule<
|
||||
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetLLMPricingRuleQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
export const invalidateGetLLMPricingRule = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetLLMPricingRuleQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
@@ -3413,6 +3413,170 @@ export enum InframonitoringtypesResponseTypeDTO {
|
||||
list = 'list',
|
||||
grouped_list = 'grouped_list',
|
||||
}
|
||||
export interface LlmpricingruletypesGettablePricingRulesDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
items: LlmpricingruletypesLLMPricingRuleDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
offset: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesLLMPricingCacheCostsDTO {
|
||||
mode: LlmpricingruletypesLLMPricingRuleCacheModeDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
read?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
write?: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesLLMPricingRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isOverride: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
modelName: string;
|
||||
modelPattern: LlmpricingruletypesStringSliceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
pricing: LlmpricingruletypesLLMRulePricingDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
provider: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
sourceId?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
* @nullable true
|
||||
*/
|
||||
syncedAt?: Date | null;
|
||||
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export enum LlmpricingruletypesLLMPricingRuleCacheModeDTO {
|
||||
subtract = 'subtract',
|
||||
additive = 'additive',
|
||||
unknown = 'unknown',
|
||||
}
|
||||
export enum LlmpricingruletypesLLMPricingRuleUnitDTO {
|
||||
per_million_tokens = 'per_million_tokens',
|
||||
}
|
||||
export interface LlmpricingruletypesLLMRulePricingDTO {
|
||||
cache?: LlmpricingruletypesLLMPricingCacheCostsDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
input: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
output: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type LlmpricingruletypesStringSliceDTO = string[] | null;
|
||||
|
||||
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
id?: string | null;
|
||||
/**
|
||||
* @type boolean
|
||||
* @nullable true
|
||||
*/
|
||||
isOverride?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
modelName: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
modelPattern: string[] | null;
|
||||
pricing: LlmpricingruletypesLLMRulePricingDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
provider: string;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
sourceId?: string | null;
|
||||
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesUpdatableLLMPricingRulesDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
rules: LlmpricingruletypesUpdatableLLMPricingRuleDTO[] | null;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesInspectMetricsRequestDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -7004,6 +7168,41 @@ export type CreateInvite201 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListLLMPricingRulesParams = {
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type ListLLMPricingRules200 = {
|
||||
data: LlmpricingruletypesGettablePricingRulesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteLLMPricingRulePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetLLMPricingRulePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetLLMPricingRule200 = {
|
||||
data: LlmpricingruletypesLLMPricingRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListPromotedAndIndexedPaths200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
@@ -46,6 +46,7 @@ function DeleteMemberDialog({
|
||||
color="destructive"
|
||||
disabled={isDeleting}
|
||||
onClick={onConfirm}
|
||||
loading={isDeleting}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Processing...' : title}
|
||||
@@ -63,7 +64,6 @@ function DeleteMemberDialog({
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
footer={footer}
|
||||
|
||||
@@ -28,18 +28,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__input {
|
||||
height: 32px;
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -48,7 +36,7 @@
|
||||
padding: var(--padding-1) var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--border);
|
||||
box-sizing: border-box;
|
||||
|
||||
&--disabled {
|
||||
@@ -65,8 +53,8 @@
|
||||
}
|
||||
|
||||
&__email-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.07px;
|
||||
@@ -178,36 +166,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.delete-dialog {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
[data-slot='dialog-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__body {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--l2-foreground);
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
letter-spacing: -0.065px;
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-4);
|
||||
margin-top: var(--margin-6);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-link-dialog {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
@@ -264,13 +222,6 @@
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
min-width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ function EditMemberDrawer({
|
||||
try {
|
||||
await rawRetry();
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -250,7 +250,7 @@ function EditMemberDrawer({
|
||||
});
|
||||
}
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -319,7 +319,7 @@ function EditMemberDrawer({
|
||||
}),
|
||||
];
|
||||
});
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -340,7 +340,7 @@ function EditMemberDrawer({
|
||||
onComplete();
|
||||
}
|
||||
|
||||
refetchUser();
|
||||
void refetchUser();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -465,7 +465,6 @@ function EditMemberDrawer({
|
||||
prev.filter((err) => err.context !== 'Name update'),
|
||||
);
|
||||
}}
|
||||
className="edit-member-drawer__input"
|
||||
placeholder="Enter name"
|
||||
disabled={isRootUser || isDeleted}
|
||||
/>
|
||||
@@ -631,7 +630,7 @@ function EditMemberDrawer({
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__footer-right">
|
||||
<Button variant="solid" color="secondary" onClick={handleClose}>
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -641,6 +640,7 @@ function EditMemberDrawer({
|
||||
color="primary"
|
||||
disabled={!isDirty || isSaving || isRootUser}
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Member Details'}
|
||||
</Button>
|
||||
|
||||
@@ -44,9 +44,8 @@ function ResetLinkDialog({
|
||||
<span className="reset-link-dialog__link-text">{resetLink}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
prefix={hasCopied ? <Check size={12} /> : <Copy size={12} />}
|
||||
className="reset-link-dialog__copy-btn"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Style } from '@signozhq/design-tokens';
|
||||
import { ChevronDown, CircleAlert, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import { ChevronDown, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
@@ -294,10 +294,8 @@ function InviteMembersModal({
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
>
|
||||
{getValidationErrorMessage()}
|
||||
</Callout>
|
||||
title={getValidationErrorMessage()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
input {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.ant-picker-suffix {
|
||||
@@ -126,12 +126,6 @@
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
min-width: 40px;
|
||||
}
|
||||
@@ -152,6 +146,7 @@
|
||||
color: var(--foreground);
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
&__footer {
|
||||
|
||||
@@ -22,9 +22,8 @@ function KeyCreatedPhase({
|
||||
<div className="add-key-modal__key-display">
|
||||
<span className="add-key-modal__key-text">{createdKey.key}</span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
className="add-key-modal__copy-btn"
|
||||
>
|
||||
|
||||
@@ -106,7 +106,7 @@ function KeyFormPhase({
|
||||
|
||||
<div className="add-key-modal__footer">
|
||||
<div className="add-key-modal__footer-right">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onClose}>
|
||||
<Button variant="solid" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -115,7 +115,6 @@ function KeyFormPhase({
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
|
||||
@@ -136,7 +136,7 @@ function EditKeyForm({
|
||||
</form>
|
||||
|
||||
<div className="edit-key-modal__footer">
|
||||
<Button variant="ghost" color="destructive" onClick={onRevokeClick}>
|
||||
<Button variant="link" color="destructive" onClick={onRevokeClick}>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
input {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.ant-picker-suffix {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { RevokeKeyContent } from '../RevokeKeyModal';
|
||||
import { RevokeKeyFooter } from '../RevokeKeyModal';
|
||||
import EditKeyForm from './EditKeyForm';
|
||||
import type { FormValues } from './types';
|
||||
import { DEFAULT_FORM_VALUES, ExpiryMode } from './types';
|
||||
@@ -158,17 +158,25 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
|
||||
}
|
||||
width={isRevokeConfirmOpen ? 'narrow' : 'base'}
|
||||
className={
|
||||
isRevokeConfirmOpen ? 'alert-dialog delete-dialog' : 'edit-key-modal'
|
||||
isRevokeConfirmOpen ? 'alert-dialog sa-delete-dialog' : 'edit-key-modal'
|
||||
}
|
||||
showCloseButton={!isRevokeConfirmOpen}
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
footer={
|
||||
isRevokeConfirmOpen ? (
|
||||
<RevokeKeyFooter
|
||||
isRevoking={isRevoking}
|
||||
onCancel={(): void => setIsRevokeConfirmOpen(false)}
|
||||
onConfirm={handleRevoke}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{isRevokeConfirmOpen ? (
|
||||
<RevokeKeyContent
|
||||
isRevoking={isRevoking}
|
||||
onCancel={(): void => setIsRevokeConfirmOpen(false)}
|
||||
onConfirm={handleRevoke}
|
||||
/>
|
||||
<>
|
||||
Revoking this key will permanently invalidate it. Any systems using this
|
||||
key will lose access immediately.
|
||||
</>
|
||||
) : (
|
||||
<EditKeyForm
|
||||
register={register}
|
||||
|
||||
@@ -72,7 +72,6 @@ function OverviewTab({
|
||||
id="sa-name"
|
||||
value={localName}
|
||||
onChange={(e): void => onNameChange(e.target.value)}
|
||||
className="sa-drawer__input"
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,39 +17,32 @@ import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
export interface RevokeKeyContentProps {
|
||||
export interface RevokeKeyFooterProps {
|
||||
isRevoking: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function RevokeKeyContent({
|
||||
export function RevokeKeyFooter({
|
||||
isRevoking,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: RevokeKeyContentProps): JSX.Element {
|
||||
}: RevokeKeyFooterProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p className="delete-dialog__body">
|
||||
Revoking this key will permanently invalidate it. Any systems using this key
|
||||
will lose access immediately.
|
||||
</p>
|
||||
<div className="delete-dialog__footer">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onCancel}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="solid" color="secondary" onClick={onCancel}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -112,15 +105,19 @@ function RevokeKeyModal(): JSX.Element {
|
||||
}}
|
||||
title={`Revoke ${keyName ?? 'key'}?`}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
className="alert-dialog sa-delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
footer={
|
||||
<RevokeKeyFooter
|
||||
isRevoking={isRevoking}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RevokeKeyContent
|
||||
isRevoking={isRevoking}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
Revoking this key will permanently invalidate it. Any systems using this key
|
||||
will lose access immediately.
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
&__tab {
|
||||
@@ -166,18 +168,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__input {
|
||||
height: 32px;
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -186,7 +176,7 @@
|
||||
padding: 0 var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
&--disabled {
|
||||
cursor: not-allowed;
|
||||
@@ -195,8 +185,8 @@
|
||||
}
|
||||
|
||||
&__input-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
@@ -129,7 +129,7 @@ function ServiceAccountDrawer({
|
||||
useEffect(() => {
|
||||
if (account?.id) {
|
||||
setLocalName(account?.name ?? '');
|
||||
setKeysPage(1);
|
||||
void setKeysPage(1);
|
||||
}
|
||||
}, [account?.id, account?.name, setKeysPage]);
|
||||
|
||||
@@ -176,7 +176,7 @@ function ServiceAccountDrawer({
|
||||
}
|
||||
const maxPage = Math.max(1, Math.ceil(keys.length / PAGE_SIZE));
|
||||
if (keysPage > maxPage) {
|
||||
setKeysPage(maxPage);
|
||||
void setKeysPage(maxPage);
|
||||
}
|
||||
}, [keysLoading, keys.length, keysPage, setKeysPage]);
|
||||
|
||||
@@ -214,8 +214,8 @@ function ServiceAccountDrawer({
|
||||
data: { name: localName },
|
||||
});
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
void refetchAccount();
|
||||
void queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -337,8 +337,8 @@ function ServiceAccountDrawer({
|
||||
onSuccess({ closeDrawer: false });
|
||||
}
|
||||
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
void refetchAccount();
|
||||
void queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -357,12 +357,12 @@ function ServiceAccountDrawer({
|
||||
]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setIsDeleteOpen(null);
|
||||
setIsAddKeyOpen(null);
|
||||
setSelectedAccountId(null);
|
||||
setActiveTab(null);
|
||||
setKeysPage(null);
|
||||
setEditKeyId(null);
|
||||
void setIsDeleteOpen(null);
|
||||
void setIsAddKeyOpen(null);
|
||||
void setSelectedAccountId(null);
|
||||
void setActiveTab(null);
|
||||
void setKeysPage(null);
|
||||
void setEditKeyId(null);
|
||||
setSaveErrors([]);
|
||||
}, [
|
||||
setSelectedAccountId,
|
||||
@@ -379,12 +379,13 @@ function ServiceAccountDrawer({
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={activeTab}
|
||||
size="sm"
|
||||
onChange={(val): void => {
|
||||
if (val) {
|
||||
setActiveTab(val as ServiceAccountDrawerTab);
|
||||
void setActiveTab(val as ServiceAccountDrawerTab);
|
||||
if (val !== ServiceAccountDrawerTab.Keys) {
|
||||
setKeysPage(null);
|
||||
setEditKeyId(null);
|
||||
void setKeysPage(null);
|
||||
void setEditKeyId(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -415,7 +416,7 @@ function ServiceAccountDrawer({
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
onClick={(): void => {
|
||||
setIsAddKeyOpen(true);
|
||||
void setIsAddKeyOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
@@ -503,7 +504,7 @@ function ServiceAccountDrawer({
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={(): void => {
|
||||
setIsDeleteOpen(true);
|
||||
void setIsDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
@@ -512,7 +513,7 @@ function ServiceAccountDrawer({
|
||||
)}
|
||||
{!isDeleted && (
|
||||
<div className="sa-drawer__footer-right">
|
||||
<Button variant="solid" color="secondary" onClick={handleClose}>
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-10);
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.custom-domain-card-meta-row.workspace-name-hidden {
|
||||
@@ -124,30 +125,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-url-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.06px;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-url-dropdown {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
@@ -204,6 +204,7 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
disabled={isFetchingHosts}
|
||||
dropdownRender={(): JSX.Element => (
|
||||
<div className="workspace-url-dropdown">
|
||||
<span className="workspace-url-dropdown-header">
|
||||
@@ -239,12 +240,7 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className="workspace-url-trigger"
|
||||
disabled={isFetchingHosts}
|
||||
variant="link"
|
||||
color="none"
|
||||
>
|
||||
<Button variant="link" color="none">
|
||||
<Link2 size={12} />
|
||||
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
||||
<ChevronDown size={12} />
|
||||
|
||||
@@ -89,25 +89,4 @@
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
width: 100%;
|
||||
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l2-foreground) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ function DomainMappingList({
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="dashed"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={(): void => add({ domain: '', adminEmail: '' })}
|
||||
prefix={<Plus size={14} />}
|
||||
className="domain-mapping-list__add-btn"
|
||||
>
|
||||
Add Domain Mapping
|
||||
</Button>
|
||||
|
||||
@@ -51,35 +51,6 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: https://github.com/SigNoz/components/issues/116
|
||||
.roles-search-wrapper {
|
||||
flex: 1;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
padding: 6px 6px 6px 8px;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l1-foreground);
|
||||
outline: none;
|
||||
height: 32px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.roles-description-tooltip {
|
||||
|
||||
@@ -22,14 +22,12 @@ function RolesSettings(): JSX.Element {
|
||||
</div>
|
||||
<div className="roles-settings-content">
|
||||
<div className="roles-settings-toolbar">
|
||||
<div className="roles-search-wrapper">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{IS_ROLE_DETAILS_AND_CRUD_ENABLED && (
|
||||
<Button
|
||||
variant="solid"
|
||||
|
||||
@@ -720,6 +720,10 @@ notifications - 2050
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.animate-fast-spin {
|
||||
animation: spin 0.5s linear infinite;
|
||||
}
|
||||
|
||||
// Custom legend tooltip for immediate display
|
||||
.legend-tooltip {
|
||||
position: fixed;
|
||||
|
||||
@@ -5586,10 +5586,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/ui@0.0.10":
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.10.tgz#cdbab838f8cb543cf5b483a86e9d9b65265b81ff"
|
||||
integrity sha512-XLeET+PgSP7heqKMsb9YZOSRT3TpfMPHNQRnY1I4SK8mXSct7BYWwK0Q3Je0uf4Z3aWOcpRYoRUPHWZQBpweFQ==
|
||||
"@signozhq/ui@0.0.12":
|
||||
version "0.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.12.tgz#b623c1729a0d85532d555fe7e756f3a4207e8e5d"
|
||||
integrity sha512-69XS/j9R+uTNMdupyjki/WK1j0d5K5j0/pJrINGiteQRRrPg/AOMue7v/W6dkLICRhXcz/mgI6tLeT2FAuzKFw==
|
||||
dependencies:
|
||||
"@chenglou/pretext" "^0.0.5"
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
@@ -5611,7 +5611,7 @@
|
||||
clsx "^2.1.1"
|
||||
cmdk "^1.1.1"
|
||||
dayjs "^1.11.10"
|
||||
lodash-es "^4.17.21"
|
||||
lodash-es "^4.18.1"
|
||||
motion "^11.11.17"
|
||||
next-themes "^0.4.6"
|
||||
nuqs "^2.8.9"
|
||||
@@ -13291,6 +13291,11 @@ lodash-es@4, lodash-es@^4.17.21:
|
||||
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash-es@^4.18.1:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
|
||||
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
|
||||
|
||||
lodash.camelcase@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz"
|
||||
|
||||
93
pkg/apiserver/signozapiserver/llmpricingrule.go
Normal file
93
pkg/apiserver/signozapiserver/llmpricingrule.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules", handler.New(
|
||||
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.List),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListLLMPricingRules",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "List pricing rules",
|
||||
Description: "Returns all LLM pricing rules for the authenticated org, with pagination.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
RequestQuery: new(llmpricingruletypes.ListPricingRulesQuery),
|
||||
Response: new(llmpricingruletypes.GettablePricingRules),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules", handler.New(
|
||||
provider.authZ.AdminAccess(provider.llmPricingRuleHandler.CreateOrUpdate),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateOrUpdateLLMPricingRules",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "Create or update pricing rules",
|
||||
Description: "Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.",
|
||||
Request: new(llmpricingruletypes.UpdatableLLMPricingRules),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
|
||||
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.Get),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetLLMPricingRule",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "Get a pricing rule",
|
||||
Description: "Returns a single LLM pricing rule by ID.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(llmpricingruletypes.GettableLLMPricingRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
|
||||
provider.authZ.AdminAccess(provider.llmPricingRuleHandler.Delete),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteLLMPricingRule",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "Delete a pricing rule",
|
||||
Description: "Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -67,6 +68,7 @@ type provider struct {
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -96,6 +98,7 @@ func NewFactory(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
@@ -130,6 +133,7 @@ func NewFactory(
|
||||
ruleStateHistoryHandler,
|
||||
spanMapperHandler,
|
||||
alertmanagerHandler,
|
||||
llmPricingRuleHandler,
|
||||
traceDetailHandler,
|
||||
rulerHandler,
|
||||
)
|
||||
@@ -166,6 +170,7 @@ func newProvider(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
@@ -202,6 +207,7 @@ func newProvider(
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -314,6 +320,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addLLMPricingRuleRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addTraceDetailRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.AdminAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.ViewAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
|
||||
ID: "GetHosts",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Get host info from Zeus.",
|
||||
@@ -39,7 +39,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
158
pkg/modules/llmpricingrule/impllmpricingrule/handler.go
Normal file
158
pkg/modules/llmpricingrule/impllmpricingrule/handler.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package impllmpricingrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const maxLimit = 100
|
||||
|
||||
type handler struct {
|
||||
module llmpricingrule.Module
|
||||
providerSettings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewHandler(module llmpricingrule.Module, providerSettings factory.ProviderSettings) llmpricingrule.Handler {
|
||||
return &handler{module: module, providerSettings: providerSettings}
|
||||
}
|
||||
|
||||
// List handles GET /api/v1/llm_pricing_rules.
|
||||
func (h *handler) List(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 := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var q llmpricingruletypes.ListPricingRulesQuery
|
||||
if err := binding.Query.BindQuery(r.URL.Query(), &q); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20
|
||||
} else if q.Limit > maxLimit {
|
||||
q.Limit = maxLimit
|
||||
}
|
||||
if q.Offset < 0 {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, llmpricingruletypes.ErrCodePricingRuleInvalidInput, "offset must be a non-negative integer"))
|
||||
return
|
||||
}
|
||||
|
||||
rules, total, err := h.module.List(ctx, orgID, q.Offset, q.Limit)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, llmpricingruletypes.NewGettableLLMPricingRulesFromLLMPricingRules(rules, total, q.Offset, q.Limit))
|
||||
}
|
||||
|
||||
// Get handles GET /api/v1/llm_pricing_rules/{id}.
|
||||
func (h *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 := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id, err := ruleIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := h.module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, rule)
|
||||
}
|
||||
|
||||
func (h *handler) CreateOrUpdate(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
req := new(llmpricingruletypes.UpdatableLLMPricingRules)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.module.CreateOrUpdate(ctx, orgID, claims.Email, req.Rules)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/v1/llm_pricing_rules/{id}.
|
||||
func (h *handler) Delete(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 := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id, err := ruleIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.Delete(ctx, orgID, id); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// ruleIDFromPath extracts and validates the {id} path variable.
|
||||
func ruleIDFromPath(r *http.Request) (valuer.UUID, error) {
|
||||
raw := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(raw)
|
||||
if err != nil {
|
||||
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, llmpricingruletypes.ErrCodePricingRuleInvalidInput, "id is not a valid uuid")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
24
pkg/modules/llmpricingrule/llmpricingrule.go
Normal file
24
pkg/modules/llmpricingrule/llmpricingrule.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package llmpricingrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*llmpricingruletypes.LLMPricingRule, int, error)
|
||||
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error)
|
||||
CreateOrUpdate(ctx context.Context, orgID valuer.UUID, userEmail string, rules []llmpricingruletypes.UpdatableLLMPricingRule) (err error)
|
||||
Delete(ctx context.Context, orgID, id valuer.UUID) error
|
||||
}
|
||||
|
||||
// Handler defines the HTTP handler interface for pricing rule endpoints.
|
||||
type Handler interface {
|
||||
List(rw http.ResponseWriter, r *http.Request)
|
||||
Get(rw http.ResponseWriter, r *http.Request)
|
||||
CreateOrUpdate(rw http.ResponseWriter, r *http.Request)
|
||||
Delete(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
@@ -265,15 +265,6 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: This should move to readAsRaw function in consume.go but for now we are keeping it here since it's only relevant for traces
|
||||
if q.spec.Signal == telemetrytypes.SignalTraces {
|
||||
if raw, ok := payload.(*qbtypes.RawData); ok {
|
||||
for _, rr := range raw.Rows {
|
||||
mergeSpanAttributeColumns(rr.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &qbtypes.Result{
|
||||
Type: q.kind,
|
||||
Value: payload,
|
||||
|
||||
@@ -431,45 +431,6 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mergeSpanAttributeColumns merges the typed ClickHouse span attribute columns
|
||||
// (attributes_string, attributes_number, attributes_bool, resources_string) into
|
||||
// unified "attributes" and "resource" keys, removing the raw columns.
|
||||
func mergeSpanAttributeColumns(data map[string]any) {
|
||||
attrStr := data["attributes_string"]
|
||||
attrNum := data["attributes_number"]
|
||||
attrBool := data["attributes_bool"]
|
||||
// todo(nitya): move to resource json
|
||||
resStr := data["resources_string"]
|
||||
|
||||
attributes := make(map[string]any)
|
||||
if m, ok := attrStr.(map[string]string); ok {
|
||||
for k, v := range m {
|
||||
attributes[k] = v
|
||||
}
|
||||
}
|
||||
if m, ok := attrNum.(map[string]float64); ok {
|
||||
for k, v := range m {
|
||||
attributes[k] = v
|
||||
}
|
||||
}
|
||||
if m, ok := attrBool.(map[string]bool); ok {
|
||||
for k, v := range m {
|
||||
attributes[k] = v
|
||||
}
|
||||
}
|
||||
delete(data, "attributes_string")
|
||||
delete(data, "attributes_number")
|
||||
delete(data, "attributes_bool")
|
||||
data["attributes"] = attributes
|
||||
|
||||
resource := map[string]string{}
|
||||
if m, ok := resStr.(map[string]string); ok {
|
||||
resource = m
|
||||
}
|
||||
data["resource"] = resource
|
||||
delete(data, "resources_string")
|
||||
}
|
||||
|
||||
// numericAsFloat converts numeric types to float64 efficiently.
|
||||
func numericAsFloat(v any) float64 {
|
||||
switch x := v.(type) {
|
||||
|
||||
@@ -85,13 +85,6 @@ func (q *traceOperatorQuery) executeWithContext(ctx context.Context, query strin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: This should move to readAsRaw function in consume.go but for now we can keep it here since it's only relevant for traces
|
||||
if raw, ok := payload.(*qbtypes.RawData); ok {
|
||||
for _, rr := range raw.Rows {
|
||||
mergeSpanAttributeColumns(rr.Data)
|
||||
}
|
||||
}
|
||||
|
||||
return &qbtypes.Result{
|
||||
Type: q.kind,
|
||||
Value: payload,
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields/implfields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule/impllmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
@@ -77,6 +79,7 @@ type Handlers struct {
|
||||
AlertmanagerHandler alertmanager.Handler
|
||||
TraceDetail tracedetail.Handler
|
||||
RulerHandler ruler.Handler
|
||||
LLMPricingRuleHandler llmpricingrule.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(
|
||||
@@ -121,5 +124,6 @@ func NewHandlers(
|
||||
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
|
||||
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
|
||||
RulerHandler: signozruler.NewHandler(rulerService),
|
||||
LLMPricingRuleHandler: impllmpricingrule.NewHandler(nil, providerSettings),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -77,6 +78,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ rulestatehistory.Handler }{},
|
||||
struct{ spanmapper.Handler }{},
|
||||
struct{ alertmanager.Handler }{},
|
||||
struct{ llmpricingrule.Handler }{},
|
||||
struct{ tracedetail.Handler }{},
|
||||
struct{ ruler.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
|
||||
@@ -283,6 +283,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.RuleStateHistory,
|
||||
handlers.SpanMapperHandler,
|
||||
handlers.AlertmanagerHandler,
|
||||
handlers.LLMPricingRuleHandler,
|
||||
handlers.TraceDetail,
|
||||
handlers.RulerHandler,
|
||||
),
|
||||
|
||||
@@ -1,50 +1,6 @@
|
||||
package telemetrytraces
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// Internal Columns.
|
||||
SpanTimestampBucketStartColumn = "ts_bucket_start"
|
||||
SpanResourceFingerPrintColumn = "resource_fingerprint"
|
||||
|
||||
// Intrinsic Columns.
|
||||
SpanTimestampColumn = "timestamp"
|
||||
SpanTraceIDColumn = "trace_id"
|
||||
SpanSpanIDColumn = "span_id"
|
||||
SpanTraceStateColumn = "trace_state"
|
||||
SpanParentSpanIDColumn = "parent_span_id"
|
||||
SpanFlagsColumn = "flags"
|
||||
SpanNameColumn = "name"
|
||||
SpanKindColumn = "kind"
|
||||
SpanKindStringColumn = "kind_string"
|
||||
SpanDurationNanoColumn = "duration_nano"
|
||||
SpanStatusCodeColumn = "status_code"
|
||||
SpanStatusMessageColumn = "status_message"
|
||||
SpanStatusCodeStringColumn = "status_code_string"
|
||||
SpanEventsColumn = "events"
|
||||
SpanLinksColumn = "links"
|
||||
|
||||
// Calculated Columns.
|
||||
SpanResponseStatusCodeColumn = "response_status_code"
|
||||
SpanExternalHTTPURLColumn = "external_http_url"
|
||||
SpanHTTPURLColumn = "http_url"
|
||||
SpanExternalHTTPMethodColumn = "external_http_method"
|
||||
SpanHTTPMethodColumn = "http_method"
|
||||
SpanHTTPHostColumn = "http_host"
|
||||
SpanDBNameColumn = "db_name"
|
||||
SpanDBOperationColumn = "db_operation"
|
||||
SpanHasErrorColumn = "has_error"
|
||||
SpanIsRemoteColumn = "is_remote"
|
||||
|
||||
// Contextual Columns.
|
||||
SpanAttributesStringColumn = "attributes_string"
|
||||
SpanAttributesNumberColumn = "attributes_number"
|
||||
SpanAttributesBoolColumn = "attributes_bool"
|
||||
SpanResourcesStringColumn = "resources_string"
|
||||
)
|
||||
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
|
||||
var (
|
||||
IntrinsicFields = map[string]telemetrytypes.TelemetryFieldKey{
|
||||
@@ -378,51 +334,6 @@ var (
|
||||
SpanSearchScopeRoot = "isroot"
|
||||
SpanSearchScopeEntryPoint = "isentrypoint"
|
||||
|
||||
// IntrinsicSpanFields lists the intrinsic span columns, in the order they
|
||||
// should appear when a raw query expands its SelectFields.
|
||||
IntrinsicSpanFields = []telemetrytypes.TelemetryFieldKey{
|
||||
{Name: SpanTimestampColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanTraceIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanSpanIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanTraceStateColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanParentSpanIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanFlagsColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanNameColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanKindColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanKindStringColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanDurationNanoColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanStatusCodeColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanStatusMessageColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanStatusCodeStringColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanEventsColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanLinksColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
}
|
||||
|
||||
// CalculatedSpanFields lists the calculated/derived span columns, in the
|
||||
// order they should appear when a raw query expands its SelectFields.
|
||||
CalculatedSpanFields = []telemetrytypes.TelemetryFieldKey{
|
||||
{Name: SpanResponseStatusCodeColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanExternalHTTPURLColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanHTTPURLColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanExternalHTTPMethodColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanHTTPMethodColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanHTTPHostColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanDBNameColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanDBOperationColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanHasErrorColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanIsRemoteColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
}
|
||||
|
||||
// ContextualSpanColumns lists the typed attribute and resource columns
|
||||
// selected raw (rather than via ColumnExpressionFor) so that consume.go
|
||||
// can merge them into unified "attributes" and "resource" maps.
|
||||
ContextualSpanColumns = []string{
|
||||
SpanAttributesStringColumn,
|
||||
SpanAttributesNumberColumn,
|
||||
SpanAttributesBoolColumn,
|
||||
SpanResourcesStringColumn,
|
||||
}
|
||||
|
||||
DefaultFields = map[string]telemetrytypes.TelemetryFieldKey{
|
||||
"timestamp": {
|
||||
Name: "timestamp",
|
||||
|
||||
@@ -78,16 +78,6 @@ func TestGetFieldKeyName(t *testing.T) {
|
||||
expectedResult: "multiIf(resource.`deployment.environment` IS NOT NULL, resource.`deployment.environment`::String, `resource_string_deployment$$environment_exists`==true, `resource_string_deployment$$environment`, NULL)",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Contextual map column - attributes_string without span context does not short-circuit",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
Name: SpanAttributesStringColumn,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
expectedResult: "attributes_string['attributes_string']",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Non-existent column",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -87,13 +89,40 @@ func (b *traceQueryStatementBuilder) Build(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isSelectFieldsEmpty := false
|
||||
/*
|
||||
Adding a tech debt note here:
|
||||
This piece of code is a hot fix and should be removed once we close issue: engineering-pod/issues/3622
|
||||
*/
|
||||
/*
|
||||
-------------------------------- Start of tech debt ----------------------------
|
||||
*/
|
||||
if requestType == qbtypes.RequestTypeRaw {
|
||||
isSelectFieldsEmpty = len(query.SelectFields) == 0
|
||||
// we are expanding here to ensure that all the conflicts are taken care in adjustKeys
|
||||
// i.e if there is a conflict we strip away context of the key in adjustKeys
|
||||
query = b.expandRawSelectFields(query)
|
||||
|
||||
selectedFields := query.SelectFields
|
||||
|
||||
if len(selectedFields) == 0 {
|
||||
sortedKeys := maps.Keys(DefaultFields)
|
||||
slices.Sort(sortedKeys)
|
||||
for _, key := range sortedKeys {
|
||||
selectedFields = append(selectedFields, DefaultFields[key])
|
||||
}
|
||||
query.SelectFields = selectedFields
|
||||
}
|
||||
|
||||
selectFieldKeys := []string{}
|
||||
for _, field := range selectedFields {
|
||||
selectFieldKeys = append(selectFieldKeys, field.Name)
|
||||
}
|
||||
|
||||
for _, x := range []string{"timestamp", "span_id", "trace_id"} {
|
||||
if !slices.Contains(selectFieldKeys, x) {
|
||||
query.SelectFields = append(query.SelectFields, DefaultFields[x])
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
-------------------------------- End of tech debt ----------------------------
|
||||
*/
|
||||
|
||||
query = b.adjustKeys(ctx, keys, query, requestType)
|
||||
|
||||
@@ -102,7 +131,7 @@ func (b *traceQueryStatementBuilder) Build(
|
||||
|
||||
switch requestType {
|
||||
case qbtypes.RequestTypeRaw:
|
||||
return b.buildListQuery(ctx, q, query, start, end, keys, variables, isSelectFieldsEmpty)
|
||||
return b.buildListQuery(ctx, q, query, start, end, keys, variables)
|
||||
case qbtypes.RequestTypeTimeSeries:
|
||||
return b.buildTimeSeriesQuery(ctx, q, query, start, end, keys, variables)
|
||||
case qbtypes.RequestTypeScalar:
|
||||
@@ -266,7 +295,6 @@ func (b *traceQueryStatementBuilder) buildListQuery(
|
||||
start, end uint64,
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
isSelectFieldsEmpty bool,
|
||||
) (*qbtypes.Statement, error) {
|
||||
|
||||
var (
|
||||
@@ -281,6 +309,7 @@ func (b *traceQueryStatementBuilder) buildListQuery(
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
|
||||
// TODO: should we deprecate `SelectFields` and return everything from a span like we do for logs?
|
||||
for _, field := range query.SelectFields {
|
||||
colExpr, err := b.fm.ColumnExpressionFor(ctx, start, end, &field, keys)
|
||||
if err != nil {
|
||||
@@ -289,12 +318,6 @@ func (b *traceQueryStatementBuilder) buildListQuery(
|
||||
sb.SelectMore(colExpr)
|
||||
}
|
||||
|
||||
if isSelectFieldsEmpty {
|
||||
for _, col := range ContextualSpanColumns {
|
||||
sb.SelectMore(col)
|
||||
}
|
||||
}
|
||||
|
||||
// From table
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
||||
|
||||
@@ -821,30 +844,3 @@ func (b *traceQueryStatementBuilder) buildResourceFilterCTE(
|
||||
variables,
|
||||
)
|
||||
}
|
||||
|
||||
// expandRawSelectFields populates SelectFields for raw (list view) queries.
|
||||
// It must be called before adjustKeys so that normalization runs over the full set.
|
||||
func (b *traceQueryStatementBuilder) expandRawSelectFields(query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation] {
|
||||
if len(query.SelectFields) == 0 {
|
||||
selectFields := make([]telemetrytypes.TelemetryFieldKey, 0, len(IntrinsicSpanFields)+len(CalculatedSpanFields))
|
||||
selectFields = append(selectFields, IntrinsicSpanFields...)
|
||||
selectFields = append(selectFields, CalculatedSpanFields...)
|
||||
query.SelectFields = selectFields
|
||||
return query
|
||||
}
|
||||
|
||||
selectFields := []telemetrytypes.TelemetryFieldKey{
|
||||
{Name: SpanTimestampColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanTraceIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
{Name: SpanSpanIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
|
||||
}
|
||||
for _, field := range query.SelectFields {
|
||||
// TODO(tvats): If a user specifies attribute.timestamp in the select fields, this loop will basically ignore it, as we already added a field by default. This can be fixed once we close https://github.com/SigNoz/engineering-pod/issues/3693
|
||||
if field.Name == SpanTimestampColumn || field.Name == SpanTraceIDColumn || field.Name == SpanSpanIDColumn {
|
||||
continue
|
||||
}
|
||||
selectFields = append(selectFields, field)
|
||||
}
|
||||
query.SelectFields = selectFields
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT name AS `name`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -468,7 +468,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, trace_state AS `trace_state`, parent_span_id AS `parent_span_id`, flags AS `flags`, name AS `name`, kind AS `kind`, kind_string AS `kind_string`, duration_nano AS `duration_nano`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -512,7 +512,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, response_status_code AS `responseStatusCode` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, response_status_code AS `responseStatusCode`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -556,7 +556,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, multiIf(toString(`attribute_string_mixed$$materialization$$key`) != '', toString(`attribute_string_mixed$$materialization$$key`), toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)) != '', toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)), NULL) AS `mixed.materialization.key` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, multiIf(toString(`attribute_string_mixed$$materialization$$key`) != '', toString(`attribute_string_mixed$$materialization$$key`), toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)) != '', toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)), NULL) AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -601,7 +601,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, `attribute_string_mixed$$materialization$$key` AS `mixed.materialization.key` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter 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 <= ?) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, `attribute_string_mixed$$materialization$$key` AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -711,7 +711,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, trace_state AS `trace_state`, parent_span_id AS `parent_span_id`, flags AS `flags`, name AS `name`, kind AS `kind`, kind_string AS `kind_string`, duration_nano AS `duration_nano`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -744,7 +744,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, trace_state AS `trace_state`, parent_span_id AS `parent_span_id`, flags AS `flags`, name AS `name`, kind AS `kind`, kind_string AS `kind_string`, duration_nano AS `duration_nano`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp AS `timestamp` asc LIMIT ?",
|
||||
Query: "SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp AS `timestamp` asc LIMIT ?",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
|
||||
185
pkg/types/llmpricingruletypes/pricing.go
Normal file
185
pkg/types/llmpricingruletypes/pricing.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package llmpricingruletypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodePricingRuleNotFound = errors.MustNewCode("pricing_rule_not_found")
|
||||
ErrCodePricingRuleInvalidInput = errors.MustNewCode("pricing_rule_invalid_input")
|
||||
)
|
||||
|
||||
type LLMPricingRuleUnit struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
UnitPerMillionTokens = LLMPricingRuleUnit{valuer.NewString("per_million_tokens")}
|
||||
)
|
||||
|
||||
type LLMPricingRuleCacheMode struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
// LLMPricingRuleCacheModeSubtract: cached tokens are inside input_tokens (OpenAI-style).
|
||||
LLMPricingRuleCacheModeSubtract = LLMPricingRuleCacheMode{valuer.NewString("subtract")}
|
||||
// LLMPricingRuleCacheModeAdditive: cached tokens are reported separately (Anthropic-style).
|
||||
LLMPricingRuleCacheModeAdditive = LLMPricingRuleCacheMode{valuer.NewString("additive")}
|
||||
// LLMPricingRuleCacheModeUnknown: provider behaviour is unknown; falls back to subtract.
|
||||
LLMPricingRuleCacheModeUnknown = LLMPricingRuleCacheMode{valuer.NewString("unknown")}
|
||||
)
|
||||
|
||||
// StringSlice is a []string that is stored as a JSON text column.
|
||||
// It is compatible with both SQLite and PostgreSQL.
|
||||
type StringSlice []string
|
||||
|
||||
// LLMRulePricing is the per-rule pricing shape, persisted as a single JSON.
|
||||
type LLMRulePricing struct {
|
||||
Input float64 `json:"input" required:"true"`
|
||||
Output float64 `json:"output" required:"true"`
|
||||
Cache *LLMPricingCacheCosts `json:"cache,omitempty"`
|
||||
}
|
||||
|
||||
type LLMPricingCacheCosts struct {
|
||||
Mode LLMPricingRuleCacheMode `json:"mode" required:"true"`
|
||||
Read float64 `json:"read"`
|
||||
Write float64 `json:"write"`
|
||||
}
|
||||
|
||||
type LLMPricingRule struct {
|
||||
bun.BaseModel `bun:"table:llm_pricing_rule,alias:llm_pricing_rule" json:"-"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
OrgID valuer.UUID `bun:"org_id,type:text,notnull" json:"orgId" required:"true"`
|
||||
SourceID *valuer.UUID `bun:"source_id,type:text" json:"sourceId,omitempty"`
|
||||
Model string `bun:"model,type:text,notnull" json:"modelName" required:"true"`
|
||||
Provider string `bun:"provider,type:text,notnull" json:"provider" required:"true"`
|
||||
ModelPattern StringSlice `bun:"model_pattern,type:text,notnull" json:"modelPattern" required:"true"`
|
||||
Unit LLMPricingRuleUnit `bun:"unit,type:text,notnull" json:"unit" required:"true"`
|
||||
Pricing LLMRulePricing `bun:"pricing,type:text,notnull,default:'{}'" json:"pricing" required:"true"`
|
||||
// IsOverride marks the row as user-pinned. When true, Zeus skips it entirely.
|
||||
IsOverride bool `bun:"is_override,notnull,default:false" json:"isOverride" required:"true"`
|
||||
SyncedAt *time.Time `bun:"synced_at" json:"syncedAt,omitempty"`
|
||||
Enabled bool `bun:"enabled,notnull,default:true" json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type GettableLLMPricingRule = LLMPricingRule
|
||||
|
||||
type StorableLLMPricingRule = LLMPricingRule
|
||||
|
||||
// UpdatableLLMPricingRule is one entry in the bulk upsert batch.
|
||||
//
|
||||
// Identification:
|
||||
// - ID set → match by id (user editing a known row).
|
||||
// - SourceID set → match by source_id (Zeus sync, or user editing a Zeus-synced row).
|
||||
// - neither set → insert a new row with source_id = NULL (user-created custom rule).
|
||||
//
|
||||
// IsOverride is a pointer so the caller can distinguish "not sent" from "set to false".
|
||||
// When IsOverride is nil AND the matched row has is_override = true, the row is fully
|
||||
// preserved — only synced_at is stamped.
|
||||
type UpdatableLLMPricingRule struct {
|
||||
ID *valuer.UUID `json:"id,omitempty"`
|
||||
SourceID *valuer.UUID `json:"sourceId,omitempty"`
|
||||
Model string `json:"modelName" required:"true"`
|
||||
Provider string `json:"provider" required:"true"`
|
||||
ModelPattern []string `json:"modelPattern" required:"true"`
|
||||
Unit LLMPricingRuleUnit `json:"unit" required:"true"`
|
||||
Pricing LLMRulePricing `json:"pricing" required:"true"`
|
||||
IsOverride *bool `json:"isOverride,omitempty"`
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type UpdatableLLMPricingRules struct {
|
||||
Rules []UpdatableLLMPricingRule `json:"rules" required:"true"`
|
||||
}
|
||||
|
||||
type ListPricingRulesQuery struct {
|
||||
Offset int `query:"offset" json:"offset"`
|
||||
Limit int `query:"limit" json:"limit"`
|
||||
}
|
||||
|
||||
type GettablePricingRules struct {
|
||||
Items []*GettableLLMPricingRule `json:"items" required:"true"`
|
||||
Total int `json:"total" required:"true"`
|
||||
Offset int `json:"offset" required:"true"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
func (LLMPricingRuleUnit) Enum() []any {
|
||||
return []any{UnitPerMillionTokens}
|
||||
}
|
||||
|
||||
func (LLMPricingRuleCacheMode) Enum() []any {
|
||||
return []any{LLMPricingRuleCacheModeSubtract, LLMPricingRuleCacheModeAdditive, LLMPricingRuleCacheModeUnknown}
|
||||
}
|
||||
|
||||
func (s StringSlice) Value() (driver.Value, error) {
|
||||
if s == nil {
|
||||
return "[]", nil
|
||||
}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (s *StringSlice) Scan(src any) error {
|
||||
var raw []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
raw = []byte(v)
|
||||
case []byte:
|
||||
raw = v
|
||||
case nil:
|
||||
*s = nil
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "llmpricingruletypes: cannot scan %T into StringSlice", src)
|
||||
}
|
||||
return json.Unmarshal(raw, s)
|
||||
}
|
||||
|
||||
func (p LLMRulePricing) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (p *LLMRulePricing) Scan(src any) error {
|
||||
var raw []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
raw = []byte(v)
|
||||
case []byte:
|
||||
raw = v
|
||||
case nil:
|
||||
*p = LLMRulePricing{}
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "llmpricingruletypes: cannot scan %T into LLMRulePricing", src)
|
||||
}
|
||||
return json.Unmarshal(raw, p)
|
||||
}
|
||||
|
||||
func NewGettableLLMPricingRulesFromLLMPricingRules(items []*LLMPricingRule, total, offset, limit int) *GettablePricingRules {
|
||||
return &GettablePricingRules{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
}
|
||||
}
|
||||
16
pkg/types/llmpricingruletypes/store.go
Normal file
16
pkg/types/llmpricingruletypes/store.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package llmpricingruletypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*StorableLLMPricingRule, int, error)
|
||||
Get(ctx context.Context, orgID, id valuer.UUID) (*StorableLLMPricingRule, error)
|
||||
GetBySourceID(ctx context.Context, orgID, sourceID valuer.UUID) (*StorableLLMPricingRule, error)
|
||||
Create(ctx context.Context, rule *StorableLLMPricingRule) error
|
||||
Update(ctx context.Context, rule *StorableLLMPricingRule) error
|
||||
Delete(ctx context.Context, orgID, id valuer.UUID) error
|
||||
}
|
||||
@@ -481,24 +481,25 @@ def test_traces_list(
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"disabled": False,
|
||||
"selectFields": [
|
||||
{"name": "span_id"},
|
||||
{"name": "span.timestamp"},
|
||||
{"name": "trace_id"},
|
||||
],
|
||||
"order": [{"key": {"name": "timestamp"}, "direction": "desc"}],
|
||||
"limit": 1,
|
||||
},
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
lambda x: [
|
||||
x[3].duration_nano,
|
||||
x[3].name,
|
||||
x[3].response_status_code,
|
||||
x[3].service_name,
|
||||
x[3].span_id,
|
||||
format_timestamp(x[3].timestamp),
|
||||
x[3].trace_id,
|
||||
], # type: Callable[[List[Traces]], List[Any]]
|
||||
),
|
||||
# Case 2: order by attribute timestamp field which is there in attributes as well
|
||||
# attribute.timestamp gets adjusted to span.timestamp
|
||||
# This should break but it doesn't because attribute.timestamp gets adjusted to timestamp
|
||||
# because of default trace.timestamp gets added by default and bug in field mapper picks
|
||||
# instrinsic field
|
||||
pytest.param(
|
||||
{
|
||||
"type": "builder_query",
|
||||
@@ -506,19 +507,16 @@ def test_traces_list(
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"disabled": False,
|
||||
"selectFields": [
|
||||
{"name": "span_id"},
|
||||
{"name": "span.timestamp"},
|
||||
{"name": "trace_id"},
|
||||
],
|
||||
"order": [
|
||||
{"key": {"name": "attribute.timestamp"}, "direction": "desc"}
|
||||
],
|
||||
"order": [{"key": {"name": "attribute.timestamp"}, "direction": "desc"}],
|
||||
"limit": 1,
|
||||
},
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
lambda x: [
|
||||
x[3].duration_nano,
|
||||
x[3].name,
|
||||
x[3].response_status_code,
|
||||
x[3].service_name,
|
||||
x[3].span_id,
|
||||
format_timestamp(x[3].timestamp),
|
||||
x[3].trace_id,
|
||||
@@ -544,7 +542,7 @@ def test_traces_list(
|
||||
], # type: Callable[[List[Traces]], List[Any]]
|
||||
),
|
||||
# Case 4: select attribute.timestamp with empty order by
|
||||
# This returns the one span which has attribute.timestamp
|
||||
# This doesn't return any data because of where_clause using aliased timestamp
|
||||
pytest.param(
|
||||
{
|
||||
"type": "builder_query",
|
||||
@@ -558,11 +556,7 @@ def test_traces_list(
|
||||
},
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
lambda x: [
|
||||
x[0].span_id,
|
||||
format_timestamp(x[0].timestamp),
|
||||
x[0].trace_id,
|
||||
], # type: Callable[[List[Traces]], List[Any]]
|
||||
lambda x: [], # type: Callable[[List[Traces]], List[Any]]
|
||||
),
|
||||
# Case 5: select timestamp with timestamp order by
|
||||
pytest.param(
|
||||
@@ -699,112 +693,6 @@ def test_traces_list_with_corrupt_data(
|
||||
assert data[key] == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"select_fields,status_code,expected_keys",
|
||||
[
|
||||
pytest.param(
|
||||
[],
|
||||
HTTPStatus.OK,
|
||||
[
|
||||
# all intrinsic column
|
||||
"timestamp",
|
||||
"trace_id",
|
||||
"span_id",
|
||||
"trace_state",
|
||||
"parent_span_id",
|
||||
"flags",
|
||||
"name",
|
||||
"kind",
|
||||
"kind_string",
|
||||
"duration_nano",
|
||||
"status_code",
|
||||
"status_message",
|
||||
"status_code_string",
|
||||
"events",
|
||||
"links",
|
||||
# all calculated columns
|
||||
"response_status_code",
|
||||
"external_http_url",
|
||||
"http_url",
|
||||
"external_http_method",
|
||||
"http_method",
|
||||
"http_host",
|
||||
"db_name",
|
||||
"db_operation",
|
||||
"has_error",
|
||||
"is_remote",
|
||||
# all contextual columns (merged in response layer)
|
||||
"attributes",
|
||||
"resource",
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{"name": "service.name"},
|
||||
],
|
||||
HTTPStatus.OK,
|
||||
["timestamp", "trace_id", "span_id", "service.name"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_traces_list_with_select_fields(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
select_fields: List[dict],
|
||||
status_code: HTTPStatus,
|
||||
expected_keys: List[str],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert 4 traces with different attributes.
|
||||
|
||||
Tests:
|
||||
1. Empty select fields should return all the fields.
|
||||
2. Non empty select field should return the select field along with timestamp, trace_id and span_id.
|
||||
"""
|
||||
traces = (
|
||||
generate_traces_with_corrupt_metadata()
|
||||
) # using this as the data doesn't matter
|
||||
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
payload = {
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"selectFields": select_fields,
|
||||
"order": [{"key": {"name": "timestamp"}, "direction": "desc"}],
|
||||
"limit": 1,
|
||||
},
|
||||
}
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int(
|
||||
(datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000
|
||||
),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type="raw",
|
||||
queries=[payload],
|
||||
)
|
||||
assert response.status_code == status_code
|
||||
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
data = response.json()
|
||||
assert len(data["data"]["data"]["results"][0]["rows"][0]["data"].keys()) == len(
|
||||
expected_keys
|
||||
)
|
||||
assert set(data["data"]["data"]["results"][0]["rows"][0]["data"].keys()) == set(
|
||||
expected_keys
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"order_by,aggregation_alias,expected_status",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user