Compare commits

...

5 Commits

Author SHA1 Message Date
nityanandagohain
7ed7d193e7 fix: opamp collector reconnect send updated config 2026-09-23 20:41:25 +05:30
Gaurav Tewari
cad93a8063 chore: remove beta tag class (#12965)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

Removed beta tag class from AI o11y . 
since it remove margin and breaks alignment. don't see any use of it .
it also shifts the pin icon a bit

before - 
<img width="149" height="160" alt="image"
src="https://github.com/user-attachments/assets/26770802-b5b0-452b-b66a-fecda0d49c38"
/>
 
 
<img width="331" height="152" alt="image"
src="https://github.com/user-attachments/assets/fba73ebe-2655-4de5-8488-eb6bb0bfebca"
/>


 now - 
 
<img width="158" height="192" alt="image"
src="https://github.com/user-attachments/assets/fb0ca483-e8db-4ff0-bb36-e0186c2e9458"
/>

<img width="272" height="63" alt="image"
src="https://github.com/user-attachments/assets/68486c3f-a704-4077-82e5-089452abf773"
/>

 



it was added for long text .
https://github.com/SigNoz/signoz/pull/5801/changes#r1736497688
for now in side nav we don't have any long text. and maybe we could have
handled this better
 

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information

<!--Please delete paragraphs that you did not use before submitting.-->

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-23 11:45:33 +00:00
Abhi kumar
aed096bf27 refactor(query-builder): compose the panel-type field map instead of listing it (#12781)
#### Description

`panelTypeDataSourceFormValuesMap` — the map deciding which builder
fields survive a panel-type switch — spelled out all 21 panel-type ×
data-source combinations as literal field lists, 435 lines of them.

They reduce to seven distinct sets:

- logs and traces carry **identical** fields for every panel type
- metrics adds its two aggregation steps (`timeAggregation`,
`spaceAggregation`)
- every panel type is one of four query shapes: series, scalar table,
single value, raw rows

Much of the apparent variation was ordering noise — a bar chart and a
table on logs have the *same* field set, listed in a different order.

Composed from those rules it's 84 lines, and the policy is legible at a
glance: charts, table and pie share a surface; table and pie differ only
by `reduceTo` on metrics; a single value has nothing to group, limit or
order; raw rows carry no aggregation. Two asymmetries that were buried
in the literals are now called out where they're decided, rather than
silently reproduced.

**No behaviour change.** Adding a panel type becomes one line — "which
shape is it?"

#### Additional Information

- Equivalence was checked cell by cell against the previous literal
table before it was removed; all 21 cells matched as sets. The old table
is in git history at `main:frontend/src/lib/query/panelQuery.ts` if you
want to re-run that comparison.
- The specs pin the **rules**, not the values, so they fail when a rule
changes — the moment to stop and decide — rather than whenever a field
moves. Two are worth reading:
- *"gives bar / histogram / table / pie the same non-metrics fields as a
time series"* states the hazard composing introduces: the aggregating
types share one field list, so an edit meant for charts reaches table
and pie too. A failure there names the reason.
- *"gives every cell its own array instance"* — the `QueryBuilder`
provider does `propsRequired?.push('dataSource')` on the list it reads
from this map, so cells sharing an instance would leak fields into each
other. My first draft shared one array across 10 cells; this test is
what guards it.
- Order is not asserted anywhere: `handleQueryChange` and the provider
both assign each field independently via `set()`, so sequence carries no
meaning.
- Three consumers, all exercised: `handleQueryChange` (dashboards v2's
kind switcher and V1's `PanelTypeSelector`) and the shared
`QueryBuilder` provider every explorer uses. Verified with `tsgo`,
`oxlint`, and the `lib` / `providers` / `WidgetCard` / Logs+Traces
explorer / `DashboardPage` suites: 243 suites, 2056 tests.
- Pre-existing and deliberately left alone: that `push` mutates module
state, so the arrays grow by one `'dataSource'` entry on every
query-builder change. Harmless today only because the assignment is
idempotent; `[...propsRequired, 'dataSource']` would fix it, but that's
the provider's bug, not this map's.
2026-09-23 11:24:51 +00:00
Nityananda Gohain
6b66ab64c8 fix: add ai-o11y quick filter migration (#12964)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
Added migration to update old instances where quick filters for ai-o11y
is not present.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
part of https://github.com/SigNoz/engineering-pod/issues/6107
2026-09-23 11:15:42 +00:00
Naman Verma
362d3a4fdf fix: backfill notification channel tuples (#12960)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
cacheci / tests (push) Has been cancelled
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

Same migration logic as number 128. Needed for enterprise servers as
these tuples decide whether a role may create, list, read, update or
delete channels at all. An existing org with zero channels still needs
them, otherwise its admin can't create the first one or even list the
empty page.
2026-09-23 09:48:11 +00:00
16 changed files with 571 additions and 468 deletions

View File

@@ -114,8 +114,4 @@
}
}
}
.beta-tag {
padding-right: 0;
}
}

View File

@@ -51,7 +51,7 @@ export default function NavItem({
data-testid={dataTestId}
>
{showIcon && <div className="nav-item-active-marker" />}
<div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}>
<div className="nav-item-data">
{showIcon && (
<div className={cx('nav-item-icon', isEarlyAccess ? 'noz-wave' : '')}>
{icon}

View File

@@ -0,0 +1,135 @@
import {
type BuilderField,
panelTypeDataSourceFormValuesMap,
type PartialPanelTypes,
} from 'lib/query/panelTypeDataSourceFormValuesMap';
import { isStaticPanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DataSource } from 'types/common/queryBuilder';
/**
* The map is composed from a few shape rules rather than spelled out per panel type
* and data source. These specs pin the rules themselves — each one fails only when a
* rule changes, which is the moment to stop and decide, rather than whenever any
* field moves.
*
* The composition it replaced was checked cell by cell against the previous literal
* table, which is in git history at `main:frontend/src/lib/query/panelQuery.ts`.
*/
function fieldsFor(
panelType: keyof PartialPanelTypes,
dataSource: DataSource,
): BuilderField[] {
return panelTypeDataSourceFormValuesMap[panelType][dataSource].builder
.queryData;
}
/** Fields present in `to` but not in `from`. */
function added(from: BuilderField[], to: BuilderField[]): BuilderField[] {
return to.filter((field) => !from.includes(field)).sort();
}
/** Panel types built on the aggregating field list. */
const AGGREGATING_TYPES: (keyof PartialPanelTypes)[] = [
PANEL_TYPES.AREA,
PANEL_TYPES.BAR,
PANEL_TYPES.HISTOGRAM,
PANEL_TYPES.TABLE,
PANEL_TYPES.PIE,
];
/** Panel types that reduce each series to one cell or slice. */
const SCALAR_TYPES: (keyof PartialPanelTypes)[] = [
PANEL_TYPES.TABLE,
PANEL_TYPES.PIE,
];
describe('panelTypeDataSourceFormValuesMap', () => {
const seriesLogs = fieldsFor(PANEL_TYPES.TIME_SERIES, DataSource.LOGS);
const seriesMetrics = fieldsFor(PANEL_TYPES.TIME_SERIES, DataSource.METRICS);
// A kind with no entry throws on switch: handleQueryChange reads
// map[panelType][dataSource] straight through, and the cast at its call site
// hides the gap. Static kinds return before that call, so they are exempt.
it('covers every panel kind that can be switched to', () => {
const uncovered = Object.entries(PANEL_KIND_TO_PANEL_TYPE)
.filter(([kind]) => !isStaticPanelKind(kind as never))
.map(([, panelType]) => panelType)
.filter((panelType) => !(panelType in panelTypeDataSourceFormValuesMap));
expect(uncovered).toStrictEqual([]);
});
it('shares one builder surface between logs and traces', () => {
Object.values(panelTypeDataSourceFormValuesMap).forEach((sources) => {
expect(sources[DataSource.LOGS].builder.queryData).toStrictEqual(
sources[DataSource.TRACES].builder.queryData,
);
});
});
// The provider pushes onto the list it reads from this map, so two cells backed by
// one instance would leak fields into each other.
it('gives every cell its own array instance', () => {
const arrays = Object.values(panelTypeDataSourceFormValuesMap).flatMap(
(sources) =>
Object.values(sources).map((source) => source.builder.queryData),
);
expect(new Set(arrays).size).toBe(arrays.length);
});
// One consequence of composing: the aggregating types share a single field list, so
// an edit meant for charts reaches table and pie too.
it.each(AGGREGATING_TYPES)(
'gives %s the same non-metrics fields as a time series',
(panelType) => {
expect(fieldsFor(panelType, DataSource.LOGS)).toStrictEqual(seriesLogs);
},
);
it('adds both metrics aggregation steps for metrics', () => {
expect(added(seriesLogs, seriesMetrics)).toStrictEqual([
'spaceAggregation',
'timeAggregation',
]);
});
it.each(SCALAR_TYPES)('offers reduceTo to %s on metrics only', (panelType) => {
expect(
added(seriesMetrics, fieldsFor(panelType, DataSource.METRICS)),
).toStrictEqual(['reduceTo']);
expect(fieldsFor(panelType, DataSource.LOGS)).not.toContain('reduceTo');
});
it('drops grouping, paging and ordering for a single value', () => {
const value = fieldsFor(PANEL_TYPES.VALUE, DataSource.LOGS);
expect(added(value, seriesLogs)).toStrictEqual([
'groupBy',
'limit',
'orderBy',
]);
expect(value).toContain('reduceTo');
});
it('offers no aggregation fields to raw rows', () => {
const rows = fieldsFor(PANEL_TYPES.LIST, DataSource.LOGS);
expect(rows).not.toContain('aggregateAttribute');
expect(rows).not.toContain('aggregateOperator');
expect(rows).not.toContain('groupBy');
expect(rows).not.toContain('having');
expect(rows).not.toContain('stepInterval');
});
it('drops paging and ordering for metrics rows', () => {
expect(
added(
fieldsFor(PANEL_TYPES.LIST, DataSource.METRICS),
fieldsFor(PANEL_TYPES.LIST, DataSource.LOGS),
),
).toStrictEqual(['functions', 'limit', 'orderBy']);
});
});

View File

@@ -1,7 +1,7 @@
/**
* Panel query shaping shared by the dashboard panel editor and the query
* builder: the per-panel-type field allowlist, the panel-type switch, and the
* dirty-check used to decide whether a panel has unsaved query edits.
* builder: the panel-type switch and the dirty-check used to decide whether a
* panel has unsaved query edits.
*/
import {
initialQueryBuilderFormValuesMap,
@@ -9,7 +9,11 @@ import {
} from 'constants/queryBuilder';
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
panelTypeDataSourceFormValuesMap,
PartialPanelTypes,
} from 'lib/query/panelTypeDataSourceFormValuesMap';
// Asks "would saving the current panel change the persisted widget spec?".
//
@@ -91,453 +95,6 @@ export const getIsQueryModified = (
);
};
export type PartialPanelTypes = {
[PANEL_TYPES.BAR]: 'bar';
[PANEL_TYPES.LIST]: 'list';
[PANEL_TYPES.TABLE]: 'table';
[PANEL_TYPES.TIME_SERIES]: 'graph';
[PANEL_TYPES.VALUE]: 'value';
[PANEL_TYPES.PIE]: 'pie';
[PANEL_TYPES.HISTOGRAM]: 'histogram';
};
export const panelTypeDataSourceFormValuesMap: Record<
keyof PartialPanelTypes,
Record<DataSource, any>
> = {
[PANEL_TYPES.BAR]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
'aggregations',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
},
[PANEL_TYPES.TIME_SERIES]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
'aggregations',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
},
[PANEL_TYPES.HISTOGRAM]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'limit',
'having',
'orderBy',
'stepInterval',
'legend',
'queryName',
'disabled',
'functions',
'expression',
'aggregations',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
},
[PANEL_TYPES.TABLE]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'functions',
'aggregations',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
},
[PANEL_TYPES.PIE]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'orderBy',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'functions',
'aggregations',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'stepInterval',
'disabled',
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
},
[PANEL_TYPES.LIST]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'queryName',
'filters',
'filter',
'limit',
'orderBy',
'functions',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: ['queryName', 'filters', 'filter', 'aggregations'],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'queryName',
'filters',
'filter',
'limit',
'orderBy',
'functions',
'aggregations',
],
},
},
},
[PANEL_TYPES.VALUE]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'reduceTo',
'having',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
'legend',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'having',
'reduceTo',
'stepInterval',
'legend',
'queryName',
'expression',
'disabled',
'functions',
'aggregations',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'reduceTo',
'having',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
'legend',
'aggregations',
],
},
},
},
};
export function handleQueryChange(
newPanelType: keyof PartialPanelTypes,
supersetQuery: Query,
@@ -555,7 +112,7 @@ export function handleQueryChange(
panelTypeDataSourceFormValuesMap[newPanelType][dataSource].builder
.queryData;
fieldsToSelect.forEach((field: keyof IBuilderQuery) => {
fieldsToSelect.forEach((field) => {
set(tempQuery, field, supersetQuery.builder.queryData[index][field]);
});

View File

@@ -0,0 +1,140 @@
/**
* Builder fields carried across a panel-type switch, per panel type and data source.
* Each shape is cut from the widest one by omission.
*/
import { PANEL_TYPES } from 'constants/queryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
export type PartialPanelTypes = {
[PANEL_TYPES.BAR]: 'bar';
[PANEL_TYPES.LIST]: 'list';
[PANEL_TYPES.TABLE]: 'table';
[PANEL_TYPES.TIME_SERIES]: 'graph';
[PANEL_TYPES.AREA]: 'area';
[PANEL_TYPES.VALUE]: 'value';
[PANEL_TYPES.PIE]: 'pie';
[PANEL_TYPES.HISTOGRAM]: 'histogram';
};
export type BuilderField = keyof IBuilderQuery;
export type PanelTypeFormValues = {
builder: { queryData: BuilderField[] };
};
/** A field added to `IBuilderQuery` fails to compile here until answered either way. */
const IS_CARRIED = {
queryName: true,
aggregateOperator: true,
aggregateAttribute: true,
aggregations: true,
timeAggregation: true,
spaceAggregation: true,
functions: true,
filter: true,
filters: true,
groupBy: true,
expression: true,
disabled: true,
having: true,
limit: true,
stepInterval: true,
orderBy: true,
reduceTo: true,
legend: true,
// `dataSource` is appended by the provider; the rest drive surfaces this switch
// does not reach.
dataSource: false,
temporality: false,
pageSize: false,
offset: false,
selectColumns: false,
source: false,
builderQueryType: false,
} satisfies Record<BuilderField, boolean>;
function omit(
fields: readonly BuilderField[],
...omitted: BuilderField[]
): BuilderField[] {
return fields.filter((field) => !omitted.includes(field));
}
const METRICS_AGGREGATION: readonly BuilderField[] = [
'timeAggregation',
'spaceAggregation',
];
const SCALAR_METRICS: readonly BuilderField[] = (
Object.entries(IS_CARRIED) as [BuilderField, boolean][]
)
.filter(([, carried]) => carried)
.map(([field]) => field);
// `reduceTo` is offered for metrics only, an asymmetry carried over from the old table.
const SERIES_METRICS: readonly BuilderField[] = omit(
SCALAR_METRICS,
'reduceTo',
);
const SERIES: readonly BuilderField[] = omit(
SERIES_METRICS,
...METRICS_AGGREGATION,
);
const SINGLE_VALUE_METRICS: readonly BuilderField[] = omit(
SCALAR_METRICS,
'groupBy',
'limit',
'orderBy',
);
const SINGLE_VALUE: readonly BuilderField[] = omit(
SINGLE_VALUE_METRICS,
...METRICS_AGGREGATION,
);
const RAW_ROWS: readonly BuilderField[] = omit(
SERIES,
'aggregateAttribute',
'aggregateOperator',
'groupBy',
'having',
'stepInterval',
'disabled',
'legend',
'expression',
);
const RAW_ROWS_METRICS: readonly BuilderField[] = omit(
RAW_ROWS,
'limit',
'orderBy',
'functions',
);
/** Each cell gets its own copy; a shared instance would let cells contaminate
* each other. */
function bySource(
logsAndTraces: readonly BuilderField[],
metrics: readonly BuilderField[],
): Record<DataSource, PanelTypeFormValues> {
return {
[DataSource.LOGS]: { builder: { queryData: [...logsAndTraces] } },
[DataSource.TRACES]: { builder: { queryData: [...logsAndTraces] } },
[DataSource.METRICS]: { builder: { queryData: [...metrics] } },
};
}
export const panelTypeDataSourceFormValuesMap: Record<
keyof PartialPanelTypes,
Record<DataSource, PanelTypeFormValues>
> = {
[PANEL_TYPES.TIME_SERIES]: bySource(SERIES, SERIES_METRICS),
[PANEL_TYPES.AREA]: bySource(SERIES, SERIES_METRICS),
[PANEL_TYPES.BAR]: bySource(SERIES, SERIES_METRICS),
[PANEL_TYPES.HISTOGRAM]: bySource(SERIES, SERIES_METRICS),
[PANEL_TYPES.TABLE]: bySource(SERIES, SCALAR_METRICS),
[PANEL_TYPES.PIE]: bySource(SERIES, SCALAR_METRICS),
[PANEL_TYPES.VALUE]: bySource(SINGLE_VALUE, SINGLE_VALUE_METRICS),
[PANEL_TYPES.LIST]: bySource(RAW_ROWS, RAW_ROWS_METRICS),
};

View File

@@ -7,10 +7,8 @@ import type {
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { PANEL_TYPES } from 'constants/queryBuilder';
import {
handleQueryChange,
type PartialPanelTypes,
} from 'lib/query/panelQuery';
import { handleQueryChange } from 'lib/query/panelQuery';
import type { PartialPanelTypes } from 'lib/query/panelTypeDataSourceFormValuesMap';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import type {

View File

@@ -32,7 +32,7 @@ import ROUTES from 'constants/routes';
import {
panelTypeDataSourceFormValuesMap,
PartialPanelTypes,
} from 'lib/query/panelQuery';
} from 'lib/query/panelTypeDataSourceFormValuesMap';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
@@ -767,12 +767,11 @@ export function QueryBuilderProvider({
queryItem.dataSource
].builder.queryData;
// `dataSource` travels with the panel type's fields, but is appended to a
// copy: `propsRequired` is the list held in
// `panelTypeDataSourceFormValuesMap`, and pushing onto it grew that
// module-level array by one entry on every call.
// `dataSource` travels with the panel type's fields, but on a copy:
// `propsRequired` is the list the map holds, and pushing onto it grew
// that array by one entry on every call.
if (propsRequired) {
[...propsRequired, 'dataSource'].forEach((p: any) => {
[...propsRequired, 'dataSource'].forEach((p) => {
set(queryItem, p, get(newQueryItem, p));
});
}

View File

@@ -105,6 +105,17 @@ func (m *Manager) RecommendAgentConfig(orgId valuer.UUID, currentConfYaml []byte
configId string,
err error,
) {
return m.recommendAgentConfig(orgId, currentConfYaml, true)
}
// Implements opamp.AgentConfigProvider
func (m *Manager) PreviewAgentConfig(orgId valuer.UUID, currentConfYaml []byte) ([]byte, error) {
recommendation, _, err := m.recommendAgentConfig(orgId, currentConfYaml, false)
return recommendation, err
}
func (m *Manager) recommendAgentConfig(orgId valuer.UUID, currentConfYaml []byte, recordDeployment bool) ([]byte, string, error) {
var configId string
recommendation := currentConfYaml
settingVersionsUsed := []string{}
@@ -134,6 +145,9 @@ func (m *Manager) RecommendAgentConfig(orgId valuer.UUID, currentConfYaml []byte
settingVersionsUsed = append(settingVersionsUsed, configId)
if !recordDeployment {
continue
}
_ = m.updateDeployStatus(
context.Background(),
orgId,

View File

@@ -239,7 +239,7 @@ func (ic *LogParsingPipelineController) getNormalizePipeline() pipelinetypes.Get
},
Config: []pipelinetypes.PipelineOperator{
{
ID: uuid.NewString(),
ID: "normalize_body_default",
Type: "normalize",
Enabled: true,
If: "body != nil",

View File

@@ -128,6 +128,12 @@ func (ta *MockAgentConfigProvider) HasReportedDeploymentStatus(orgID valuer.UUID
return exists
}
// AgentConfigProvider interface
func (ta *MockAgentConfigProvider) PreviewAgentConfig(orgId valuer.UUID, baseConfYaml []byte) ([]byte, error) {
recommendedYaml, _, err := ta.RecommendAgentConfig(orgId, baseConfYaml)
return recommendedYaml, err
}
// AgentConfigProvider interface
func (ta *MockAgentConfigProvider) GetDeployStatusByHash(_ context.Context, _ valuer.UUID, _ string) (opamptypes.DeployStatus, error) {
return opamptypes.DeployStatusUnknown, nil

View File

@@ -24,6 +24,8 @@ type Agent struct {
remoteConfig *protobufs.AgentRemoteConfig
Status *protobufs.AgentToServer
reconnectConfigChecked bool
// can this agent be load balancer
CanLB bool
@@ -291,6 +293,11 @@ func (agent *Agent) processStatusUpdate(
// We need to recalculate the config.
configChanged = agent.updateRemoteConfig(configProvider)
} else if agent.remoteConfig == nil && !agent.reconnectConfigChecked && agent.Config != "" {
// A running agent reconnected after a server restart; settings may have
// changed while it was away (e.g. startup reconciliation).
agent.reconnectConfigChecked = true
configChanged = agent.updateRemoteConfigIfStale(configProvider)
}
// If remote config is changed and different from what the Agent has then
@@ -312,6 +319,20 @@ func (agent *Agent) processStatusUpdate(
}
}
// updateRemoteConfigIfStale records a deployment only when the recommendation
// differs from the agent's effective config.
func (agent *Agent) updateRemoteConfigIfStale(configProvider AgentConfigProvider) bool {
recommendedConfig, err := configProvider.PreviewAgentConfig(agent.OrgID, []byte(agent.Config))
if err != nil {
agent.logger.Error("could not preview config recommendation for agent", "agent_id", agent.AgentID, errors.Attr(err))
return false
}
if string(recommendedConfig) == agent.Config {
return false
}
return agent.updateRemoteConfig(configProvider)
}
func (agent *Agent) updateRemoteConfig(configProvider AgentConfigProvider) bool {
recommendedConfig, confId, err := configProvider.RecommendAgentConfig(agent.OrgID, []byte(agent.Config))
if err != nil {

View File

@@ -18,6 +18,10 @@ type AgentConfigProvider interface {
err error,
)
// PreviewAgentConfig returns the config RecommendAgentConfig would, without
// recording a deployment.
PreviewAgentConfig(orgId valuer.UUID, currentConfYaml []byte) ([]byte, error)
// Report deployment status for config recommendations generated by RecommendAgentConfig
ReportConfigDeploymentStatus(
orgId valuer.UUID,

View File

@@ -255,6 +255,8 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewNormalizeQuickFilterFieldsFactory(sqlstore),
sqlmigration.NewAddSpanMapperOriginFactory(sqlstore, sqlschema),
sqlmigration.NewAddCloudIntegrationTuplesFactory(sqlstore),
sqlmigration.NewAddNotificationChannelTuplesFactory(sqlstore),
sqlmigration.NewAddAIObservabilityQuickFiltersFactory(sqlstore),
)
}

View File

@@ -0,0 +1,140 @@
package sqlmigration
import (
"context"
"database/sql"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addNotificationChannelTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddNotificationChannelTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_notification_channel_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addNotificationChannelTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addNotificationChannelTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addNotificationChannelTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
// notification-channel moved from legacy role gates to CheckResources. Existing
// organizations need the same tuples that new organizations receive from the
// managed-role registry at bootstrap.
tuples := []migrationTuple{
{authtypes.SigNozAdminRoleName, "metaresource", "notification-channel", "create"},
{authtypes.SigNozAdminRoleName, "metaresource", "notification-channel", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "notification-channel", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "notification-channel", "delete"},
{authtypes.SigNozAdminRoleName, "metaresource", "notification-channel", "list"},
{authtypes.SigNozEditorRoleName, "metaresource", "notification-channel", "read"},
{authtypes.SigNozEditorRoleName, "metaresource", "notification-channel", "list"},
{authtypes.SigNozViewerRoleName, "metaresource", "notification-channel", "read"},
{authtypes.SigNozViewerRoleName, "metaresource", "notification-channel", "list"},
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
return tx.Commit()
}
func (migration *addNotificationChannelTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,91 @@
package sqlmigration
import (
"context"
"time"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/valuer"
)
type storableAIObservabilityQuickFilter struct {
bun.BaseModel `bun:"table:quick_filter"`
ID valuer.UUID `bun:"id,pk,type:text"`
OrgID string `bun:"org_id,type:text,notnull"`
Filter string `bun:"filter,type:text,notnull"`
Source string `bun:"source,type:text,notnull"`
CreatedAt time.Time `bun:"created_at"`
UpdatedAt time.Time `bun:"updated_at"`
}
type addAIObservabilityQuickFilters struct{}
func NewAddAIObservabilityQuickFiltersFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_ai_o11y_quick_filters"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addAIObservabilityQuickFilters{}, nil
})
}
func (migration *addAIObservabilityQuickFilters) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addAIObservabilityQuickFilters) Up(ctx context.Context, db *bun.DB) error {
filters := []telemetryFieldKeyOutput{
{Name: "deployment.environment", FieldContext: "resource", FieldDataType: "string"},
{Name: "gen_ai.operation.name", FieldContext: "attribute", FieldDataType: "string"},
{Name: "gen_ai.provider.name", FieldContext: "attribute", FieldDataType: "string"},
{Name: "gen_ai.request.model", FieldContext: "attribute", FieldDataType: "string"},
{Name: "service.name", FieldContext: "resource", FieldDataType: "string"},
{Name: "gen_ai.tool.name", FieldContext: "attribute", FieldDataType: "string"},
{Name: "gen_ai.agent.name", FieldContext: "attribute", FieldDataType: "string"},
}
filterJSON, err := marshalUnescaped(filters)
if err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var orgIDs []string
if err := tx.NewSelect().Table("organizations").Column("id").Scan(ctx, &orgIDs); err != nil {
return err
}
if len(orgIDs) == 0 {
return tx.Commit()
}
now := time.Now()
rows := make([]*storableAIObservabilityQuickFilter, 0, len(orgIDs))
for _, orgID := range orgIDs {
rows = append(rows, &storableAIObservabilityQuickFilter{
ID: valuer.GenerateUUID(),
OrgID: orgID,
Filter: string(filterJSON),
Source: "ai_observability",
CreatedAt: now,
UpdatedAt: now,
})
}
if _, err := tx.NewInsert().Model(&rows).On("CONFLICT (org_id, source) DO NOTHING").Exec(ctx); err != nil {
return err
}
return tx.Commit()
}
func (migration *addAIObservabilityQuickFilters) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -47,7 +47,7 @@ var (
ResourceRole = NewResourceRole()
ResourceServiceAccount = NewResourceServiceAccount()
ResourceUser = NewResourceUser()
ResourceMetaResourceNotificationChannel = NewResourceMetaResource(KindNotificationChannel)
ResourceMetaResourceNotificationChannel = NewResourceMetaResource(KindNotificationChannel, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete)
ResourceMetaResourceRoutePolicy = NewResourceMetaResource(KindRoutePolicy)
ResourceMetaResourceApdexSetting = NewResourceMetaResource(KindApdexSetting)
ResourceMetaResourceAuthDomain = NewResourceMetaResource(KindAuthDomain)