Compare commits

..

8 Commits

Author SHA1 Message Date
Nikhil Soni
20afb694ca feat: add implementation for aggregation store 2026-05-27 00:23:35 +05:30
Vinicius Lourenço
f890cefdde chore(frontend): add agents/claude markdown file (#11463) 2026-05-27 00:23:35 +05:30
Karan Balani
251ca5b722 feat(meterreporter): add jitter to meter collection cycles (#11451)
* feat(meterreporter): jitter first run and per-tick to spread Zeus load

* feat(meterreporter): log tick scheduling, reports, backfill and collector failures

* fix(meterreporter): make jitter defaults track Interval via sentinel

* refactor(meterreporter): drop redundant >=0 guards in jitter validation

* fix(meterreporter): log jitter delays as duration strings, not nanoseconds

* feat(meterreporter): emit delay_ns alongside delay string for graphing

* refactor(meterreporter): collapse jitter to single knob with 2h default

* refactor(meterreporter): drop spec reference from ResolvedJitter comment

* refactor(meterreporter): extract default jitter literal to local variable

* refactor(meterreporter): merge jitter helper into Config.NewJitter

* chore: move info logs to debug

* chore: remove debug logs

---------

Co-authored-by: Karan Balani <29383381+balanikaran@users.noreply.github.com>
2026-05-27 00:23:35 +05:30
Naman Verma
d6167efb8c chore: add name column in dashboards table for v2 dashboards (#11456)
* chore: add name column in dashboards table for v2 dashboards

* chore: empty commit to rerun tests

* chore: empty commit to rerun tests
2026-05-27 00:23:35 +05:30
Vishal Sharma
207585edf8 feat(ai-assistant): polish composer UX and add Cmd+K entry (#11362)
* feat(ai-assistant): polish composer UX and add Cmd+K entry

- ChatInput textarea auto-grows up to 200px (was locked at 2 rows) so
  long prompts aren't trapped in a scrolling porthole; default rows
  bumped 2 → 3.
- Composer container shows an accent-primary focus ring via
  :focus-within so the active state is visible.
- Cmd+K palette surfaces an "Open AI Assistant" entry, gated on
  useIsAIAssistantEnabled() and emitting Opened with source: 'cmdk'.

* fix(ai-assistant): a11y, scroll, and routing polish

Address a punch list of UX/a11y bugs surfaced while auditing the AI
Assistant:

- Stop auto-scroll fighting the user during streaming. VirtualizedMessages
  now tracks atBottom via Virtuoso's atBottomStateChange and bails the
  manual scrollTo when the user has scrolled away.
- Resolve dynamic-route templates in getRouteKey so /ai-assistant/:id
  picks up the new AI_ASSISTANT title entry instead of falling through to
  the default browser title.
- Give the icon-only message actions (copy / thumbs up-down / regenerate)
  real aria-labels, and aria-pressed reflecting vote state.
- Convert context-picker rows from divs to buttons. Categories become a
  proper tablist with roving tabindex and ArrowUp/ArrowDown navigation;
  entities use aria-pressed for toggle semantics. SCSS resets native
  button defaults and adds focus-visible outlines.
- Enforce required clarification fields. Submit is disabled until every
  required field is filled, and handleSubmit bails early to guard the
  keyboard-Enter bypass.

* fix(ai-assistant): move focus to new tab on context picker arrow keys

The roving-tabindex pattern stalled because state updated but DOM focus
stayed on the original button — whose closure had the old category — so
subsequent arrow keys never advanced past the second tab. Added refs to
each tab and call `.focus()` on the newly-active one after state update.

* fix(ai-assistant): scroll regression on user send and focus polish

- VirtualizedMessages: when the user is scrolled up and sends a new
  message, force-anchor to the bottom so they see their own send and
  the assistant's follow-up. Previous bailout kept them stranded.
- Composer: move the :focus-within highlight from the outer .input
  wrapper onto .composer so the action footer (Add Context / mic /
  send) isn't visually inside the focus ring.
- Context picker: drop the :focus-visible outlines on category and
  entity buttons; the existing :hover + .active / .selected styles
  carry the focus story.

* fix(ai-assistant): complete tablist semantics and cross-pane keyboard nav

Finish the context picker tablist pattern and extend keyboard navigation
into the entity panel so arrow keys carry the user all the way to a
selection without reaching for the mouse.

- Tabs get `id` + `aria-controls`; the right pane becomes a real
  `role="tabpanel"` with `id` + `aria-labelledby` pointing back at the
  active tab.
- Add `Home` / `End` to the tablist; add `ArrowRight` to cross from the
  active tab into the entity list.
- Add `ArrowUp` / `ArrowDown` / `Home` / `End` cycling within the entity
  list, and `ArrowLeft` to cross back to the active tab.

* docs(app-layout): note that ROUTES order matters for ambiguous templates

* fix(ai-assistant): reset scroll anchor on conversation switch

Key `VirtualizedMessages` by `conversationId` so atBottomRef,
lastSeenUserMessageIdRef, and Virtuoso's internal scroll position
all reset together — otherwise a "scrolled up" state from the
previous conversation could block auto-scroll for the next one's
incoming stream.

* fix(app-layout): add titles for dynamic-template routes

After `getRouteKey` started matching `:param` templates (1bad9ec76),
six routes that previously fell through to DEFAULT now resolve to a
key with no translation, causing react-helmet to render the raw key
as <title>. Add explicit titles for TRACE_DETAIL_OLD,
SERVICE_TOP_LEVEL_OPERATIONS, ROLE_DETAILS, TRACES_FUNNELS_DETAIL,
INTEGRATIONS_DETAIL, and PUBLIC_DASHBOARD.

* fix(ai-assistant): polish chat input voice controls and context picker

- Convert .micDiscard / .micStop from <div onClick> to native <button>
  so the voice-recording controls are keyboard-operable (WCAG 2.1.1).
  Adds a small SCSS reset for native button defaults so the 24px circle
  isn't inflated by browser padding / font metrics.
- Add role="status" aria-live="polite" with an aria-label to the
  .micRecording container so screen-reader users hear when recording
  starts.
- Truncate entityRefs.current to filteredContextOptions.length next to
  the filter so switching from a large context category (e.g. 100
  dashboards) to a smaller one doesn't leave stale null slots from
  earlier renders. Keyboard nav math already used the new length for
  its modulo, so this is housekeeping rather than a correctness fix.
- Drop redundant `messages.length` from the auto-scroll effect deps —
  `messages` already covers it.

* fix(ai-assistant): polish clarification form a11y and numeric validation

- Add aria-required to text / number Inputs and to SelectTrigger so
  screen readers announce "required" without needing the visual `*`.
  Mark the visual `*` aria-hidden so it doesn't get double-announced
  alongside the aria-required state.
- Convert the multi_select wrapper from <div> + <span> to <fieldset> +
  <legend>, the WCAG 1.3.1-recommended grouping for related checkboxes
  so SRs announce the group label before each option. Adds an SCSS
  reset so the native fieldset border / padding / margin don't shift
  the layout vs. the surrounding <div>-based field rows.
- Map an empty <Input> value to `null` for number fields instead of
  Number('') === 0, so a required numeric field cleared after typing
  no longer silently reads as a valid `0` in `isFieldFilled`.

* fix(ai-assistant): address Codex review findings

- ClarificationForm: parse number defaults in `initialAnswerFor`. The
  generated DTO types `default` as `string | string[] | null`, so a
  server-supplied numeric default arrives as `"5"`. The previous code
  stored that string verbatim and `isFieldFilled` (which requires
  `typeof === 'number'`) then disabled Submit for a visibly-filled
  required field. Now parses to a real number, or `null` for empty /
  NaN inputs.
- ChatInput context picker: share a single stable tabpanel id across
  all tabs. Previously each tab's `aria-controls` pointed at
  `ai-context-tabpanel-${category}`, but only the active category's
  tabpanel was actually rendered — so two of three tabs always pointed
  at nonexistent ids. APG permits a single dynamic panel whose
  `aria-labelledby` swaps to the active tab.
- Extract analytics `source` magic strings into typed `as const`
  enums in `events.ts` per the CLAUDE.local.md guidance. New
  `AIAssistantOpenSource` ({ Icon, Shortcut, Cmdk }) replaces inline
  strings in `AIAssistantTrigger`, `AIAssistantModal`, and
  `cmdKPalette`. New `VoiceInputSource` ({ Button, Shortcut }) replaces
  inline strings in `ChatInput`.
- ClarificationForm comment: the form root isn't a `<form>` element,
  so an "Enter inside text field" submit-bypass can't actually happen.
  Reword the comment to reflect what the guard is really for —
  defensive against direct handler invocation.

* fix(ai-assistant): add tooltips to icon-only buttons

Several icon-only buttons in the AI Assistant relied on the native
`title` attribute (slow, inconsistent across browsers) or only an
`aria-label` (invisible to sighted users) for hover affordance.
Wrapped with `TooltipSimple` to match the established pattern in
`AIAssistantPanel`, `MessageFeedback`, `UserMessageActions`, etc.

- ApprovalCard: the diff toolbar view-option icons (split / unified /
  wrap), the expand-diff button, and the CopyButton.
- RichCodeBlock: the per-block copy-code button.
- ChatInput: voice-recording discard / stop-and-send buttons (newly
  native <button>s after the previous a11y pass), and the Send button.

* fix(ai-assistant): use component-prefixed class for multi_select fieldset

`.fieldset` is on the CLAUDE.local.md list of generic CSS-module class
names to avoid alongside `.header` / `.body` / `.label` / `.row`.
Rename to `.multiSelectFieldset` so the intent is obvious at
grep / diff time.

* refactor(ai-assistant): address PR review on composer + approval card

Migrate icon-only and icon-prefixed buttons to use the DS Button
`prefix` slot, replace the manual context-picker `<button>` rows with
DS Button (overriding centered layout via `--button-justify-content`
CSS var), extract context-picker keyboard handlers into named
useCallbacks, and move the `MessageFeedback` rating vocabulary into
typed `as const` maps keyed on `FeedbackRatingDTO`. The approval-diff
view-mode toggle stays on the manual `ToggleGroup` composition so each
`TooltipSimple` can wrap its `ToggleGroupItem` (the items-API loses
per-item tooltip anchoring). Disable Radix Dialog auto-focus on the
approval-diff modal so the first Copy button's tooltip doesn't surface
on open.

* feat(ai-assistant): surface modal in Cmd+K + fix modal header icon color

- Add an "Open AI Assistant" entry to the Cmd+K palette with a `cmd+j`
  shortcut hint. Selecting it now starts a fresh conversation and opens
  the modal (matching Cmd+J), rather than the side drawer.
- Modal header icons (history, new, expand, minimize, close) were
  defaulting to the primary color and rendering blue. Set
  `color="secondary"` to match the side-panel header.
- Migrate the modal + panel header icon buttons from icon-as-children
  to the DS Button `prefix` slot for consistency with the rest of the
  AI Assistant composer.

* fix(ai-assistant): refocus composer on new conversation / switch

Key ChatInput by conversationId in ConversationView (matching how
VirtualizedMessages is already keyed) so the "+ New conversation"
click remounts the composer and its mount-effect re-grabs textarea
focus. Side benefit: prior text/attachments/contexts no longer leak
across conversation switches.
2026-05-27 00:23:35 +05:30
Nikhil Soni
dd76af883a chore: update openapi specs 2026-05-26 16:15:43 +05:30
Nikhil Soni
412ec1813a chore: use v1 for aggregations api 2026-05-26 16:15:43 +05:30
Nikhil Soni
cb7b183171 feat: add endpoint & module for trace aggregations 2026-05-26 16:15:43 +05:30
10 changed files with 454 additions and 0 deletions

View File

@@ -5672,6 +5672,14 @@ components:
required:
- items
type: object
SpantypesGettableTraceAggregations:
properties:
aggregations:
items:
$ref: '#/components/schemas/SpantypesSpanAggregationResult'
nullable: true
type: array
type: object
SpantypesGettableWaterfallTrace:
properties:
aggregations:
@@ -5737,6 +5745,14 @@ components:
- name
- condition
type: object
SpantypesPostableTraceAggregations:
properties:
aggregations:
items:
$ref: '#/components/schemas/SpantypesSpanAggregation'
nullable: true
type: array
type: object
SpantypesPostableWaterfall:
properties:
aggregations:
@@ -11364,6 +11380,75 @@ paths:
summary: Test notification channel (deprecated)
tags:
- channels
/api/v1/traces/{traceID}/aggregations:
post:
deprecated: false
description: Computes span aggregations grouped by requested field.
operationId: GetTraceAggregations
parameters:
- in: path
name: traceID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SpantypesPostableTraceAggregations'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableTraceAggregations'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get aggregations for a trace
tags:
- tracedetail
/api/v1/user:
get:
deprecated: false

View File

@@ -6775,6 +6775,13 @@ export interface SpantypesSpanAggregationResultDTO {
value?: SpantypesSpanAggregationResultDTOValue;
}
export interface SpantypesGettableTraceAggregationsDTO {
/**
* @type array,null
*/
aggregations?: SpantypesSpanAggregationResultDTO[] | null;
}
export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
@@ -7018,6 +7025,13 @@ export interface SpantypesSpanAggregationDTO {
field?: TelemetrytypesTelemetryFieldKeyDTO;
}
export interface SpantypesPostableTraceAggregationsDTO {
/**
* @type array,null
*/
aggregations?: SpantypesSpanAggregationDTO[] | null;
}
export interface SpantypesPostableWaterfallDTO {
/**
* @type array,null
@@ -8351,6 +8365,17 @@ export type UpdateSpanMapperPathParameters = {
groupId: string;
mapperId: string;
};
export type GetTraceAggregationsPathParameters = {
traceID: string;
};
export type GetTraceAggregations200 = {
data: SpantypesGettableTraceAggregationsDTO;
/**
* @type string
*/
status: string;
};
export type ListUsersDeprecated200 = {
/**
* @type array

View File

@@ -12,17 +12,120 @@ import type {
} from 'react-query';
import type {
GetTraceAggregations200,
GetTraceAggregationsPathParameters,
GetWaterfall200,
GetWaterfallPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
SpantypesPostableTraceAggregationsDTO,
SpantypesPostableWaterfallDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Computes span aggregations grouped by requested field.
* @summary Get aggregations for a trace
*/
export const getTraceAggregations = (
{ traceID }: GetTraceAggregationsPathParameters,
spantypesPostableTraceAggregationsDTO?: BodyType<SpantypesPostableTraceAggregationsDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetTraceAggregations200>({
url: `/api/v1/traces/${traceID}/aggregations`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableTraceAggregationsDTO,
signal,
});
};
export const getGetTraceAggregationsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
> => {
const mutationKey = ['getTraceAggregations'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof getTraceAggregations>>,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getTraceAggregations(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetTraceAggregationsMutationResult = NonNullable<
Awaited<ReturnType<typeof getTraceAggregations>>
>;
export type GetTraceAggregationsMutationBody =
| BodyType<SpantypesPostableTraceAggregationsDTO>
| undefined;
export type GetTraceAggregationsMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get aggregations for a trace
*/
export const useGetTraceAggregations = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
> => {
return useMutation(getGetTraceAggregationsMutationOptions(options));
};
/**
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
* @summary Get waterfall view for a trace

View File

@@ -48,5 +48,24 @@ func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/traces/{traceID}/aggregations", handler.New(
provider.authzMiddleware.ViewAccess(provider.traceDetailHandler.GetTraceAggregations),
handler.OpenAPIDef{
ID: "GetTraceAggregations",
Tags: []string{"tracedetail"},
Summary: "Get aggregations for a trace",
Description: "Computes span aggregations grouped by requested field.",
Request: new(spantypes.PostableTraceAggregations),
RequestContentType: "application/json",
Response: new(spantypes.GettableTraceAggregations),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -59,3 +59,24 @@ func (h *handler) GetWaterfallV4(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, result)
}
func (h *handler) GetTraceAggregations(rw http.ResponseWriter, r *http.Request) {
req := new(spantypes.PostableTraceAggregations)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
if err := req.Validate(); err != nil {
render.Error(rw, err)
return
}
result, err := h.module.GetTraceAggregations(r.Context(), mux.Vars(r)["traceID"], req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, result)
}

View File

@@ -105,6 +105,49 @@ func (m *module) getFullWaterfall(ctx context.Context, traceID string, summary *
return spantypes.NewGettableWaterfallTrace(waterfallTrace, selectedSpans, nil, true, nil), nil
}
func (m *module) GetTraceAggregations(ctx context.Context, traceID string, req *spantypes.PostableTraceAggregations) (*spantypes.GettableTraceAggregations, error) {
summary, err := m.store.GetTraceSummary(ctx, traceID)
if err != nil {
return nil, err
}
traceDurationNs := uint64(summary.End.UnixNano()) - uint64(summary.Start.UnixNano())
results := make([]spantypes.SpanAggregationResult, 0, len(req.Aggregations))
for _, agg := range req.Aggregations {
result := spantypes.SpanAggregationResult{Field: agg.Field, Aggregation: agg.Aggregation}
switch agg.Aggregation {
case spantypes.SpanAggregationSpanCount:
result.Value, err = m.store.GetSpanCountByField(ctx, traceID, summary, agg.Field)
if err != nil {
return nil, err
}
case spantypes.SpanAggregationDuration:
durationNs, err2 := m.store.GetSpanDurationByField(ctx, traceID, summary, agg.Field)
if err2 != nil {
return nil, err2
}
result.Value = make(map[string]uint64, len(durationNs))
for k, ns := range durationNs {
result.Value[k] = ns / 1_000_000
}
case spantypes.SpanAggregationExecutionTimePercentage:
durationNs, err2 := m.store.GetSpanDurationByField(ctx, traceID, summary, agg.Field)
if err2 != nil {
return nil, err2
}
result.Value = make(map[string]uint64, len(durationNs))
if traceDurationNs > 0 {
for k, ns := range durationNs {
result.Value[k] = ns * 100 / traceDurationNs
}
}
}
results = append(results, result)
}
return &spantypes.GettableTraceAggregations{Aggregations: results}, nil
}
// getWindowedWaterfall builds the waterfall tree with minimal data and then returns only a window of full spans.
func (m *module) getWindowedWaterfall(ctx context.Context, traceID, selectedSpanID string, uncollapsedSpans []string, start, end time.Time) (*spantypes.GettableWaterfallTrace, error) {
// Step 1: minimal fetch → build full tree → select visible window

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"regexp"
"time"
sqlbuilder "github.com/huandu/go-sqlbuilder"
@@ -11,10 +12,33 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const colServiceName = `resource_string_service$$$$name` // $ gets escaped so $$$$ converts to $$.
// validFieldName only allows characters safe to embed as ClickHouse map subscript literals.
var validFieldName = regexp.MustCompile(`^[a-zA-Z0-9._\-]+$`)
// buildFieldExpr returns a ClickHouse SQL expression for the value of fieldKey per span.
func buildFieldExpr(fieldKey telemetrytypes.TelemetryFieldKey) (string, error) {
if !validFieldName.MatchString(fieldKey.Name) {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid field name: %q", fieldKey.Name)
}
n := fieldKey.Name
switch fieldKey.FieldContext {
case telemetrytypes.FieldContextResource:
return fmt.Sprintf("resources_string['%s']", n), nil
case telemetrytypes.FieldContextAttribute:
return fmt.Sprintf(
"multiIf(mapContains(attributes_string,'%[1]s'),attributes_string['%[1]s'],"+
"mapContains(attributes_number,'%[1]s'),toString(attributes_number['%[1]s']),"+
"mapContains(attributes_bool,'%[1]s'),toString(attributes_bool['%[1]s']),'')",
n), nil
}
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported field context: %v", fieldKey.FieldContext)
}
type traceStore struct {
telemetryStore telemetrystore.TelemetryStore
}
@@ -97,6 +121,14 @@ func (s *traceStore) GetMinimalSpans(ctx context.Context, traceID string, start,
return spans, nil
}
func (s *traceStore) GetSpanCountByField(ctx context.Context, traceID string, summary *spantypes.TraceSummary, fieldKey telemetrytypes.TelemetryFieldKey) (map[string]uint64, error) {
return nil, nil
}
func (s *traceStore) GetSpanDurationByField(ctx context.Context, traceID string, summary *spantypes.TraceSummary, fieldKey telemetrytypes.TelemetryFieldKey) (map[string]uint64, error) {
return nil, nil
}
func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, start, end time.Time, spanIDs []string) ([]spantypes.StorableSpan, error) {
if len(spanIDs) == 0 {
return []spantypes.StorableSpan{}, nil
@@ -133,3 +165,96 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta
}
return spans, nil
}
type spanCountRow struct {
FieldValue string `ch:"field_value"`
Count uint64 `ch:"count"`
}
func (s *traceStore) GetSpanCountByField(ctx context.Context, traceID string, summary *spantypes.TraceSummary, fieldKey telemetrytypes.TelemetryFieldKey) (map[string]uint64, error) {
fieldExpr, err := buildFieldExpr(fieldKey)
if err != nil {
return nil, err
}
query := fmt.Sprintf(`
SELECT %[1]s AS field_value, count() AS count
FROM %[2]s.%[3]s
WHERE trace_id=? AND ts_bucket_start>=? AND ts_bucket_start<=?
AND %[1]s != ''
GROUP BY field_value`,
fieldExpr, spantypes.TraceDB, spantypes.TraceTable,
)
var rows []spanCountRow
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &rows, query,
traceID, summary.Start.Unix()-1800, summary.End.Unix(),
); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying span count by field")
}
result := make(map[string]uint64, len(rows))
for _, r := range rows {
result[r.FieldValue] = r.Count
}
return result, nil
}
type spanDurationRow struct {
FieldValue string `ch:"field_value"`
TotalNs uint64 `ch:"total_ns"`
}
func (s *traceStore) GetSpanDurationByField(ctx context.Context, traceID string, summary *spantypes.TraceSummary, fieldKey telemetrytypes.TelemetryFieldKey) (map[string]uint64, error) {
fieldExpr, err := buildFieldExpr(fieldKey)
if err != nil {
return nil, err
}
// 4-level query: deduplicate → window function → non-overlapping per span → sum per field.
// The window function computes the running max end time of preceding spans in the same
// field partition (ordered by start), so each span only contributes its non-overlapping
// tail — matching the Go-side mergeSpanIntervals semantics in V3.
query := fmt.Sprintf(`
SELECT field_value, sum(non_overlapping_ns) AS total_ns
FROM (
SELECT
field_value,
multiIf(
start_ns >= prev_max_end_ns, duration_nano,
start_ns + duration_nano > prev_max_end_ns, start_ns + duration_nano - prev_max_end_ns,
0
) AS non_overlapping_ns
FROM (
SELECT
field_value,
start_ns,
duration_nano,
ifNull(max(start_ns + duration_nano) OVER (
PARTITION BY field_value
ORDER BY start_ns
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
), 0) AS prev_max_end_ns
FROM (
SELECT DISTINCT ON (span_id)
%[1]s AS field_value,
toUnixTimestamp64Nano(timestamp) AS start_ns,
duration_nano
FROM %[2]s.%[3]s
WHERE trace_id=? AND ts_bucket_start>=? AND ts_bucket_start<=?
ORDER BY toUnixTimestamp64Nano(timestamp) ASC, name ASC
)
WHERE field_value != ''
)
)
GROUP BY field_value`,
fieldExpr, spantypes.TraceDB, spantypes.TraceTable,
)
var rows []spanDurationRow
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &rows, query,
traceID, summary.Start.Unix()-1800, summary.End.Unix(),
); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying span duration by field")
}
result := make(map[string]uint64, len(rows))
for _, r := range rows {
result[r.FieldValue] = r.TotalNs
}
return result, nil
}

View File

@@ -11,10 +11,12 @@ import (
type Handler interface {
GetWaterfall(http.ResponseWriter, *http.Request)
GetWaterfallV4(http.ResponseWriter, *http.Request)
GetTraceAggregations(http.ResponseWriter, *http.Request)
}
// Module defines the business logic for trace detail operations.
type Module interface {
GetWaterfall(ctx context.Context, traceID string, req *spantypes.PostableWaterfall) (*spantypes.GettableWaterfallTrace, error)
GetWaterfallV4(ctx context.Context, traceID string, selectedSpanID string, uncollapsedSpans []string, selectAllLimit uint) (*spantypes.GettableWaterfallTrace, error)
GetTraceAggregations(ctx context.Context, traceID string, req *spantypes.PostableTraceAggregations) (*spantypes.GettableTraceAggregations, error)
}

View File

@@ -48,3 +48,30 @@ func (SpanAggregationType) Enum() []any {
func (s SpanAggregationType) isValid() bool {
return slices.ContainsFunc(s.Enum(), func(v any) bool { return v == s })
}
// PostableTraceAggregations is the request body for the V4 aggregations endpoint.
type PostableTraceAggregations struct {
Aggregations []SpanAggregation `json:"aggregations"`
}
func (p *PostableTraceAggregations) Validate() error {
if len(p.Aggregations) > maxAggregationItems {
return ErrTooManyAggregationItems
}
for _, a := range p.Aggregations {
if !a.Aggregation.isValid() {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown aggregation type: %q", a.Aggregation)
}
fc := a.Field.FieldContext
if fc != telemetrytypes.FieldContextResource && fc != telemetrytypes.FieldContextAttribute {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "aggregation field context must be %q or %q, got %q",
telemetrytypes.FieldContextResource, telemetrytypes.FieldContextAttribute, fc)
}
}
return nil
}
// GettableTraceAggregations is the response for the V4 aggregations endpoint.
type GettableTraceAggregations struct {
Aggregations []SpanAggregationResult `json:"aggregations"`
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -29,4 +30,7 @@ type TraceStore interface {
GetTraceSpans(ctx context.Context, traceID string, summary *TraceSummary) ([]StorableSpan, error)
GetMinimalSpans(ctx context.Context, traceID string, start, end time.Time) ([]MinimalSpan, error)
GetTraceSpansByIDs(ctx context.Context, traceID string, start, end time.Time, spanIDs []string) ([]StorableSpan, error)
GetSpanCountByField(ctx context.Context, traceID string, summary *TraceSummary, fieldKey telemetrytypes.TelemetryFieldKey) (map[string]uint64, error)
GetSpanDurationByField(ctx context.Context, traceID string, summary *TraceSummary, fieldKey telemetrytypes.TelemetryFieldKey) (map[string]uint64, error)
}