diff --git a/deploy/docker-swarm/docker-compose.ha.yaml b/deploy/docker-swarm/docker-compose.ha.yaml index ae43f8b30c3..e15971d8adc 100644 --- a/deploy/docker-swarm/docker-compose.ha.yaml +++ b/deploy/docker-swarm/docker-compose.ha.yaml @@ -190,7 +190,7 @@ services: # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml signoz: !!merge <<: *db-depend - image: signoz/signoz:v0.125.1 + image: signoz/signoz:v0.126.0 ports: - "8080:8080" # signoz port # - "6060:6060" # pprof port diff --git a/deploy/docker-swarm/docker-compose.yaml b/deploy/docker-swarm/docker-compose.yaml index 5f877798af0..6215e56c0dc 100644 --- a/deploy/docker-swarm/docker-compose.yaml +++ b/deploy/docker-swarm/docker-compose.yaml @@ -117,7 +117,7 @@ services: # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml signoz: !!merge <<: *db-depend - image: signoz/signoz:v0.125.1 + image: signoz/signoz:v0.126.0 ports: - "8080:8080" # signoz port volumes: diff --git a/deploy/docker/docker-compose.ha.yaml b/deploy/docker/docker-compose.ha.yaml index 80a7d047eba..4cf0ce53814 100644 --- a/deploy/docker/docker-compose.ha.yaml +++ b/deploy/docker/docker-compose.ha.yaml @@ -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.125.1} + image: signoz/signoz:${VERSION:-v0.126.0} container_name: signoz ports: - "8080:8080" # signoz port diff --git a/deploy/docker/docker-compose.yaml b/deploy/docker/docker-compose.yaml index dcc61a74b71..0b14e88e121 100644 --- a/deploy/docker/docker-compose.yaml +++ b/deploy/docker/docker-compose.yaml @@ -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.125.1} + image: signoz/signoz:${VERSION:-v0.126.0} container_name: signoz ports: - "8080:8080" # signoz port diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 7e08ef3e805..040d2f1e901 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -102,7 +102,11 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { return <>{children}; } - if (pathname.startsWith('/ai-assistant/') && !isAIAssistantEnabled) { + if ( + (pathname === ROUTES.AI_ASSISTANT_BASE || + pathname.startsWith('/ai-assistant/')) && + !isAIAssistantEnabled + ) { return ; } diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index b548006d925..ea8172c5656 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -229,18 +229,18 @@ function App(): JSX.Element { } setRoutes((prev) => { - const hasAi = prev.some((r) => r.path === ROUTES.AI_ASSISTANT); + const hasAi = prev.some((r) => r.key === 'AI_ASSISTANT'); if (isAIAssistantEnabled === hasAi) { return prev; } if (isAIAssistantEnabled) { - const aiRoute = defaultRoutes.find((r) => r.path === ROUTES.AI_ASSISTANT); + const aiRoute = defaultRoutes.find((r) => r.key === 'AI_ASSISTANT'); if (!aiRoute) { return prev; } - return [...prev.filter((r) => r.path !== ROUTES.AI_ASSISTANT), aiRoute]; + return [...prev.filter((r) => r.key !== 'AI_ASSISTANT'), aiRoute]; } - return prev.filter((r) => r.path !== ROUTES.AI_ASSISTANT); + return prev.filter((r) => r.key !== 'AI_ASSISTANT'); }); }, [isLoggedInState, isAIAssistantEnabled]); @@ -254,6 +254,7 @@ function App(): JSX.Element { if ( pathname === ROUTES.ONBOARDING || pathname.startsWith('/public/dashboard/') || + pathname === '/ai-assistant' || pathname.startsWith('/ai-assistant/') ) { window.Pylon?.('hideChatBubble'); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index e0e0fdfc0fc..959b6e6046c 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -501,7 +501,7 @@ const routes: AppRoutes[] = [ isPrivate: true, }, { - path: ROUTES.AI_ASSISTANT, + path: [ROUTES.AI_ASSISTANT_BASE, ROUTES.AI_ASSISTANT], exact: true, component: AIAssistantPage, key: 'AI_ASSISTANT', diff --git a/frontend/src/api/AIAPIInstance.ts b/frontend/src/api/AIAPIInstance.ts index ace5a0bb7a8..684a1932022 100644 --- a/frontend/src/api/AIAPIInstance.ts +++ b/frontend/src/api/AIAPIInstance.ts @@ -40,6 +40,7 @@ export function setAIBackendUrl(url: string | null): void { if (aiBackendUrl === url) { return; } + aiBackendUrl = url; AIAssistantInstance.defaults.baseURL = url ? `${url}${AI_API_PATH}` : ''; } diff --git a/frontend/src/api/ai-assistant/sigNozAIAssistantAPI.schemas.ts b/frontend/src/api/ai-assistant/sigNozAIAssistantAPI.schemas.ts index baeb39533ff..9f695124590 100644 --- a/frontend/src/api/ai-assistant/sigNozAIAssistantAPI.schemas.ts +++ b/frontend/src/api/ai-assistant/sigNozAIAssistantAPI.schemas.ts @@ -37,6 +37,16 @@ export enum ApplyFilterSignalDTO { traces = 'traces', metrics = 'metrics', } +export enum ApprovalStateDTO { + pending = 'pending', + approved = 'approved', + rejected = 'rejected', + superseded = 'superseded', +} +export enum ApprovalActionTypeDTO { + modify = 'modify', + delete = 'delete', +} /** * Resolved approval (approved/rejected/superseded) anchored on the assistant message that proposed it. Pending approvals never appear here - they live at the top-level pendingApproval slot. */ @@ -63,16 +73,6 @@ export interface ApprovalActionSummaryDTO { resolvedAt: string; } -export enum ApprovalActionTypeDTO { - modify = 'modify', - delete = 'delete', -} -export enum ApprovalStateDTO { - pending = 'pending', - approved = 'approved', - rejected = 'rejected', - superseded = 'superseded', -} export type ApprovalSummaryDTODiff = { [key: string]: unknown }; export interface ApprovalSummaryDTO { @@ -139,6 +139,16 @@ export interface CancelRequestDTO { threadId: string; } +export enum ExecutionStateDTO { + queued = 'queued', + running = 'running', + awaiting_approval = 'awaiting_approval', + awaiting_clarification = 'awaiting_clarification', + resumed = 'resumed', + completed = 'completed', + failed = 'failed', + canceled = 'canceled', +} export interface CancelResponseDTO { /** * @type string @@ -153,6 +163,13 @@ export type ClarificationFieldDTOOptions = string[] | null; export type ClarificationFieldDTODefault = string | string[] | null; +export enum ClarificationFieldTypeDTO { + text = 'text', + number = 'number', + select = 'select', + multi_select = 'multi_select', + boolean = 'boolean', +} export interface ClarificationFieldDTO { /** * @type string @@ -175,13 +192,6 @@ export interface ClarificationFieldDTO { default?: ClarificationFieldDTODefault; } -export enum ClarificationFieldTypeDTO { - text = 'text', - number = 'number', - select = 'select', - multi_select = 'multi_select', - boolean = 'boolean', -} export enum ClarificationStateDTO { pending = 'pending', submitted = 'submitted', @@ -252,6 +262,79 @@ export interface ClarifyResponseDTO { executionId: string; } +/** + * Identifier exposed on the wire for each counter row. + + Mirrors the ``RateLimitCounterType`` model enum minus the cost + counter. The daily-cost limit is enforced internally (Redis + counter + 429 from the pre-flight gate) but never surfaced on the + customer-facing API: shipping the raw provider cost to tenant users + pins our public pricing model to what we pay Anthropic and forecloses + markup, per-seat bundling, or tiered pricing. Cost stays internal on + ``assistant_executions`` + Redis for billing. + */ +export enum CounterTypeNameDTO { + hourly_message = 'hourly_message', + daily_message = 'daily_message', + daily_token = 'daily_token', +} +/** + * "auto" if derived from current page; "mention" if explicitly @-picked. + */ +export enum MessageContextDTOSource { + auto = 'auto', + mention = 'mention', +} +/** + * Resource taxonomy. Use metadata.page for concrete page identity. + */ +export enum MessageContextDTOType { + dashboard = 'dashboard', + alert = 'alert', + saved_view = 'saved_view', + logs_explorer = 'logs_explorer', + traces_explorer = 'traces_explorer', + metrics_explorer = 'metrics_explorer', + service = 'service', +} +/** + * Required for resource-detail pages: dashboard_detail, panel_edit, panel_fullscreen, alert_edit, service_detail. Always required for saved_view (mention-only — no list page in V1). Must be null for list/index/draft/detail-as-metadata pages such as dashboard_list, alert_list, alert_new, alerts_triggered, services_list, log_detail, and trace_detail. + */ +export type MessageContextDTOResourceId = string | null; + +export type MessageContextDTOResourceName = string | null; + +export type MessageContextDTOMetadataAnyOf = { [key: string]: unknown }; + +/** + * Page-specific extras. metadata.page identifies list/draft pages and detail-as-metadata pages. Required non-empty metadata keys: panel_edit.widgetId, panel_fullscreen.widgetId, trace_detail.traceId. timeRange.{start,end} use Unix milliseconds. + */ +export type MessageContextDTOMetadata = MessageContextDTOMetadataAnyOf | null; + +export interface MessageContextDTO { + /** + * @enum auto,mention + * @type string + * @description "auto" if derived from current page; "mention" if explicitly @-picked. + */ + source: MessageContextDTOSource; + /** + * @enum dashboard,alert,saved_view,logs_explorer,traces_explorer,metrics_explorer,service + * @type string + * @description Resource taxonomy. Use metadata.page for concrete page identity. + */ + type: MessageContextDTOType; + /** + * @description Required for resource-detail pages: dashboard_detail, panel_edit, panel_fullscreen, alert_edit, service_detail. Always required for saved_view (mention-only — no list page in V1). Must be null for list/index/draft/detail-as-metadata pages such as dashboard_list, alert_list, alert_new, alerts_triggered, services_list, log_detail, and trace_detail. + */ + resourceId?: MessageContextDTOResourceId; + resourceName?: MessageContextDTOResourceName; + /** + * @description Page-specific extras. metadata.page identifies list/draft pages and detail-as-metadata pages. Required non-empty metadata keys: panel_edit.widgetId, panel_fullscreen.widgetId, trace_detail.traceId. timeRange.{start,end} use Unix milliseconds. + */ + metadata?: MessageContextDTOMetadata; +} + export type CreateMessageRequestDTOContexts = MessageContextDTO[] | null; export type CreateMessageRequestDTOForkFromMessageId = string | null; @@ -289,6 +372,16 @@ export interface CreateThreadResponseDTO { threadId: string; } +/** + * Single sub-error entry — matches Go ErrorsResponseerroradditional. + */ +export interface ErrorResponseAdditionalDTO { + /** + * @type string + */ + message: string; +} + export type ErrorBodyDTOErrors = ErrorResponseAdditionalDTO[] | null; export type ErrorBodyDTOUrl = string | null; @@ -321,26 +414,6 @@ export interface ErrorResponseDTO { error: ErrorBodyDTO; } -/** - * Single sub-error entry — matches Go ErrorsResponseerroradditional. - */ -export interface ErrorResponseAdditionalDTO { - /** - * @type string - */ - message: string; -} - -export enum ExecutionStateDTO { - queued = 'queued', - running = 'running', - awaiting_approval = 'awaiting_approval', - awaiting_clarification = 'awaiting_clarification', - resumed = 'resumed', - completed = 'completed', - failed = 'failed', - canceled = 'canceled', -} export enum FeedbackRatingDTO { positive = 'positive', negative = 'negative', @@ -356,6 +429,30 @@ export interface FeedbackResponseDTO { [key: string]: unknown; } +export type ValidationErrorDTOLocItem = string | number; + +export type ValidationErrorDTOCtx = { [key: string]: unknown }; + +export interface ValidationErrorDTO { + /** + * @type array + */ + loc: ValidationErrorDTOLocItem[]; + /** + * @type string + */ + msg: string; + /** + * @type string + */ + type: string; + input?: unknown; + /** + * @type object + */ + ctx?: ValidationErrorDTOCtx; +} + export interface HTTPValidationErrorDTO { /** * @type array @@ -393,6 +490,15 @@ export type MessageActionDTOQuery = MessageActionDTOQueryAnyOf | null; export type MessageActionDTOUrl = string | null; +export enum MessageActionKindDTO { + undo = 'undo', + revert = 'revert', + restore = 'restore', + follow_up = 'follow_up', + open_resource = 'open_resource', + open_docs = 'open_docs', + apply_filter = 'apply_filter', +} /** * Assistant action. Kind-specific requirements: rollback actions require actionMetadataId/resourceType/resourceId; follow_up requires input.intent; open_resource requires resourceType/resourceId; apply_filter requires signal and query; open_docs requires a SigNoz docs url. */ @@ -413,75 +519,9 @@ export interface MessageActionDTO { url?: MessageActionDTOUrl; } -export enum MessageActionKindDTO { - undo = 'undo', - revert = 'revert', - restore = 'restore', - follow_up = 'follow_up', - open_resource = 'open_resource', - open_docs = 'open_docs', - apply_filter = 'apply_filter', -} export enum MessageContentTypeDTO { markdown = 'markdown', } -/** - * "auto" if derived from current page; "mention" if explicitly @-picked. - */ -export enum MessageContextDTOSource { - auto = 'auto', - mention = 'mention', -} -/** - * Resource taxonomy. Use metadata.page for concrete page identity. - */ -export enum MessageContextDTOType { - dashboard = 'dashboard', - alert = 'alert', - saved_view = 'saved_view', - logs_explorer = 'logs_explorer', - traces_explorer = 'traces_explorer', - metrics_explorer = 'metrics_explorer', - service = 'service', -} -/** - * Required for resource-detail pages: dashboard_detail, panel_edit, panel_fullscreen, alert_edit, service_detail. Always required for saved_view (mention-only — no list page in V1). Must be null for list/index/draft/detail-as-metadata pages such as dashboard_list, alert_list, alert_new, alerts_triggered, services_list, log_detail, and trace_detail. - */ -export type MessageContextDTOResourceId = string | null; - -export type MessageContextDTOResourceName = string | null; - -export type MessageContextDTOMetadataAnyOf = { [key: string]: unknown }; - -/** - * Page-specific extras. metadata.page identifies list/draft pages and detail-as-metadata pages. Required non-empty metadata keys: panel_edit.widgetId, panel_fullscreen.widgetId, trace_detail.traceId. timeRange.{start,end} use Unix milliseconds. - */ -export type MessageContextDTOMetadata = MessageContextDTOMetadataAnyOf | null; - -export interface MessageContextDTO { - /** - * @enum auto,mention - * @type string - * @description "auto" if derived from current page; "mention" if explicitly @-picked. - */ - source: MessageContextDTOSource; - /** - * @enum dashboard,alert,saved_view,logs_explorer,traces_explorer,metrics_explorer,service - * @type string - * @description Resource taxonomy. Use metadata.page for concrete page identity. - */ - type: MessageContextDTOType; - /** - * @description Required for resource-detail pages: dashboard_detail, panel_edit, panel_fullscreen, alert_edit, service_detail. Always required for saved_view (mention-only — no list page in V1). Must be null for list/index/draft/detail-as-metadata pages such as dashboard_list, alert_list, alert_new, alerts_triggered, services_list, log_detail, and trace_detail. - */ - resourceId?: MessageContextDTOResourceId; - resourceName?: MessageContextDTOResourceName; - /** - * @description Page-specific extras. metadata.page identifies list/draft pages and detail-as-metadata pages. Required non-empty metadata keys: panel_edit.widgetId, panel_fullscreen.widgetId, trace_detail.traceId. timeRange.{start,end} use Unix milliseconds. - */ - metadata?: MessageContextDTOMetadata; -} - export enum MessageRoleDTO { user = 'user', assistant = 'assistant', @@ -616,6 +656,10 @@ export interface RevertRequestDTO { actionMetadataId: string; } +export enum ScopeDTO { + user = 'user', + org = 'org', +} export type ThreadDetailResponseDTOTitle = string | null; export type ThreadDetailResponseDTOState = ExecutionStateDTO | null; @@ -663,18 +707,6 @@ export interface ThreadDetailResponseDTO { export type ThreadListResponseDTONextCursor = string | null; -export interface ThreadListResponseDTO { - /** - * @type array - */ - threads: ThreadSummaryDTO[]; - nextCursor?: ThreadListResponseDTONextCursor; - /** - * @type boolean - */ - hasMore?: boolean; -} - export type ThreadSummaryDTOTitle = string | null; export type ThreadSummaryDTOState = ExecutionStateDTO | null; @@ -709,6 +741,18 @@ export interface ThreadSummaryDTO { updatedAt: string; } +export interface ThreadListResponseDTO { + /** + * @type array + */ + threads: ThreadSummaryDTO[]; + nextCursor?: ThreadListResponseDTONextCursor; + /** + * @type boolean + */ + hasMore?: boolean; +} + export interface UndoRequestDTO { /** * @type string @@ -726,28 +770,29 @@ export interface UpdateThreadRequestDTO { archived?: UpdateThreadRequestDTOArchived; } -export type ValidationErrorDTOLocItem = string | number; - -export type ValidationErrorDTOCtx = { [key: string]: unknown }; +export type UsageResponseDTONextPage = string | null; -export interface ValidationErrorDTO { - /** - * @type array - */ - loc: ValidationErrorDTOLocItem[]; - /** - * @type string - */ - msg: string; +/** + * One row in the ``GET /usage`` response. + */ +export interface UsageRowDTO { + type: CounterTypeNameDTO; + scope: ScopeDTO; + used: number; + limit: number; /** * @type string + * @format date-time */ - type: string; - input?: unknown; + resetsAt: string; +} + +export interface UsageResponseDTO { /** - * @type object + * @type array */ - ctx?: ValidationErrorDTOCtx; + data: UsageRowDTO[]; + nextPage?: UsageResponseDTONextPage; } export type ApprovalEventDTODiff = { [key: string]: unknown }; @@ -909,6 +954,20 @@ export interface ErrorEventDTO { retryAction?: RetryActionDTO; } +/** + * Per-connection SSE keep-alive emitted every `sse_heartbeat_interval_seconds`. + + Carries no `executionId` and no `eventId` — heartbeats are wire-level + keep-alives, not part of the replayable event log. + */ +export const HeartbeatEventDTOValue = { + /** + * @type string + */ + type: 'heartbeat', +} as const; +export type HeartbeatEventDTO = typeof HeartbeatEventDTOValue; + export type MessageActionEventDTOActionMetadataId = string | null; export type MessageActionEventDTOResourceType = string | null; @@ -1315,3 +1374,14 @@ export type SubmitFeedbackApiV1AssistantMessagesMessageIdFeedbackPostHeaders = { */ 'X-SigNoz-URL'?: string | null; }; + +export type GetUsageApiV1AssistantUsageGetHeaders = { + /** + * @description SigNoz auth token (Bearer or raw JWT) + */ + authorization?: string | null; + /** + * @description SigNoz instance base URL for multi-tenant deployments. Falls back to SIGNOZ_API_URL env var when omitted. + */ + 'X-SigNoz-URL'?: string | null; +}; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 32fb7bde78d..5dd5c61ea56 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -88,6 +88,7 @@ const ROUTES = { PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId', SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts', AI_ASSISTANT: '/ai-assistant/:conversationId', + AI_ASSISTANT_BASE: '/ai-assistant', AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview', MCP_SERVER: '/settings/mcp-server', } as const; diff --git a/frontend/src/container/AIAssistant/components/MessageBubble/MessageBubble.tsx b/frontend/src/container/AIAssistant/components/MessageBubble/MessageBubble.tsx index dedfcc85f8c..f13a5fdf26b 100644 --- a/frontend/src/container/AIAssistant/components/MessageBubble/MessageBubble.tsx +++ b/frontend/src/container/AIAssistant/components/MessageBubble/MessageBubble.tsx @@ -178,7 +178,7 @@ export default function MessageBubble({ - {!isUser && ( + {!isUser && !message.isRateLimitError && ( Promise, + set: StoreSetter, +): Promise { + for (let attempt = 0; attempt <= 1; attempt += 1) { + if (attempt > 0) { + // Drop any partial content/events from the previous attempt so the + // retried execution's stream isn't concatenated with the dead one. + set((s) => { + resetStreamingState(s, conversationId); + }); + } + // eslint-disable-next-line no-await-in-loop + const executionId = await start(); + const ctrl = newStreamController(conversationId); + try { + // eslint-disable-next-line no-await-in-loop + await runStreamingLoop(executionId, { + conversationId, + set, + signal: ctrl.signal, + }); + streamControllers.delete(conversationId); + return; + } catch (err) { + streamControllers.delete(conversationId); + if (err instanceof AuthExpiredError && attempt < 1) { + continue; + } + throw err; + } + } +} + /** * Runs one SSE execution stream, updating the per-conversation stream state. * * Breaks early and sets pendingApproval / pendingClarification when the * agent needs user input before it can continue. * - * Throws on `error` events — the caller's catch block handles UI feedback. + * On an `invalid_token` error event (e.g. MCP auth expired mid-execution), + * throws `AuthExpiredError` so the caller can re-issue the originating + * action via `streamWithAuthRetry`. We don't refresh here ourselves — the + * retry's REST call will 401 and the shared axios `interceptorRejected` + * handles rotation + replay. Throws on any other `error` event — the + * caller's catch block handles UI feedback. */ // eslint-disable-next-line sonarjs/cognitive-complexity async function runStreamingLoop( @@ -325,6 +388,15 @@ async function runStreamingLoop( }); break; } else if (event.type === 'error') { + // MCP/SigNoz auth expired mid-execution — signal the caller to + // re-issue the originating action. The retry's REST call will hit + // 401 and the shared axios `interceptorRejected` will rotate the + // access token + replay, so we don't refresh here ourselves. + // (Backend sets `retryAction: 'manual'`, so the failed execution + // can't itself be resumed — only a fresh one helps.) + if (event.error.code === 'invalid_token') { + throw new AuthExpiredError(); + } throw Object.assign(new Error(event.error.message), { retryAction: event.retryAction, }); @@ -412,13 +484,41 @@ function hasPendingInput(conversationId: string, get: StoreGetter): boolean { return Boolean(stream?.pendingApproval || stream?.pendingClarification); } +function parseErrorBody(value: unknown): string | null { + if (typeof value === 'string') { + try { + return parseErrorBody(JSON.parse(value)); + } catch { + return null; + } + } + const message = (value as ErrorResponseDTO | undefined)?.error?.message; + return typeof message === 'string' && message.length > 0 ? message : null; +} + /** - * Commits an error message and removes the stream entry. + * Returns the backend's `error.message` when `err` is a 429 axios response + * (typically from the threads API surface — createThread, sendMessage, approve, + * clarify, regenerate). Returns null for any other error so callers fall + * through to their generic copy. + */ +function rateLimitMessage(err: unknown): string | null { + if (axios.isAxiosError(err) && err.response?.status === 429) { + return parseErrorBody(err.response.data); + } + return null; +} + +/** + * Commits an error message and removes the stream entry. When `isRateLimit` + * is true, the committed message is flagged so the feedback/regenerate bar + * is hidden — clicking regenerate would just 429 again. */ function finalizeStreamingError( conversationId: string, errorContent: string, set: StoreSetter, + isRateLimit = false, ): void { set((s) => { const conv = s.conversations[conversationId]; @@ -428,6 +528,7 @@ function finalizeStreamingError( role: 'assistant', content: errorContent, createdAt: Date.now(), + ...(isRateLimit ? { isRateLimitError: true } : {}), }); conv.updatedAt = Date.now(); } @@ -801,7 +902,12 @@ export const useAIAssistantStore = create()( }); // Reconnect to SSE if backend execution is still running - // and we don't already have an active SSE reader for this thread + // and we don't already have an active SSE reader for this + // thread. No auth-retry wrapper here: on `invalid_token` + // there's no "originating action" to redo — reopening the + // same dead executionId would just re-emit the failure. + // Let the error bubble; the user can send a new message, + // which will go through `streamWithAuthRetry`. if ( detail.activeExecutionId && !streamControllers.has(threadId) && @@ -1052,14 +1158,12 @@ export const useAIAssistantStore = create()( } }); } - const executionId = await sendMessageToThread(threadId, text, contexts); - const ctrl = newStreamController(convId); - await runStreamingLoop(executionId, { - conversationId: convId, + const tid = threadId; + await streamWithAuthRetry( + convId, + () => sendMessageToThread(tid, text, contexts), set, - signal: ctrl.signal, - }); - streamControllers.delete(convId); + ); if (!hasPendingInput(convId, get)) { finalizeStreamingMessage(convId, set, get); @@ -1070,11 +1174,14 @@ export const useAIAssistantStore = create()( return; } console.error('[AIAssistant] sendMessage failed:', err); - const message = - err instanceof SSEStreamError && err.status === 429 - ? 'You sent that a bit too quickly. Please wait a moment and try again.' - : 'Something went wrong while fetching the response. Please try again.'; - finalizeStreamingError(convId, message, set); + const rateLimit = rateLimitMessage(err); + finalizeStreamingError( + convId, + rateLimit ?? + 'Something went wrong while fetching the response. Please try again.', + set, + rateLimit !== null, + ); } }, @@ -1094,14 +1201,11 @@ export const useAIAssistantStore = create()( }); try { - const executionId = await approveExecution(approvalId); - const ctrl = newStreamController(conversationId); - await runStreamingLoop(executionId, { + await streamWithAuthRetry( conversationId, + () => approveExecution(approvalId), set, - signal: ctrl.signal, - }); - streamControllers.delete(conversationId); + ); if (!hasPendingInput(conversationId, get)) { finalizeStreamingMessage(conversationId, set, get); } @@ -1110,10 +1214,13 @@ export const useAIAssistantStore = create()( return; } console.error('[AIAssistant] approveAction failed:', err); + const rateLimit = rateLimitMessage(err); finalizeStreamingError( conversationId, - 'Something went wrong while processing the approval. Please try again.', + rateLimit ?? + 'Something went wrong while processing the approval. Please try again.', set, + rateLimit !== null, ); } }, @@ -1176,14 +1283,11 @@ export const useAIAssistantStore = create()( }); try { - const executionId = await regenerateMessage(messageId); - const ctrl = newStreamController(conversationId); - await runStreamingLoop(executionId, { + await streamWithAuthRetry( conversationId, + () => regenerateMessage(messageId), set, - signal: ctrl.signal, - }); - streamControllers.delete(conversationId); + ); if (!hasPendingInput(conversationId, get)) { finalizeStreamingMessage(conversationId, set, get); } @@ -1192,10 +1296,13 @@ export const useAIAssistantStore = create()( return; } console.error('[AIAssistant] regenerateAssistantMessage failed:', err); + const rateLimit = rateLimitMessage(err); finalizeStreamingError( conversationId, - 'Something went wrong while regenerating the response. Please try again.', + rateLimit ?? + 'Something went wrong while regenerating the response. Please try again.', set, + rateLimit !== null, ); } }, @@ -1245,14 +1352,11 @@ export const useAIAssistantStore = create()( }); try { - const executionId = await clarifyExecution(clarificationId, answers); - const ctrl = newStreamController(conversationId); - await runStreamingLoop(executionId, { + await streamWithAuthRetry( conversationId, + () => clarifyExecution(clarificationId, answers), set, - signal: ctrl.signal, - }); - streamControllers.delete(conversationId); + ); if (!hasPendingInput(conversationId, get)) { finalizeStreamingMessage(conversationId, set, get); } @@ -1261,10 +1365,13 @@ export const useAIAssistantStore = create()( return; } console.error('[AIAssistant] submitClarification failed:', err); + const rateLimit = rateLimitMessage(err); finalizeStreamingError( conversationId, - 'Something went wrong while processing your answers. Please try again.', + rateLimit ?? + 'Something went wrong while processing your answers. Please try again.', set, + rateLimit !== null, ); } }, diff --git a/frontend/src/container/AIAssistant/types.ts b/frontend/src/container/AIAssistant/types.ts index 57eae04a48f..34ca31f9751 100644 --- a/frontend/src/container/AIAssistant/types.ts +++ b/frontend/src/container/AIAssistant/types.ts @@ -86,6 +86,11 @@ export interface Message { actions?: MessageActionDTO[]; /** Persisted feedback rating — set after user votes and the API confirms. */ feedbackRating?: FeedbackRating | null; + /** + * Set on client-side rate-limit error messages so the feedback/regenerate + * bar (copy/vote/regenerate) is hidden — retrying would just 429 again. + */ + isRateLimitError?: boolean; createdAt: number; } diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.module.scss b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.module.scss index bcb924c5444..ba20dc6bb0f 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.module.scss +++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.module.scss @@ -8,6 +8,7 @@ align-items: center; padding: 8px 16px; gap: 8px; + min-height: 52px; // KeyValueLabel renders with a global `.key-value-label` root; keep it from // shrinking on the trace details header. @@ -20,22 +21,35 @@ flex-shrink: 0; } -.filter { - min-width: 0; +.traceIdSection { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; } -.isExpanded { - max-width: none; - flex: 1; +.filterSection { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + margin-left: auto; } -.oldViewBtn { +.headerActions { + display: flex; + align-items: center; + gap: 8px; flex-shrink: 0; } -.analyticsBtn { - flex-shrink: 0; - margin-left: auto; +.filter { + min-width: 0; +} + +.isExpanded { + max-width: none; + flex: 1; } .subHeader { diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx index ba5e723b371..a634bcb6d22 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx +++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx @@ -21,6 +21,7 @@ import { ArrowLeft, CalendarClock, ChartPie, + CornerUpLeft, Server, Timer, } from '@signozhq/icons'; @@ -117,7 +118,7 @@ function TraceDetailsHeader({
{!isFilterExpanded && ( - <> +
)} {isDataLoaded && ( - <> +
{!isFilterExpanded && ( - <> - + +
+ + + + + Switch to legacy trace view +
+
)} -
+
setIsFilterExpanded(false)} />
- {!isFilterExpanded && ( - - )} - +
)}
diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx index 87d8fc25912..6ba24b468e5 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx +++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import type { MenuItem } from '@signozhq/ui/dropdown-menu'; import { Button } from '@signozhq/ui/button'; import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu'; -import { Ellipsis } from '@signozhq/icons'; +import { Settings2 } from '@signozhq/icons'; import { useTraceStore } from '../stores/traceStore'; @@ -93,7 +93,8 @@ function TraceOptionsMenu({ variant="ghost" size="icon" color="secondary" - prefix={} + aria-label="Trace options" + prefix={} /> ); diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx index 05dcc3531aa..bdb69d4e28c 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx +++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx @@ -6,6 +6,7 @@ import TraceDetailsHeader from '../TraceDetailsHeader'; const mockGoBack = jest.fn(); const mockPush = jest.fn(); +const mockReplace = jest.fn(); const mockHasInAppHistory = jest.fn(); jest.mock('lib/history', () => ({ @@ -13,13 +14,47 @@ jest.mock('lib/history', () => ({ default: { goBack: (): void => mockGoBack(), push: (path: string): void => mockPush(path), - replace: jest.fn(), + replace: (path: string): void => mockReplace(path), location: { pathname: '/', search: '' }, listen: (): (() => void) => (): void => undefined, }, hasInAppHistory: (): boolean => mockHasInAppHistory(), })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: (): { id: string } => ({ id: 'trace-123' }), +})); + +const mockSetLocalStorageKey = jest.fn(); +jest.mock('api/browser/localstorage/set', () => ({ + __esModule: true, + default: (key: string, value: string): void => + mockSetLocalStorageKey(key, value), +})); + +jest.mock( + '../../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters', + () => ({ + __esModule: true, + default: (): JSX.Element =>
, + }), +); + +jest.mock('../../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel', () => ({ + __esModule: true, + default: ({ isOpen }: { isOpen: boolean }): JSX.Element => ( +
+ ), +})); + +jest.mock('components/FieldsSelector', () => ({ + __esModule: true, + default: ({ isOpen }: { isOpen: boolean }): JSX.Element => ( +
+ ), +})); + const baseProps = { filterMetadata: { startTime: 0, @@ -58,3 +93,70 @@ describe('TraceDetailsHeader – back button', () => { expect(mockGoBack).not.toHaveBeenCalled(); }); }); + +describe('TraceDetailsHeader – action cluster', () => { + beforeEach(() => { + mockReplace.mockClear(); + mockSetLocalStorageKey.mockClear(); + }); + + it('does not render the action buttons while data is still loading', () => { + render(); + + expect( + screen.queryByRole('button', { name: /switch to legacy trace view/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /^analytics$/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /trace options/i }), + ).not.toBeInTheDocument(); + }); + + it('renders Legacy View, Analytics, and Settings action buttons once data is loaded', () => { + render(); + + expect( + screen.getByRole('button', { name: /switch to legacy trace view/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /^analytics$/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /trace options/i }), + ).toBeInTheDocument(); + }); + + it('routes to the legacy trace view and persists the preference on click', () => { + render(); + + fireEvent.click( + screen.getByRole('button', { name: /switch to legacy trace view/i }), + ); + + expect(mockSetLocalStorageKey).toHaveBeenCalledWith( + 'TRACE_DETAILS_PREFER_OLD_VIEW', + 'true', + ); + expect(mockReplace).toHaveBeenCalledTimes(1); + expect(mockReplace).toHaveBeenCalledWith( + expect.stringContaining('/trace-old/trace-123'), + ); + }); + + it('toggles the AnalyticsPanel open state when the Analytics button is clicked', () => { + render(); + + const panel = screen.getByTestId('analytics-panel'); + expect(panel).toHaveAttribute('data-open', 'false'); + + const analyticsBtn = screen.getByRole('button', { name: /^analytics$/i }); + + fireEvent.click(analyticsBtn); + expect(panel).toHaveAttribute('data-open', 'true'); + + fireEvent.click(analyticsBtn); + expect(panel).toHaveAttribute('data-open', 'false'); + }); +}); diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss index 04a30d81591..3cfdd961f24 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss +++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss @@ -3,12 +3,6 @@ align-items: center; gap: 12px; - // QuerySearch child sets `query-builder-search-v2` globally; size it to the - // search container by reaching into the descendant. - :global(.query-builder-search-v2) { - width: 100%; - } - // ToggleGroup children use generated class names; nest the global selectors // under the local row so they only apply inside this filter row. :global([class*='toggle-group']) { @@ -20,8 +14,43 @@ } } +// Expanded-mode root: grows to fill .filter wrapper, and lets the search +// input flex within. In collapsed mode none of these grow — the whole +// Filters region is content-sized (just the pill + result + toggle). .isExpanded { flex: 1; + + .searchInput { + flex: 1; + } + + .searchAndNav { + flex: 1; + } +} + +.categoryControls { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.searchInput { + display: flex; + align-items: center; + min-width: 0; +} + +.searchPill { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.searchAndNav { + display: flex; + align-items: center; + min-width: 0; } .searchContainer { @@ -29,6 +58,25 @@ min-width: 0; } +.resultActions { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.expandedActions { + display: flex; + align-items: center; + gap: 2px; +} + +.highlightControl { + display: flex; + align-items: center; + flex-shrink: 0; +} + .pill { display: flex; align-items: center; @@ -85,14 +133,6 @@ border-radius: 4px; } -.collapseBtn { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - box-shadow: none; -} - .highlightErrorsToggle { display: flex; align-items: center; @@ -100,37 +140,3 @@ flex-shrink: 0; white-space: nowrap; } - -.preNextToggle { - display: flex; - flex-shrink: 0; - gap: 12px; -} - -.preNextCount { - display: flex; - align-items: center; - margin: auto; - color: var(--l2-foreground); - font-family: 'Geist Mono'; - font-size: 12px; - font-weight: 400; - line-height: 18px; -} - -.filterStatus { - display: flex; - align-items: center; - gap: 6px; - flex-shrink: 0; - color: var(--l2-foreground); - font-family: 'Geist Mono'; - font-size: 12px; - font-weight: 400; - line-height: 18px; -} - -.hasError { - color: var(--destructive); - cursor: help; -} diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx index ed4b4a30f73..402ebbad43d 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx +++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx @@ -1,15 +1,7 @@ import { useCallback, useRef, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useCopyToClipboard } from 'react-use'; -import { - ChevronDown, - ChevronUp, - Copy, - Info, - Loader, - Search, - X, -} from '@signozhq/icons'; +import { ChevronsRight, Copy, Search, X } from '@signozhq/icons'; import { Switch } from '@signozhq/ui/switch'; import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group'; import { toast } from '@signozhq/ui/sonner'; @@ -21,7 +13,6 @@ import { TooltipTrigger, } from '@signozhq/ui/tooltip'; import { Typography } from '@signozhq/ui/typography'; -import { AxiosError } from 'axios'; import cx from 'classnames'; import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch'; import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils'; @@ -42,6 +33,7 @@ import { SpanCategory, useSpanCategoryFilter, } from './hooks/useSpanCategoryFilter'; +import QueryResult from './QueryResult'; import styles from './Filters.module.scss'; @@ -152,6 +144,16 @@ function Filters({ runQuery(expressionRef.current); }, [runQuery]); + const handleClear = useCallback((): void => { + setExpression(''); + expressionRef.current = ''; + setFilters({ items: [], op: 'AND' }); + setFilteredSpanIds([]); + onFilteredSpansChange?.([], false); + setCurrentSearchedIndex(0); + setNoData(false); + }, [onFilteredSpansChange]); + // Expression-based filter hooks const filterProps = { expression, @@ -266,164 +268,167 @@ function Filters({
); - const statusIndicators = ( - <> - {isFetching && } - {error && ( - - - - - API error - - - - {(error as AxiosError)?.message || 'Something went wrong'} - - - )} - {!error && noData && ( - - No results found - - )} - - ); + const hasExpression = expression.trim().length > 0; + const hasResults = filteredSpanIds.length > 0; - // --- COLLAPSED VIEW --- - if (!isExpanded) { - const pill = ( - /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ -
- - {expression || 'Search...'} - {expression && } -
- ); + const handlePrev = useCallback((): void => { + handlePrevNext(currentSearchedIndex - 1); + setCurrentSearchedIndex((prev) => prev - 1); + }, [currentSearchedIndex, handlePrevNext]); - return ( - -
- {expression ? ( - - {pill} - -
-
- Search query - -
-
{expression}
-
-
-
- ) : ( - pill - )} - {highlightErrorsToggle} - {statusIndicators} -
-
- ); - } + const handleNext = useCallback((): void => { + handlePrevNext(currentSearchedIndex + 1); + setCurrentSearchedIndex((prev) => prev + 1); + }, [currentSearchedIndex, handlePrevNext]); - // --- EXPANDED VIEW --- - return ( - -
- { - if (value) { - handleCategoryChange(value as SpanCategory); - } - }} - size="sm" - > - {categories.map((category) => ( - - {category} - - ))} - - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
{ - if (!containerRef.current?.contains(e.relatedTarget as Node)) { - handleBlur(); - } - }} - > - -
- {filteredSpanIds.length > 0 && ( -
- - {currentSearchedIndex + 1} / {filteredSpanIds.length} - + const pill = ( + /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ +
+ + {expression || 'Search...'} + {expression && } +
+ ); + + const pillWithPopover = expression ? ( + + {pill} + +
+
+ Search query -
+
{expression}
+
+
+
+ ) : ( + pill + ); + + // Mode-conditional render: only one of (pill | QuerySearch) is mounted + // at a time. Collapsing unmounts the editor — half-written queries are + // dropped, so collapse can't accidentally commit a malformed expression + // and fire an erroring /query_range request. + return ( + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
{ + const relatedTarget = e.relatedTarget as Node | null; + const blurredIntoSelf = !!containerRef.current?.contains(relatedTarget); + if (!blurredIntoSelf) { + handleBlur(); + } + }} + > + {isExpanded && ( +
+ { + if (value) { + handleCategoryChange(value as SpanCategory); + } }} + size="sm" > - - + {categories.map((category) => ( + + {category} + + ))} +
)} - - {highlightErrorsToggle} - {statusIndicators} + +
+ {isExpanded ? ( +
+
+ +
+
+ ) : ( +
{pillWithPopover}
+ )} +
+ +
+ + {isExpanded && ( +
+ {hasExpression && ( + + + + + Clear filter + + )} + + + + + Collapse filters + +
+ )} +
+ +
{highlightErrorsToggle}
); diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/QueryResult.module.scss b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/QueryResult.module.scss new file mode 100644 index 00000000000..d39e3e85cc6 --- /dev/null +++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/QueryResult.module.scss @@ -0,0 +1,32 @@ +.resultNavCount { + padding: 0 6px; + white-space: nowrap; + color: var(--l1-foreground); + font-family: 'Geist Mono'; + font-size: 12px; +} + +.resultNavDivider { + width: 1px; + height: 14px; + background: var(--l3-border); + margin: 0 4px; + flex-shrink: 0; +} + +.filterStatus { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + color: var(--l2-foreground); + font-family: 'Geist Mono'; + font-size: 12px; + font-weight: 400; + line-height: 18px; +} + +.hasError { + color: var(--destructive); + cursor: help; +} diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/QueryResult.tsx b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/QueryResult.tsx new file mode 100644 index 00000000000..0323bcf5b5c --- /dev/null +++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/QueryResult.tsx @@ -0,0 +1,111 @@ +import { ChevronDown, ChevronUp, Info, Loader } from '@signozhq/icons'; +import { Button } from '@signozhq/ui/button'; +import { + TooltipContent, + TooltipRoot, + TooltipTrigger, +} from '@signozhq/ui/tooltip'; +import { Typography } from '@signozhq/ui/typography'; +import { AxiosError } from 'axios'; +import cx from 'classnames'; + +import styles from './QueryResult.module.scss'; + +type QueryResultProps = { + hasExpression: boolean; + hasResults: boolean; + isFetching: boolean; + error: unknown; + noData: boolean; + currentIndex: number; + total: number; + onPrev: () => void; + onNext: () => void; + showNavigation?: boolean; +}; + +function QueryResult({ + hasExpression, + hasResults, + isFetching, + error, + noData, + currentIndex, + total, + onPrev, + onNext, + showNavigation = true, +}: QueryResultProps): JSX.Element | null { + if (!hasExpression) { + return null; + } + + let content: JSX.Element | null = null; + if (hasResults && showNavigation) { + // Prefer count over loader on refresh so stale results stay visible. + content = ( + <> + + {currentIndex + 1} / {total} + + + + + ); + } else if (isFetching) { + content = ; + } else if (error) { + content = ( + + + + + API error + + + + {(error as AxiosError)?.message || 'Something went wrong'} + + + ); + } else if (noData) { + content = ( + + No results found + + ); + } + + if (!content) { + return null; + } + + return ( + <> + {content} + {showNavigation && } + + ); +} + +QueryResult.defaultProps = { + showNavigation: true, +}; + +export default QueryResult; diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 60d9d8d7a1f..007dd197dea 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -825,4 +825,5 @@ body.ai-assistant-panel-open { // overrides :root { --input-focus-outline-width: 0; + --radius-2: 4px; } diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 2372e7a1534..29792ec9b52 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -135,4 +135,5 @@ export const routePermission: Record = { AI_ASSISTANT: ['ADMIN', 'EDITOR', 'VIEWER'], AI_ASSISTANT_ICON_PREVIEW: ['ADMIN', 'EDITOR', 'VIEWER'], MCP_SERVER: ['ADMIN', 'EDITOR', 'VIEWER'], + AI_ASSISTANT_BASE: ['ADMIN', 'EDITOR', 'VIEWER'], }; diff --git a/go.mod b/go.mod index ba76f0029d3..9ddec16ea95 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.7 require ( dario.cat/mergo v1.0.2 github.com/AfterShip/clickhouse-sql-parser v0.4.16 - github.com/ClickHouse/clickhouse-go/v2 v2.40.1 + github.com/ClickHouse/clickhouse-go/v2 v2.43.0 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd github.com/SigNoz/signoz-otel-collector v0.144.3 @@ -38,8 +38,8 @@ require ( github.com/mailru/easyjson v0.9.0 github.com/open-telemetry/opamp-go v0.22.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0 - github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 - github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c + github.com/openfga/api/proto v0.0.0-20260319214821-f153694bfc20 + github.com/openfga/language/pkg/go v0.2.1 github.com/opentracing/opentracing-go v1.2.0 github.com/perses/perses v0.53.1 github.com/pkg/errors v0.9.1 @@ -73,7 +73,7 @@ require ( go.opentelemetry.io/collector/pdata v1.54.0 go.opentelemetry.io/contrib/config v0.10.0 go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 @@ -81,7 +81,7 @@ require ( go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.1 golang.org/x/crypto v0.49.0 - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 golang.org/x/net v0.52.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 @@ -92,7 +92,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.35.3 - modernc.org/sqlite v1.40.1 + modernc.org/sqlite v1.48.2 ) require ( @@ -138,7 +138,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/muhlemmer/gu v0.3.1 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/perses/common v0.30.2 // indirect github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 // indirect github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect @@ -164,12 +164,12 @@ require ( golang.org/x/term v0.41.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - modernc.org/libc v1.66.10 // indirect + modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) @@ -182,7 +182,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect - github.com/ClickHouse/ch-go v0.67.0 // indirect + github.com/ClickHouse/ch-go v0.71.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Yiling-J/theine-go v0.6.2 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b @@ -229,7 +229,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.27.0 // indirect + github.com/google/cel-go v0.28.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.18.0 // indirect @@ -288,14 +288,14 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.148.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.148.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.148.0 // indirect - github.com/openfga/openfga v1.11.2 - github.com/paulmach/orb v0.11.1 // indirect + github.com/openfga/openfga v1.14.1 + github.com/paulmach/orb v0.12.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pierrec/lz4/v4 v4.1.23 // indirect + github.com/pierrec/lz4/v4 v4.1.25 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/pressly/goose/v3 v3.26.0 // indirect + github.com/pressly/goose/v3 v3.27.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/exporter-toolkit v0.15.1 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect @@ -305,7 +305,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect - github.com/segmentio/asm v1.2.0 // indirect + github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/backo-go v1.0.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil/v4 v4.25.12 // indirect @@ -381,7 +381,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.60.0 go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect @@ -394,12 +394,12 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.33.0 // indirect + golang.org/x/mod v0.34.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d google.golang.org/grpc v1.80.0 // indirect gopkg.in/telebot.v3 v3.3.8 // indirect k8s.io/client-go v0.35.3 // indirect diff --git a/go.sum b/go.sum index 35a6f9d778a..25aa37f14b3 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/AfterShip/clickhouse-sql-parser v0.4.16 h1:gpl+wXclYUKT0p4+gBq22XeRYWwEoZ9f35vogqMvkLQ= github.com/AfterShip/clickhouse-sql-parser v0.4.16/go.mod h1:W0Z82wJWkJxz2RVun/RMwxue3g7ut47Xxl+SFqdJGus= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= @@ -87,10 +87,10 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgv github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/ch-go v0.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc= -github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18= -github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4= -github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc= +github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= +github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= +github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE= +github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -493,8 +493,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= -github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= +github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -815,6 +815,10 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w= +github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM= +github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -833,8 +837,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nexucis/lamenv v0.5.2 h1:tK/u3XGhCq9qIoVNcXsK9LZb8fKopm0A5weqSRvHd7M= github.com/nexucis/lamenv v0.5.2/go.mod h1:HusJm6ltmmT7FMG8A750mOLuME6SHCsr2iFYxp5fFi0= github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= @@ -875,12 +879,12 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 h1:58mhO5nqkdka2Mpg5mijuZOHScX7reowhzRciwjFCU8= -github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67/go.mod h1:XDX4qYNBUM2Rsa2AbKPh+oocZc2zgme+EF2fFC6amVU= -github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c h1:xPbHNFG8QbPr/fpL7u0MPI0x74/BCLm7Sx02btL1m5Q= -github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c/go.mod h1:BG26d1Fk4GSg0wMj60TRJ6Pe4ka2WQ33akhO+mzt3t0= -github.com/openfga/openfga v1.11.2 h1:6vFZSSE0pyyt9qz320BgQLh/sHxZY5nfPOcJ3d5g8Bg= -github.com/openfga/openfga v1.11.2/go.mod h1:aCDb0gaWsU6dDAdC+zNOR2XC2W3lteGwKSkRWcSjGW8= +github.com/openfga/api/proto v0.0.0-20260319214821-f153694bfc20 h1:xdVG0EDz9Z9Uhd7YZ5OMN1F8tkAz/Dpgdjxd0cuTBJo= +github.com/openfga/api/proto v0.0.0-20260319214821-f153694bfc20/go.mod h1:XDX4qYNBUM2Rsa2AbKPh+oocZc2zgme+EF2fFC6amVU= +github.com/openfga/language/pkg/go v0.2.1 h1:nmVJTPfjvaJC2EWGcy8HrUyL15KkIfjjnmB3VFVeCts= +github.com/openfga/language/pkg/go v0.2.1/go.mod h1:wg+EuPmYIaM855F2uPygT1hJoWcoUxAoecgYC5akXsw= +github.com/openfga/openfga v1.14.1 h1:z43+jLcv8FjaKKRf4WlMYZsfSXLvetcxkO8D4vApEQY= +github.com/openfga/openfga v1.14.1/go.mod h1:AqMyFFi3y24Hko1mIME6ctOdCsCru2HA3uHX1vu9bMg= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -889,8 +893,8 @@ github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= -github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= +github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= @@ -904,8 +908,8 @@ github.com/perses/common v0.30.2/go.mod h1:DFtur1QPah2/ChXbKKhw7djYdwNgz27s5fPKp github.com/perses/perses v0.53.1 h1:9VY/6p9QWrZwPSV7qiwTMSOsgcB37Lb1AXKT0ORXc6I= github.com/perses/perses v0.53.1/go.mod h1:ro8fsgBkHYOdrL/MV+fdP9mflKzYCy/+gcbxiaReI/A= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU= -github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -923,8 +927,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM= +github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78= github.com/prometheus/alertmanager v0.31.1 h1:eAmIC42lzbWslHkMt693T36qdxfyZULswiHr681YS3Q= github.com/prometheus/alertmanager v0.31.1/go.mod h1:zWPQwhbLt2ybee8rL921UONeQ59Oncash+m/hGP17tU= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1010,8 +1014,8 @@ github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624 github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/segmentio/analytics-go/v3 v3.2.1 h1:G+f90zxtc1p9G+WigVyTR0xNfOghOGs/PYAlljLOyeg= github.com/segmentio/analytics-go/v3 v3.2.1/go.mod h1:p8owAF8X+5o27jmvUognuXxdtqvSGtD0ZrfY2kcS9bE= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -1310,8 +1314,8 @@ go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63 go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0/go.mod h1:34csimR1lUhdT5HH4Rii9aKPrvBcnFRwxLwcevsU+Kk= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 h1:c9r/G1CSw4dPI1jaNNG9RnQP+q4SvZnHciDQJVIvchU= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0/go.mod h1:gO9smoZe9KnZcJCqcB0lMmQ4Z5VEifYmjMTpnwtTSuQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/contrib/otelconf v0.18.0 h1:ciF2Gf00BWs0DnexKFZXcxg9kJ8r3SUW1LOzW3CsKA8= go.opentelemetry.io/contrib/otelconf v0.18.0/go.mod h1:FcP7k+JLwBLdOxS6qY6VQ/4b5VBntI6L6o80IMwhAeI= go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk= @@ -1330,8 +1334,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= @@ -1412,8 +1416,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1439,8 +1443,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1693,8 +1697,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1837,8 +1841,8 @@ google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76 google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1942,18 +1946,20 @@ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= -modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= -modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= -modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1962,8 +1968,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= -modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= +modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= @@ -1975,8 +1981,8 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/querier/querier.go b/pkg/querier/querier.go index 14cdad58330..980a81e08e9 100644 --- a/pkg/querier/querier.go +++ b/pkg/querier/querier.go @@ -84,244 +84,53 @@ func New( } } -// extractShiftFromBuilderQuery extracts the shift value from timeShift function if present. -func extractShiftFromBuilderQuery[T any](spec qbtypes.QueryBuilderQuery[T]) int64 { - for _, fn := range spec.Functions { - if fn.Name == qbtypes.FunctionNameTimeShift && len(fn.Args) > 0 { - switch v := fn.Args[0].Value.(type) { - case float64: - return int64(v) - case int64: - return v - case int: - return int64(v) - case string: - if shiftFloat, err := strconv.ParseFloat(v, 64); err == nil { - return int64(shiftFloat) - } - } - } - } - return 0 -} - -// adjustTimeRangeForShift adjusts the time range based on the shift value from timeShift function. -func adjustTimeRangeForShift[T any](spec qbtypes.QueryBuilderQuery[T], tr qbtypes.TimeRange, kind qbtypes.RequestType) qbtypes.TimeRange { - // Only apply time shift for time series and scalar queries - // Raw/list queries don't support timeshift - if kind != qbtypes.RequestTypeTimeSeries && kind != qbtypes.RequestTypeScalar { - return tr - } - - // Use the ShiftBy field if it's already populated, otherwise extract it - shiftBy := spec.ShiftBy - if shiftBy == 0 { - shiftBy = extractShiftFromBuilderQuery(spec) - } - - if shiftBy == 0 { - return tr - } - - // ShiftBy is in seconds, convert to milliseconds and shift backward in time - shiftMS := shiftBy * 1000 - return qbtypes.TimeRange{ - From: tr.From - uint64(shiftMS), - To: tr.To - uint64(shiftMS), - } -} - func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error) { tmplVars := req.Variables if tmplVars == nil { tmplVars = make(map[string]qbtypes.VariableItem) } + event := &qbtypes.QBEvent{ Version: "v5", NumberOfQueries: len(req.CompositeQuery.Queries), PanelType: req.RequestType.StringValue(), } - intervalWarnings := []string{} - - dependencyQueries := make(map[string]bool) - traceOperatorQueries := make(map[string]qbtypes.QueryBuilderTraceOperator) + q.populateQBEvent(event, req.CompositeQuery.Queries) - for _, query := range req.CompositeQuery.Queries { - if query.Type == qbtypes.QueryTypeTraceOperator { - if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok { - // Parse expression to find dependencies - if err := spec.ParseExpression(); err != nil { - return nil, err - } - - deps := spec.CollectReferencedQueries(spec.ParsedExpression) - for _, dep := range deps { - dependencyQueries[dep] = true - } - traceOperatorQueries[spec.Name] = spec - } - } + // TraceOperatorQuery leverages other queries defined in the rangeRequest + // Eg: C := A => B + // Need to create dependency map { "A": true, "B": true } + dependencyQueries, err := q.constructTraceOperatorDependencyMap(req.CompositeQuery.Queries) + if err != nil { + return nil, err } - // First pass: collect all metric names that need temporality - metricNames := make([]string, 0) - for idx, query := range req.CompositeQuery.Queries { - event.QueryType = query.Type.StringValue() - switch query.Type { - case qbtypes.QueryTypeBuilder: - if spec, ok := query.Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]); ok { - for _, agg := range spec.Aggregations { - if agg.MetricName != "" { - metricNames = append(metricNames, agg.MetricName) - } - } - } - // if step interval is not set, we set it ourselves with recommended value - // if step interval is set to value which could result in points more than - // allowed, we override it. - switch spec := query.Spec.(type) { - case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]: - event.TracesUsed = true - event.FilterApplied = spec.Filter != nil && spec.Filter.Expression != "" - event.GroupByApplied = len(spec.GroupBy) > 0 - if spec.StepInterval.Seconds() == 0 { - spec.StepInterval = qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.RecommendedStepInterval(req.Start, req.End)), - } - } - if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepInterval(req.Start, req.End)) { - newStep := qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.MinAllowedStepInterval(req.Start, req.End)), - } - intervalWarnings = append(intervalWarnings, fmt.Sprintf(intervalWarn, spec.Name, spec.StepInterval.Seconds(), newStep.Seconds())) - spec.StepInterval = newStep - } - req.CompositeQuery.Queries[idx].Spec = spec - case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]: - event.LogsUsed = true - event.FilterApplied = spec.Filter != nil && spec.Filter.Expression != "" - event.GroupByApplied = len(spec.GroupBy) > 0 - if spec.StepInterval.Seconds() == 0 { - spec.StepInterval = qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.RecommendedStepInterval(req.Start, req.End)), - } - } - if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepInterval(req.Start, req.End)) { - newStep := qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.MinAllowedStepInterval(req.Start, req.End)), - } - intervalWarnings = append(intervalWarnings, fmt.Sprintf(intervalWarn, spec.Name, spec.StepInterval.Seconds(), newStep.Seconds())) - spec.StepInterval = newStep - } - req.CompositeQuery.Queries[idx].Spec = spec - case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]: - event.MetricsUsed = true - event.FilterApplied = spec.Filter != nil && spec.Filter.Expression != "" - event.GroupByApplied = len(spec.GroupBy) > 0 - - if spec.Source == telemetrytypes.SourceMeter { - if spec.StepInterval.Seconds() == 0 { - spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMeter(req.Start, req.End))} - } - - if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End)) { - newStep := qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End)), - } - spec.StepInterval = newStep - } - } else { - if spec.StepInterval.Seconds() == 0 { - spec.StepInterval = qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMetric(req.Start, req.End)), - } - } - if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End)) { - newStep := qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End)), - } - intervalWarnings = append(intervalWarnings, fmt.Sprintf(intervalWarn, spec.Name, spec.StepInterval.Seconds(), newStep.Seconds())) - spec.StepInterval = newStep - } - } - req.CompositeQuery.Queries[idx].Spec = spec - } - case qbtypes.QueryTypePromQL: - event.MetricsUsed = true - switch spec := query.Spec.(type) { - case qbtypes.PromQuery: - if spec.Step.Seconds() == 0 { - spec.Step = qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMetric(req.Start, req.End)), - } - } - req.CompositeQuery.Queries[idx].Spec = spec - } - case qbtypes.QueryTypeClickHouseSQL: - switch spec := query.Spec.(type) { - case qbtypes.ClickHouseQuery: - if strings.TrimSpace(spec.Query) != "" { - event.MetricsUsed = strings.Contains(spec.Query, "signoz_metrics") - event.LogsUsed = strings.Contains(spec.Query, "signoz_logs") - event.TracesUsed = strings.Contains(spec.Query, "signoz_traces") - } - } - case qbtypes.QueryTypeTraceOperator: - if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok { - if spec.StepInterval.Seconds() == 0 { - spec.StepInterval = qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.RecommendedStepInterval(req.Start, req.End)), - } - } - - if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepInterval(req.Start, req.End)) { - newStep := qbtypes.Step{ - Duration: time.Second * time.Duration(querybuilder.MinAllowedStepInterval(req.Start, req.End)), - } - intervalWarnings = append(intervalWarnings, fmt.Sprintf(intervalWarn, spec.Name, spec.StepInterval.Seconds(), newStep.Seconds())) - spec.StepInterval = newStep - } - req.CompositeQuery.Queries[idx].Spec = spec - } - } - } + // Step interval is the aggregation parameter for timeseries requests. + // We need to set if it is unspecified or adjust it if value is not within recommended range + intervalWarnings := q.adjustStepInterval(req.CompositeQuery.Queries, req.Start, req.End) queries := make(map[string]qbtypes.Query) steps := make(map[string]qbtypes.Step) - missingMetrics := []string{} - missingMetricQueries := []string{} - for _, query := range req.CompositeQuery.Queries { - var queryName string - var isTraceOperator bool + // Resolve metric metadata once per request: patches each metric-aggregation + // query's spec in place, returns the queries whose every aggregation was + // missing (used for preseeded empty results), and any dormant-metric + // warning string. NotFound errors for never-seen metrics are propagated. + missingMetricQueries, dormantMetricsWarningMsg, err := q.resolveMetricMetadata(ctx, req.CompositeQuery.Queries, req.Start, req.End) + if err != nil { + return nil, err + } + missingMetricQuerySet := make(map[string]bool, len(missingMetricQueries)) + for _, name := range missingMetricQueries { + missingMetricQuerySet[name] = true + } - switch query.Type { - case qbtypes.QueryTypeTraceOperator: - if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok { - queryName = spec.Name - isTraceOperator = true - } - case qbtypes.QueryTypePromQL: - if spec, ok := query.Spec.(qbtypes.PromQuery); ok { - queryName = spec.Name - } - case qbtypes.QueryTypeClickHouseSQL: - if spec, ok := query.Spec.(qbtypes.ClickHouseQuery); ok { - queryName = spec.Name - } - case qbtypes.QueryTypeBuilder: - switch spec := query.Spec.(type) { - case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]: - queryName = spec.Name - case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]: - queryName = spec.Name - case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]: - queryName = spec.Name - } - } + for _, query := range req.CompositeQuery.Queries { + queryName := query.GetQueryName() - if !isTraceOperator && dependencyQueries[queryName] { + // skip if it is dependecy of traceOperatorQuery + if query.GetType() != qbtypes.QueryTypeTraceOperator && dependencyQueries[queryName] { continue } @@ -376,40 +185,13 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype queries[spec.Name] = bq steps[spec.Name] = spec.StepInterval case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]: - var metricTemporality map[string]metrictypes.Temporality - var metricTypes map[string]metrictypes.Type - if len(metricNames) > 0 { - var err error - metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...) - if err != nil { - q.logger.WarnContext(ctx, "failed to fetch metric temporality", errors.Attr(err), slog.Any("metrics", metricNames)) - return nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality") - } - q.logger.DebugContext(ctx, "fetched metric temporalities and types", slog.Any("metric_temporality", metricTemporality), slog.Any("metric_types", metricTypes)) - } - presentAggregations := []qbtypes.MetricAggregation{} - for i := range spec.Aggregations { - if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Temporality == metrictypes.Unknown { - if temp, ok := metricTemporality[spec.Aggregations[i].MetricName]; ok && temp != metrictypes.Unknown { - spec.Aggregations[i].Temporality = temp - } - } - if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType { - if foundMetricType, ok := metricTypes[spec.Aggregations[i].MetricName]; ok && foundMetricType != metrictypes.UnspecifiedType { - spec.Aggregations[i].Type = foundMetricType - } - } - if spec.Aggregations[i].Type == metrictypes.UnspecifiedType { - missingMetrics = append(missingMetrics, spec.Aggregations[i].MetricName) - continue - } - presentAggregations = append(presentAggregations, spec.Aggregations[i]) - } - if len(presentAggregations) == 0 { - missingMetricQueries = append(missingMetricQueries, spec.Name) + // Spec was already patched by resolveMetricMetadata. Queries + // whose every aggregation was missing live in + // missingMetricQuerySet and produce empty preseeded results + // rather than running here. + if missingMetricQuerySet[spec.Name] { continue } - spec.Aggregations = presentAggregations spec.ShiftBy = extractShiftFromBuilderQuery(spec) timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType) var bq *builderQuery[qbtypes.MetricAggregation] @@ -428,38 +210,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype } } } - nonExistentMetrics := []string{} - var dormantMetricsWarningMsg string - if len(missingMetrics) > 0 { - lastSeenInfo, _ := q.metadataStore.FetchLastSeenInfoMulti(ctx, missingMetrics...) - for _, missingMetricName := range missingMetrics { - if ts, ok := lastSeenInfo[missingMetricName]; ok && ts > 0 { - continue - } - nonExistentMetrics = append(nonExistentMetrics, missingMetricName) - } - if len(nonExistentMetrics) == 1 { - return nil, errors.NewNotFoundf(errors.CodeNotFound, "could not find the metric %s", nonExistentMetrics[0]) - } else if len(nonExistentMetrics) > 1 { - return nil, errors.NewNotFoundf(errors.CodeNotFound, "the following metrics were not found: %s", strings.Join(nonExistentMetrics, ", ")) - } - lastSeenStr := func(name string) string { - if ts, ok := lastSeenInfo[name]; ok && ts > 0 { - ago := humanize.RelTime(time.UnixMilli(ts), time.Now(), "ago", "from now") - return fmt.Sprintf("%s (last seen %s)", name, ago) - } - return name // this case won't come cuz lastSeenStr is never called for metrics in nonExistentMetrics - } - if len(missingMetrics) == 1 { - dormantMetricsWarningMsg = fmt.Sprintf("no data found for the metric %s in the query time range", lastSeenStr(missingMetrics[0])) - } else { - parts := make([]string, len(missingMetrics)) - for i, m := range missingMetrics { - parts[i] = lastSeenStr(m) - } - dormantMetricsWarningMsg = fmt.Sprintf("no data found for the following metrics in the query time range: %s", strings.Join(parts, ", ")) - } - } preseededResults := make(map[string]any) for _, name := range missingMetricQueries { // at this point missing metrics will not have any non existent metrics, only normal ones switch req.RequestType { @@ -496,6 +246,166 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype return qbResp, qbErr } +func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.QueryEnvelope) { + for _, query := range queries { + // BUG: QueryType doesn't make sense as range_request can have multiple query types. + event.QueryType = query.Type.StringValue() + + switch query.Type { + case qbtypes.QueryTypeBuilder: + filter := query.GetFilter() + event.FilterApplied = event.FilterApplied || (filter != nil && filter.Expression != "") + event.GroupByApplied = event.GroupByApplied || len(query.GetGroupBy()) > 0 + switch query.Spec.(type) { + case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]: + event.TracesUsed = true + case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]: + event.LogsUsed = true + case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]: + event.MetricsUsed = true + } + case qbtypes.QueryTypePromQL: + event.MetricsUsed = true + case qbtypes.QueryTypeTraceOperator: + event.TracesUsed = true + case qbtypes.QueryTypeClickHouseSQL: + sql := query.GetQuery() + if strings.TrimSpace(sql) != "" { + event.MetricsUsed = strings.Contains(sql, "signoz_metrics") + event.LogsUsed = strings.Contains(sql, "signoz_logs") + event.TracesUsed = strings.Contains(sql, "signoz_traces") + } + } + } +} + +// resolveMetricMetadata fetches metadata for every metric referenced by builder +// metric-aggregation queries, patches each query's aggregations in place with +// the resolved values, and classifies any metric that could not be resolved. +// +// Side effects on queries: +// - Aggregations with Unknown Temporality / UnspecifiedType are filled in from +// the metadata store. +// - Aggregations whose Type is still UnspecifiedType after the patch are +// dropped from the spec. +// - Queries whose entire aggregation list was dropped are NOT patched and are +// surfaced via the returned missingMetricQueries; the caller should skip +// them. +// +// Returns: +// - missingMetricQueries: names of queries whose every aggregation was +// missing. Used downstream to preseed empty result placeholders so the +// response still has an entry per requested query name. +// - dormantWarning: a human-readable warning describing metrics that exist in +// the store but produced no data within the query window. Empty when no +// such metrics are present. +// - err: NotFound when one or more referenced metrics have never been seen, +// or Internal when a metadata fetch fails. +func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, dormantWarning string, err error) { + metricNames := make([]string, 0) + for idx := range queries { + if queries[idx].Type != qbtypes.QueryTypeBuilder { + continue + } + spec, ok := queries[idx].Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]) + if !ok { + continue + } + for _, agg := range spec.Aggregations { + if agg.MetricName != "" { + metricNames = append(metricNames, agg.MetricName) + } + } + } + + if len(metricNames) == 0 { + return nil, "", nil + } + + metricTemporality, metricTypes, err := q.metadataStore.FetchTemporalityAndTypeMulti(ctx, start, end, metricNames...) + if err != nil { + q.logger.WarnContext(ctx, "failed to fetch metric temporality", errors.Attr(err), slog.Any("metrics", metricNames)) + return nil, "", errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality") + } + q.logger.DebugContext(ctx, "fetched metric temporalities and types", slog.Any("metric_temporality", metricTemporality), slog.Any("metric_types", metricTypes)) + + missingMetrics := []string{} + for idx := range queries { + if queries[idx].Type != qbtypes.QueryTypeBuilder { + continue + } + spec, ok := queries[idx].Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]) + if !ok { + continue + } + + presentAggregations := make([]qbtypes.MetricAggregation, 0, len(spec.Aggregations)) + for i := range spec.Aggregations { + if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Temporality == metrictypes.Unknown { + if temp, ok := metricTemporality[spec.Aggregations[i].MetricName]; ok && temp != metrictypes.Unknown { + spec.Aggregations[i].Temporality = temp + } + } + if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType { + if foundMetricType, ok := metricTypes[spec.Aggregations[i].MetricName]; ok && foundMetricType != metrictypes.UnspecifiedType { + spec.Aggregations[i].Type = foundMetricType + } + } + if spec.Aggregations[i].Type == metrictypes.UnspecifiedType { + missingMetrics = append(missingMetrics, spec.Aggregations[i].MetricName) + continue + } + presentAggregations = append(presentAggregations, spec.Aggregations[i]) + } + if len(presentAggregations) == 0 { + missingMetricQueries = append(missingMetricQueries, spec.Name) + continue + } + spec.Aggregations = presentAggregations + queries[idx].Spec = spec + } + + if len(missingMetrics) == 0 { + return missingMetricQueries, "", nil + } + + // Classify each missing metric: never-seen → NotFound error; seen-but-no- + // data-in-window → dormant warning. + lastSeenInfo, _ := q.metadataStore.FetchLastSeenInfoMulti(ctx, missingMetrics...) + nonExistentMetrics := []string{} + for _, name := range missingMetrics { + if ts, ok := lastSeenInfo[name]; ok && ts > 0 { + continue + } + nonExistentMetrics = append(nonExistentMetrics, name) + } + if len(nonExistentMetrics) == 1 { + return nil, "", errors.NewNotFoundf(errors.CodeNotFound, "could not find the metric %s", nonExistentMetrics[0]) + } + if len(nonExistentMetrics) > 1 { + return nil, "", errors.NewNotFoundf(errors.CodeNotFound, "the following metrics were not found: %s", strings.Join(nonExistentMetrics, ", ")) + } + + // All missing metrics are dormant — assemble the warning string. + lastSeenStr := func(name string) string { + if ts, ok := lastSeenInfo[name]; ok && ts > 0 { + ago := humanize.RelTime(time.UnixMilli(ts), time.Now(), "ago", "from now") + return fmt.Sprintf("%s (last seen %s)", name, ago) + } + return name + } + if len(missingMetrics) == 1 { + dormantWarning = fmt.Sprintf("no data found for the metric %s in the query time range", lastSeenStr(missingMetrics[0])) + } else { + parts := make([]string, len(missingMetrics)) + for i, m := range missingMetrics { + parts[i] = lastSeenStr(m) + } + dormantWarning = fmt.Sprintf("no data found for the following metrics in the query time range: %s", strings.Join(parts, ", ")) + } + return missingMetricQueries, dormantWarning, nil +} + func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream) { event := &qbtypes.QBEvent{ @@ -1093,3 +1003,129 @@ func (q *querier) mergeTimeSeriesResults(cachedValue *qbtypes.TimeSeriesData, fr return result } + +func secondsStep(s uint64) qbtypes.Step { + return qbtypes.Step{Duration: time.Second * time.Duration(s)} +} + +// clampStep sets the step to recommended when zero and clamps to min when below it. +// When clamped and warn is true, a warning is appended for the user. +func clampStep(qe *qbtypes.QueryEnvelope, recommended, min uint64, warnings *[]string) { + step := qe.GetStepInterval() + if step.Seconds() == 0 { + step = secondsStep(recommended) + qe.SetStepInterval(step) + } + if step.Seconds() < float64(min) { + newStep := secondsStep(min) + *warnings = append(*warnings, fmt.Sprintf(intervalWarn, qe.GetQueryName(), step.Seconds(), newStep.Seconds())) + qe.SetStepInterval(newStep) + } +} + +// extractShiftFromBuilderQuery extracts the shift value from timeShift function if present. +func extractShiftFromBuilderQuery[T any](spec qbtypes.QueryBuilderQuery[T]) int64 { + for _, fn := range spec.Functions { + if fn.Name == qbtypes.FunctionNameTimeShift && len(fn.Args) > 0 { + switch v := fn.Args[0].Value.(type) { + case float64: + return int64(v) + case int64: + return v + case int: + return int64(v) + case string: + if shiftFloat, err := strconv.ParseFloat(v, 64); err == nil { + return int64(shiftFloat) + } + } + } + } + return 0 +} + +// adjustTimeRangeForShift adjusts the time range based on the shift value from timeShift function. +func adjustTimeRangeForShift[T any](spec qbtypes.QueryBuilderQuery[T], tr qbtypes.TimeRange, kind qbtypes.RequestType) qbtypes.TimeRange { + // Only apply time shift for time series and scalar queries + // Raw/list queries don't support timeshift + if kind != qbtypes.RequestTypeTimeSeries && kind != qbtypes.RequestTypeScalar { + return tr + } + + // Use the ShiftBy field if it's already populated, otherwise extract it + shiftBy := spec.ShiftBy + if shiftBy == 0 { + shiftBy = extractShiftFromBuilderQuery(spec) + } + + if shiftBy == 0 { + return tr + } + + // ShiftBy is in seconds, convert to milliseconds and shift backward in time + shiftMS := shiftBy * 1000 + return qbtypes.TimeRange{ + From: tr.From - uint64(shiftMS), + To: tr.To - uint64(shiftMS), + } +} + +func (q *querier) constructTraceOperatorDependencyMap(queries []qbtypes.QueryEnvelope) (map[string]bool, error) { + dependencyQueries := make(map[string]bool) + + for _, query := range queries { + if query.Type == qbtypes.QueryTypeTraceOperator { + if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok { + // Parse expression to find dependencies + if err := spec.ParseExpression(); err != nil { + return nil, err + } + + deps := spec.CollectReferencedQueries(spec.ParsedExpression) + for _, dep := range deps { + dependencyQueries[dep] = true + } + } + } + } + + return dependencyQueries, nil +} + +// adjustStepInterval normalizes each query's step interval in place and returns +// any clamp warnings emitted along the way. +func (q *querier) adjustStepInterval(queries []qbtypes.QueryEnvelope, start, end uint64) []string { + // Compute the per-signal bounds once per call — they only depend on start/end. + traceLogRecommended := querybuilder.RecommendedStepInterval(start, end) + traceLogMin := querybuilder.MinAllowedStepInterval(start, end) + meterRecommended := querybuilder.RecommendedStepIntervalForMeter(start, end) + meterMin := querybuilder.MinAllowedStepIntervalForMeter(start, end) + metricRecommended := querybuilder.RecommendedStepIntervalForMetric(start, end) + metricMin := querybuilder.MinAllowedStepIntervalForMetric(start, end) + + warnings := make([]string, 0) + for idx := range queries { + qe := &queries[idx] + switch qe.Type { + case qbtypes.QueryTypeBuilder: + switch qe.Spec.(type) { + case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation], qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]: + clampStep(qe, traceLogRecommended, traceLogMin, &warnings) + case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]: + if qe.GetSource() == telemetrytypes.SourceMeter { + clampStep(qe, meterRecommended, meterMin, &warnings) + } else { + clampStep(qe, metricRecommended, metricMin, &warnings) + } + } + case qbtypes.QueryTypePromQL: + // PromQL only fills an unset step — no min clamp. + if qe.GetStepInterval().Seconds() == 0 { + qe.SetStepInterval(secondsStep(metricRecommended)) + } + case qbtypes.QueryTypeTraceOperator: + clampStep(qe, traceLogRecommended, traceLogMin, &warnings) + } + } + return warnings +} diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 432f1a05a7a..b8364a75c14 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -209,6 +209,7 @@ func NewSQLMigrationProviderFactories( sqlmigration.NewAddScopeToPlannedMaintenanceFactory(sqlstore, sqlschema), sqlmigration.NewMigrateInstalledIntegrationDashboardsFactory(sqlstore), sqlmigration.NewAddDashboardNameFactory(sqlstore, sqlschema), + sqlmigration.NewFixChangelogOperationTypeFactory(sqlstore, sqlschema), ) } diff --git a/pkg/sqlmigration/090_fix_changelog_operation_type.go b/pkg/sqlmigration/090_fix_changelog_operation_type.go new file mode 100644 index 00000000000..359a6007d73 --- /dev/null +++ b/pkg/sqlmigration/090_fix_changelog_operation_type.go @@ -0,0 +1,116 @@ +package sqlmigration + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlschema" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect" + "github.com/uptrace/bun/migrate" +) + +type fixChangelogOperationType struct { + sqlstore sqlstore.SQLStore + sqlschema sqlschema.SQLSchema +} + +func NewFixChangelogOperationTypeFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("fix_changelog_operation_type"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return &fixChangelogOperationType{ + sqlstore: sqlstore, + sqlschema: sqlschema, + }, nil + }) +} + +func (migration *fixChangelogOperationType) Register(migrations *migrate.Migrations) error { + return migrations.Register(migration.Up, migration.Down) +} + +func (migration *fixChangelogOperationType) Up(ctx context.Context, db *bun.DB) error { + // Fix OpenFGA table column types to match the expected schema. + // + // Migration 054 introduced two bugs for PostgreSQL: + // 1. changelog.operation is TEXT, should be INTEGER (OpenFGA v1.14.0 passes int32) + // 2. condition_name and condition_context types are swapped in both + // tuple and changelog tables (BYTEA <-> TEXT) + // + // Changelog: drop and recreate (it is only used by OpenFGA's ReadChanges + // API which SigNoz does not call; authorization data lives in tuple). + // Tuple: alter columns in place (condition columns are always NULL since + // SigNoz does not use FGA conditions). + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + sqls := [][]byte{} + + // 1. Drop and recreate changelog with correct types. + changelogTable, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("changelog")) + if err != nil { + return err + } + + dropTableSQLs := migration.sqlschema.Operator().DropTable(changelogTable) + sqls = append(sqls, dropTableSQLs...) + + if migration.sqlstore.BunDB().Dialect().Name() == dialect.PG { + createTableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "changelog", + Columns: []*sqlschema.Column{ + {Name: "store", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "object_type", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "object_id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "relation", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "_user", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "condition_name", DataType: sqlschema.DataTypeText, Nullable: true}, + {Name: "condition_context", DataType: sqlschema.DataTypeBytea, Nullable: true}, + {Name: "operation", DataType: sqlschema.DataTypeInteger, Nullable: false}, + {Name: "ulid", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "inserted_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"store", "ulid", "object_type"}}, + }) + sqls = append(sqls, createTableSQLs...) + } else { + createTableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "changelog", + Columns: []*sqlschema.Column{ + {Name: "store", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "object_type", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "object_id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "relation", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "user_object_type", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "user_object_id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "user_relation", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "condition_name", DataType: sqlschema.DataTypeText, Nullable: true}, + {Name: "condition_context", DataType: sqlschema.DataTypeBytea, Nullable: true}, + {Name: "operation", DataType: sqlschema.DataTypeInteger, Nullable: false}, + {Name: "ulid", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "inserted_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"store", "ulid", "object_type"}}, + }) + sqls = append(sqls, createTableSQLs...) + } + + for _, sql := range sqls { + if _, err := tx.ExecContext(ctx, string(sql)); err != nil { + return err + } + } + + return tx.Commit() +} + +func (migration *fixChangelogOperationType) Down(_ context.Context, _ *bun.DB) error { + return nil +} diff --git a/pkg/telemetrystore/clickhousetelemetrystore/provider.go b/pkg/telemetrystore/clickhousetelemetrystore/provider.go index db5844904aa..65152f01482 100644 --- a/pkg/telemetrystore/clickhousetelemetrystore/provider.go +++ b/pkg/telemetrystore/clickhousetelemetrystore/provider.go @@ -136,7 +136,8 @@ func (p *provider) AsyncInsert(ctx context.Context, query string, wait bool, arg event := telemetrystore.NewQueryEvent(query, args) ctx = telemetrystore.WrapBeforeQuery(p.hooks, ctx, event) - err := p.clickHouseConn.AsyncInsert(ctx, query, wait, args...) + // TODO: migrate to WithAsync() — https://github.com/SigNoz/engineering-pod/issues/5093 + err := p.clickHouseConn.AsyncInsert(ctx, query, wait, args...) //nolint:staticcheck event.Err = err telemetrystore.WrapAfterQuery(p.hooks, ctx, event)