mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-27 06:30:26 +00:00
Compare commits
1 Commits
chore/refa
...
deprecate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30d069154b |
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.117.1
|
||||
image: signoz/signoz:v0.117.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.117.1
|
||||
image: signoz/signoz:v0.117.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.117.1}
|
||||
image: signoz/signoz:${VERSION:-v0.117.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.117.1}
|
||||
image: signoz/signoz:${VERSION:-v0.117.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -7,21 +7,29 @@ import (
|
||||
"log/slog"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
|
||||
querierV5 "github.com/SigNoz/signoz/pkg/querier"
|
||||
|
||||
@@ -37,6 +45,16 @@ const (
|
||||
type AnomalyRule struct {
|
||||
*baserules.BaseRule
|
||||
|
||||
mtx sync.Mutex
|
||||
|
||||
reader interfaces.Reader
|
||||
|
||||
// querierV2 is used for alerts created after the introduction of new metrics query builder
|
||||
querierV2 interfaces.Querier
|
||||
|
||||
// querierV5 is used for alerts migrated after the introduction of new query builder
|
||||
querierV5 querierV5.Querier
|
||||
|
||||
provider anomaly.Provider
|
||||
providerV2 anomalyV2.Provider
|
||||
|
||||
@@ -85,6 +103,14 @@ func NewAnomalyRule(
|
||||
|
||||
logger.Info("using seasonality", "seasonality", t.seasonality.String())
|
||||
|
||||
querierOptsV2 := querierV2.QuerierOptions{
|
||||
Reader: reader,
|
||||
Cache: cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
}
|
||||
|
||||
t.querierV2 = querierV2.NewQuerier(querierOptsV2)
|
||||
t.reader = reader
|
||||
if t.seasonality == anomaly.SeasonalityHourly {
|
||||
t.provider = anomaly.NewHourlyProvider(
|
||||
anomaly.WithCache[*anomaly.HourlyProvider](cache),
|
||||
@@ -122,6 +148,7 @@ func NewAnomalyRule(
|
||||
)
|
||||
}
|
||||
|
||||
t.querierV5 = querierV5
|
||||
t.version = p.Version
|
||||
t.logger = logger
|
||||
return &t, nil
|
||||
@@ -306,8 +333,14 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
|
||||
prevState := r.State()
|
||||
|
||||
valueFormatter := units.FormatterFromUnit(r.Unit())
|
||||
|
||||
var res ruletypes.Vector
|
||||
var err error
|
||||
|
||||
if r.version == "v5" {
|
||||
r.logger.InfoContext(ctx, "running v5 query")
|
||||
res, err = r.buildAndRunQueryV5(ctx, r.OrgID(), ts)
|
||||
@@ -319,8 +352,220 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
opts := baserules.EvalVectorOptions{
|
||||
DeleteLabels: []string{labels.MetricNameLabel, labels.TemporalityLabel},
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
resultFPs := map[uint64]struct{}{}
|
||||
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
|
||||
|
||||
ruleReceivers := r.Threshold.GetRuleReceivers()
|
||||
ruleReceiverMap := make(map[string][]string)
|
||||
for _, value := range ruleReceivers {
|
||||
ruleReceiverMap[value.Name] = value.Channels
|
||||
}
|
||||
return r.EvalVector(ctx, ts, res, opts)
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
for _, lbl := range smpl.Metric {
|
||||
l[lbl.Name] = lbl.Value
|
||||
}
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
|
||||
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
// who are not used to Go's templating system.
|
||||
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
|
||||
|
||||
// utility function to apply go template on labels and annotations
|
||||
expand := func(text string) string {
|
||||
|
||||
tmpl := ruletypes.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
tmplData,
|
||||
times.Time(timestamp.FromTime(ts)),
|
||||
nil,
|
||||
)
|
||||
result, err := tmpl.Expand()
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("<error expanding template: %s>", err)
|
||||
r.logger.ErrorContext(ctx, "Expanding alert template failed", errors.Attr(err), "data", tmplData, "rule_name", r.Name())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
|
||||
resultLabels := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
|
||||
|
||||
for name, value := range r.Labels().Map() {
|
||||
lb.Set(name, expand(value))
|
||||
}
|
||||
|
||||
lb.Set(labels.AlertNameLabel, r.Name())
|
||||
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
||||
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(labels.Labels, 0, len(r.Annotations().Map()))
|
||||
for name, value := range r.Annotations().Map() {
|
||||
annotations = append(annotations, labels.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
if smpl.IsMissing {
|
||||
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(labels.NoDataLabel, "true")
|
||||
}
|
||||
|
||||
lbs := lb.Labels()
|
||||
h := lbs.Hash()
|
||||
resultFPs[h] = struct{}{}
|
||||
|
||||
if _, ok := alerts[h]; ok {
|
||||
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h])
|
||||
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
ActiveAt: ts,
|
||||
State: model.StatePending,
|
||||
Value: smpl.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
Missing: smpl.IsMissing,
|
||||
IsRecovering: smpl.IsRecovering,
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
// Update the last value and annotations if so, create a new alert entry otherwise.
|
||||
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
|
||||
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
// Update the recovering and missing state of existing alert
|
||||
alert.IsRecovering = a.IsRecovering
|
||||
alert.Missing = a.Missing
|
||||
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
|
||||
alert.Receivers = ruleReceiverMap[v]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
r.Active[h] = a
|
||||
}
|
||||
|
||||
itemsToAdd := []model.RuleStateHistory{}
|
||||
|
||||
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
|
||||
for fp, a := range r.Active {
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLables)
|
||||
if err != nil {
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "labels", a.Labels)
|
||||
}
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
a.State = model.StateInactive
|
||||
a.ResolvedAt = ts
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: model.StateInactive,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
|
||||
a.State = model.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := model.StateFiring
|
||||
if a.Missing {
|
||||
state = model.StateNoData
|
||||
}
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// We need to change firing alert to recovering if the returned sample meets recovery threshold
|
||||
changeFiringToRecovering := a.State == model.StateFiring && a.IsRecovering
|
||||
// We need to change recovering alerts to firing if the returned sample meets target threshold
|
||||
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
// in any of the above case we need to update the status of alert
|
||||
if changeFiringToRecovering || changeRecoveringToFiring {
|
||||
state := model.StateRecovering
|
||||
if changeRecoveringToFiring {
|
||||
state = model.StateFiring
|
||||
}
|
||||
a.State = state
|
||||
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
currentState := r.State()
|
||||
|
||||
overallStateChanged := currentState != prevState
|
||||
for idx, item := range itemsToAdd {
|
||||
item.OverallStateChanged = overallStateChanged
|
||||
item.OverallState = currentState
|
||||
itemsToAdd[idx] = item
|
||||
}
|
||||
|
||||
r.RecordRuleStateHistory(ctx, prevState, currentState, itemsToAdd)
|
||||
|
||||
return len(r.Active), nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) String() string {
|
||||
|
||||
ar := ruletypes.PostableRule{
|
||||
AlertName: r.Name(),
|
||||
RuleCondition: r.Condition(),
|
||||
EvalWindow: r.EvalWindow(),
|
||||
Labels: r.Labels().Map(),
|
||||
Annotations: r.Annotations().Map(),
|
||||
PreferredChannels: r.PreferredChannels(),
|
||||
}
|
||||
|
||||
byt, err := json.Marshal(ar)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error marshaling alerting rule: %s", err.Error())
|
||||
}
|
||||
|
||||
return string(byt)
|
||||
}
|
||||
|
||||
@@ -116,12 +116,7 @@ describe.each([
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('FORMAT')).toBeInTheDocument();
|
||||
expect(screen.getByText('Number of Rows')).toBeInTheDocument();
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.getByText('Columns')).toBeInTheDocument();
|
||||
}
|
||||
expect(screen.getByText('Columns')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allows changing export format', () => {
|
||||
@@ -151,17 +146,6 @@ describe.each([
|
||||
});
|
||||
|
||||
it('allows changing columns scope', () => {
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
|
||||
expect(screen.queryByRole('radio', { name: 'All' })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('radio', { name: 'Selected' }),
|
||||
).not.toBeInTheDocument();
|
||||
return;
|
||||
}
|
||||
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
|
||||
@@ -226,12 +210,7 @@ describe.each([
|
||||
mockUseQueryBuilder.mockReturnValue({ stagedQuery: mockQuery });
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
|
||||
// For traces, column scope is always Selected and the radio is hidden
|
||||
if (dataSource !== DataSource.TRACES) {
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Selected' }));
|
||||
}
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Selected' }));
|
||||
fireEvent.click(screen.getByText('Export'));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -248,11 +227,6 @@ describe.each([
|
||||
});
|
||||
|
||||
it('sends no selectFields when column scope is All', async () => {
|
||||
// For traces, column scope is always Selected — this test only applies to other sources
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'All' }));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Button, Popover, Radio, Tooltip, Typography } from 'antd';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
|
||||
import { Download, DownloadIcon, Loader2 } from 'lucide-react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -15,12 +14,10 @@ import './DownloadOptionsMenu.styles.scss';
|
||||
|
||||
interface DownloadOptionsMenuProps {
|
||||
dataSource: DataSource;
|
||||
selectedColumns?: TelemetryFieldKey[];
|
||||
}
|
||||
|
||||
export default function DownloadOptionsMenu({
|
||||
dataSource,
|
||||
selectedColumns,
|
||||
}: DownloadOptionsMenuProps): JSX.Element {
|
||||
const [exportFormat, setExportFormat] = useState<string>(DownloadFormats.CSV);
|
||||
const [rowLimit, setRowLimit] = useState<number>(DownloadRowCounts.TEN_K);
|
||||
@@ -38,19 +35,9 @@ export default function DownloadOptionsMenu({
|
||||
await handleExportRawData({
|
||||
format: exportFormat,
|
||||
rowLimit,
|
||||
clearSelectColumns:
|
||||
dataSource !== DataSource.TRACES &&
|
||||
columnsScope === DownloadColumnsScopes.ALL,
|
||||
selectedColumns,
|
||||
clearSelectColumns: columnsScope === DownloadColumnsScopes.ALL,
|
||||
});
|
||||
}, [
|
||||
exportFormat,
|
||||
rowLimit,
|
||||
columnsScope,
|
||||
selectedColumns,
|
||||
handleExportRawData,
|
||||
dataSource,
|
||||
]);
|
||||
}, [exportFormat, rowLimit, columnsScope, handleExportRawData]);
|
||||
|
||||
const popoverContent = useMemo(
|
||||
() => (
|
||||
@@ -85,22 +72,18 @@ export default function DownloadOptionsMenu({
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
{dataSource !== DataSource.TRACES && (
|
||||
<>
|
||||
<div className="horizontal-line" />
|
||||
<div className="horizontal-line" />
|
||||
|
||||
<div className="columns-scope">
|
||||
<Typography.Text className="title">Columns</Typography.Text>
|
||||
<Radio.Group
|
||||
value={columnsScope}
|
||||
onChange={(e): void => setColumnsScope(e.target.value)}
|
||||
>
|
||||
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
|
||||
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="columns-scope">
|
||||
<Typography.Text className="title">Columns</Typography.Text>
|
||||
<Radio.Group
|
||||
value={columnsScope}
|
||||
onChange={(e): void => setColumnsScope(e.target.value)}
|
||||
>
|
||||
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
|
||||
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -114,14 +97,7 @@ export default function DownloadOptionsMenu({
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
[
|
||||
exportFormat,
|
||||
rowLimit,
|
||||
columnsScope,
|
||||
isDownloading,
|
||||
handleExport,
|
||||
dataSource,
|
||||
],
|
||||
[exportFormat, rowLimit, columnsScope, isDownloading, handleExport],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -48,7 +48,6 @@ import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
|
||||
import GridCard from './GridCard';
|
||||
import { Card, CardContainer, ReactGridLayout } from './styles';
|
||||
import {
|
||||
applyRowCollapse,
|
||||
hasColumnWidthsChanged,
|
||||
removeUndefinedValuesFromLayout,
|
||||
} from './utils';
|
||||
@@ -269,10 +268,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedWidgets = selectedDashboard?.data?.widgets?.map((e) =>
|
||||
e.id === currentSelectRowId ? { ...e, title: newTitle } : e,
|
||||
currentWidget.title = newTitle;
|
||||
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
|
||||
(e) => e.id !== currentSelectRowId,
|
||||
);
|
||||
|
||||
updatedWidgets?.push(currentWidget);
|
||||
|
||||
const updatedSelectedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
data: {
|
||||
@@ -314,13 +316,88 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
}
|
||||
const { updatedLayout, updatedPanelMap } = applyRowCollapse(
|
||||
id,
|
||||
dashboardLayout,
|
||||
currentPanelMap,
|
||||
);
|
||||
setCurrentPanelMap((prev) => ({ ...prev, ...updatedPanelMap }));
|
||||
setDashboardLayout(sortLayout(updatedLayout));
|
||||
const rowProperties = { ...currentPanelMap[id] };
|
||||
const updatedPanelMap = { ...currentPanelMap };
|
||||
|
||||
let updatedDashboardLayout = [...dashboardLayout];
|
||||
if (rowProperties.collapsed === true) {
|
||||
rowProperties.collapsed = false;
|
||||
const widgetsInsideTheRow = rowProperties.widgets;
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
|
||||
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j].y += maxY;
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + maxY,
|
||||
}));
|
||||
}
|
||||
}
|
||||
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
|
||||
} else {
|
||||
rowProperties.collapsed = true;
|
||||
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
let widgetsInsideTheRow: Layout[] = [];
|
||||
let isPanelMapUpdated = false;
|
||||
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
|
||||
if (currentPanelMap[dashboardLayout[j].i]) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
widgetsInsideTheRow = [];
|
||||
isPanelMapUpdated = true;
|
||||
break;
|
||||
} else {
|
||||
widgetsInsideTheRow.push(dashboardLayout[j]);
|
||||
}
|
||||
}
|
||||
if (!isPanelMapUpdated) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
}
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout[currentIdx];
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j].y += maxY;
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + maxY,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
updatedDashboardLayout = updatedDashboardLayout.filter(
|
||||
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
|
||||
);
|
||||
}
|
||||
setCurrentPanelMap((prev) => ({
|
||||
...prev,
|
||||
...updatedPanelMap,
|
||||
[id]: {
|
||||
...rowProperties,
|
||||
},
|
||||
}));
|
||||
|
||||
setDashboardLayout(sortLayout(updatedDashboardLayout));
|
||||
};
|
||||
|
||||
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
import { applyRowCollapse, PanelMap } from '../utils';
|
||||
|
||||
// Helper to produce deeply-frozen objects that mimic what zustand/immer returns.
|
||||
function freeze<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj), (_, v) =>
|
||||
typeof v === 'object' && v !== null ? Object.freeze(v) : v,
|
||||
) as T;
|
||||
}
|
||||
|
||||
// ─── fixtures ────────────────────────────────────────────────────────────────
|
||||
|
||||
const ROW_ID = 'row1';
|
||||
|
||||
/** A layout with one row followed by two widgets. */
|
||||
function makeLayout(): Layout[] {
|
||||
return [
|
||||
{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 },
|
||||
{ i: 'w1', x: 0, y: 1, w: 6, h: 4 },
|
||||
{ i: 'w2', x: 6, y: 1, w: 6, h: 4 },
|
||||
];
|
||||
}
|
||||
|
||||
/** panelMap where the row is expanded (collapsed = false, widgets = []). */
|
||||
function makeExpandedPanelMap(): PanelMap {
|
||||
return {
|
||||
[ROW_ID]: { collapsed: false, widgets: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/** panelMap where the row is collapsed (widgets stored inside). */
|
||||
function makeCollapsedPanelMap(): PanelMap {
|
||||
return {
|
||||
[ROW_ID]: {
|
||||
collapsed: true,
|
||||
widgets: [
|
||||
{ i: 'w1', x: 0, y: 1, w: 6, h: 4 },
|
||||
{ i: 'w2', x: 6, y: 1, w: 6, h: 4 },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── frozen-input guard (regression for zustand/immer read-only bug) ──────────
|
||||
|
||||
describe('applyRowCollapse – does not mutate frozen inputs', () => {
|
||||
it('does not throw when collapsing a row with frozen layout + panelMap', () => {
|
||||
expect(() =>
|
||||
applyRowCollapse(
|
||||
ROW_ID,
|
||||
freeze(makeLayout()),
|
||||
freeze(makeExpandedPanelMap()),
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not throw when expanding a row with frozen layout + panelMap', () => {
|
||||
// Collapsed layout only has the row item; widgets live in panelMap.
|
||||
const collapsedLayout = freeze([{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }]);
|
||||
expect(() =>
|
||||
applyRowCollapse(ROW_ID, collapsedLayout, freeze(makeCollapsedPanelMap())),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('leaves the original layout array untouched after collapse', () => {
|
||||
const layout = makeLayout();
|
||||
const originalY = layout[1].y; // w1.y before collapse
|
||||
applyRowCollapse(ROW_ID, layout, makeExpandedPanelMap());
|
||||
expect(layout[1].y).toBe(originalY);
|
||||
});
|
||||
|
||||
it('leaves the original panelMap untouched after collapse', () => {
|
||||
const panelMap = makeExpandedPanelMap();
|
||||
applyRowCollapse(ROW_ID, makeLayout(), panelMap);
|
||||
expect(panelMap[ROW_ID].collapsed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── collapse behaviour ───────────────────────────────────────────────────────
|
||||
|
||||
describe('applyRowCollapse – collapsing a row', () => {
|
||||
it('sets collapsed = true on the row entry', () => {
|
||||
const { updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
makeLayout(),
|
||||
makeExpandedPanelMap(),
|
||||
);
|
||||
expect(updatedPanelMap[ROW_ID].collapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('stores the child widgets inside the panelMap entry', () => {
|
||||
const { updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
makeLayout(),
|
||||
makeExpandedPanelMap(),
|
||||
);
|
||||
const ids = updatedPanelMap[ROW_ID].widgets.map((w) => w.i);
|
||||
expect(ids).toContain('w1');
|
||||
expect(ids).toContain('w2');
|
||||
});
|
||||
|
||||
it('removes child widgets from the returned layout', () => {
|
||||
const { updatedLayout } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
makeLayout(),
|
||||
makeExpandedPanelMap(),
|
||||
);
|
||||
const ids = updatedLayout.map((l) => l.i);
|
||||
expect(ids).not.toContain('w1');
|
||||
expect(ids).not.toContain('w2');
|
||||
expect(ids).toContain(ROW_ID);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── expand behaviour ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('applyRowCollapse – expanding a row', () => {
|
||||
it('sets collapsed = false on the row entry', () => {
|
||||
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
|
||||
const { updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
collapsedLayout,
|
||||
makeCollapsedPanelMap(),
|
||||
);
|
||||
expect(updatedPanelMap[ROW_ID].collapsed).toBe(false);
|
||||
});
|
||||
|
||||
it('restores child widgets to the returned layout', () => {
|
||||
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
|
||||
const { updatedLayout } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
collapsedLayout,
|
||||
makeCollapsedPanelMap(),
|
||||
);
|
||||
const ids = updatedLayout.map((l) => l.i);
|
||||
expect(ids).toContain('w1');
|
||||
expect(ids).toContain('w2');
|
||||
});
|
||||
|
||||
it('restored child widgets appear in both the layout and the panelMap entry', () => {
|
||||
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
|
||||
const { updatedLayout, updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
collapsedLayout,
|
||||
makeCollapsedPanelMap(),
|
||||
);
|
||||
// The previously-stored widgets should now be back in the live layout.
|
||||
expect(updatedLayout.map((l) => l.i)).toContain('w1');
|
||||
// The panelMap entry still holds a reference to them (stale until next collapse).
|
||||
expect(updatedPanelMap[ROW_ID].widgets.map((w) => w.i)).toContain('w1');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── y-offset adjustment ──────────────────────────────────────────────────────
|
||||
|
||||
describe('applyRowCollapse – y-offset adjustments for rows below', () => {
|
||||
it('shifts items below a second row down when the first row expands', () => {
|
||||
const ROW2 = 'row2';
|
||||
// Layout: row1 (y=0,h=1) | w1 (y=1,h=4) | row2 (y=5,h=1) | w3 (y=6,h=2)
|
||||
const layout: Layout[] = [
|
||||
{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 },
|
||||
{ i: 'w1', x: 0, y: 1, w: 12, h: 4 },
|
||||
{ i: ROW2, x: 0, y: 5, w: 12, h: 1 },
|
||||
{ i: 'w3', x: 0, y: 6, w: 12, h: 2 },
|
||||
];
|
||||
const panelMap: PanelMap = {
|
||||
[ROW_ID]: {
|
||||
collapsed: true,
|
||||
widgets: [{ i: 'w1', x: 0, y: 1, w: 12, h: 4 }],
|
||||
},
|
||||
[ROW2]: { collapsed: false, widgets: [] },
|
||||
};
|
||||
// Expanding row1 should push row2 and w3 down by the height of w1 (4).
|
||||
const collapsedLayout = layout.filter((l) => l.i !== 'w1');
|
||||
const { updatedLayout } = applyRowCollapse(ROW_ID, collapsedLayout, panelMap);
|
||||
|
||||
const row2Item = updatedLayout.find((l) => l.i === ROW2);
|
||||
expect(row2Item?.y).toBe(5 + 4); // shifted by maxY = 4
|
||||
});
|
||||
});
|
||||
@@ -4,122 +4,6 @@ import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type PanelMap = Record<
|
||||
string,
|
||||
{ widgets: Layout[]; collapsed: boolean }
|
||||
>;
|
||||
|
||||
export interface RowCollapseResult {
|
||||
updatedLayout: Layout[];
|
||||
updatedPanelMap: PanelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure function that computes the new layout and panelMap after toggling a
|
||||
* row's collapsed state. All inputs are treated as immutable — no input object
|
||||
* is mutated, so it is safe to pass frozen objects from the zustand store.
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function applyRowCollapse(
|
||||
id: string,
|
||||
dashboardLayout: Layout[],
|
||||
currentPanelMap: PanelMap,
|
||||
): RowCollapseResult {
|
||||
// Deep-copy the row's own properties so we can mutate our local copy.
|
||||
const rowProperties = {
|
||||
...currentPanelMap[id],
|
||||
widgets: [...(currentPanelMap[id]?.widgets ?? [])],
|
||||
};
|
||||
|
||||
// Shallow-copy each entry's widgets array so inner .map() calls are safe.
|
||||
const updatedPanelMap: PanelMap = Object.fromEntries(
|
||||
Object.entries(currentPanelMap).map(([k, v]) => [
|
||||
k,
|
||||
{ ...v, widgets: [...v.widgets] },
|
||||
]),
|
||||
);
|
||||
|
||||
let updatedDashboardLayout = [...dashboardLayout];
|
||||
|
||||
if (rowProperties.collapsed === true) {
|
||||
// ── EXPAND ──────────────────────────────────────────────────────────────
|
||||
rowProperties.collapsed = false;
|
||||
const widgetsInsideTheRow = rowProperties.widgets;
|
||||
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
|
||||
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
|
||||
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j] = {
|
||||
...updatedDashboardLayout[j],
|
||||
y: updatedDashboardLayout[j].y + maxY,
|
||||
};
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({ ...w, y: w.y + maxY }));
|
||||
}
|
||||
}
|
||||
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
|
||||
} else {
|
||||
// ── COLLAPSE ─────────────────────────────────────────────────────────────
|
||||
rowProperties.collapsed = true;
|
||||
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
let widgetsInsideTheRow: Layout[] = [];
|
||||
let isPanelMapUpdated = false;
|
||||
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
|
||||
if (currentPanelMap[dashboardLayout[j].i]) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
widgetsInsideTheRow = [];
|
||||
isPanelMapUpdated = true;
|
||||
break;
|
||||
} else {
|
||||
widgetsInsideTheRow.push(dashboardLayout[j]);
|
||||
}
|
||||
}
|
||||
if (!isPanelMapUpdated) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
}
|
||||
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout[currentIdx];
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
|
||||
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j] = {
|
||||
...updatedDashboardLayout[j],
|
||||
y: updatedDashboardLayout[j].y + maxY,
|
||||
};
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({ ...w, y: w.y + maxY }));
|
||||
}
|
||||
}
|
||||
|
||||
updatedDashboardLayout = updatedDashboardLayout.filter(
|
||||
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
|
||||
);
|
||||
}
|
||||
|
||||
updatedPanelMap[id] = { ...rowProperties };
|
||||
|
||||
return { updatedLayout: updatedDashboardLayout, updatedPanelMap };
|
||||
}
|
||||
|
||||
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||
layout.map((obj) =>
|
||||
Object.fromEntries(
|
||||
|
||||
@@ -15,7 +15,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
||||
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
|
||||
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -294,23 +293,21 @@ export default function Home(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="home-container">
|
||||
{IS_SERVICE_ACCOUNTS_ENABLED && (
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
storageKey={LOCALSTORAGE.DISMISSED_API_KEYS_DEPRECATION_BANNER}
|
||||
message={
|
||||
<>
|
||||
<strong>API Keys</strong> have been deprecated and replaced by{' '}
|
||||
<strong>Service Accounts</strong>. Please migrate to Service Accounts for
|
||||
programmatic API access.
|
||||
</>
|
||||
}
|
||||
action={{
|
||||
label: 'Go to Service Accounts',
|
||||
onClick: (): void => history.push(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
storageKey={LOCALSTORAGE.DISMISSED_API_KEYS_DEPRECATION_BANNER}
|
||||
message={
|
||||
<>
|
||||
<strong>API Keys</strong> have been deprecated and replaced by{' '}
|
||||
<strong>Service Accounts</strong>. Please migrate to Service Accounts for
|
||||
programmatic API access.
|
||||
</>
|
||||
}
|
||||
action={{
|
||||
label: 'Go to Service Accounts',
|
||||
onClick: (): void => history.push(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="sticky-header">
|
||||
<Header
|
||||
|
||||
@@ -92,10 +92,7 @@ function LogsActionsContainer({
|
||||
/>
|
||||
</div>
|
||||
<div className="download-options-container">
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.LOGS}
|
||||
selectedColumns={options?.selectColumns}
|
||||
/>
|
||||
<DownloadOptionsMenu dataSource={DataSource.LOGS} />
|
||||
</div>
|
||||
<div className="format-options-container">
|
||||
<LogsFormatOptionsMenu
|
||||
|
||||
@@ -42,15 +42,8 @@ function LogsPanelComponent({
|
||||
setPageSize(value);
|
||||
setOffset(0);
|
||||
setRequestData((prev) => {
|
||||
const newQueryData = {
|
||||
...prev.query,
|
||||
builder: {
|
||||
...prev.query.builder,
|
||||
queryData: prev.query.builder.queryData.map((qd, i) =>
|
||||
i === 0 ? { ...qd, pageSize: value } : qd,
|
||||
),
|
||||
},
|
||||
};
|
||||
const newQueryData = { ...prev.query };
|
||||
newQueryData.builder.queryData[0].pageSize = value;
|
||||
return {
|
||||
...prev,
|
||||
query: newQueryData,
|
||||
|
||||
@@ -42,19 +42,11 @@ function Panel({
|
||||
};
|
||||
}
|
||||
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
|
||||
const updatedQueryForList = {
|
||||
...updatedQuery,
|
||||
builder: {
|
||||
...updatedQuery.builder,
|
||||
queryData: updatedQuery.builder.queryData.map((qd, i) =>
|
||||
i === 0 ? { ...qd, pageSize: 10 } : qd,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
query: updatedQueryForList,
|
||||
query: updatedQuery,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
|
||||
tableParams: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const IS_SERVICE_ACCOUNTS_ENABLED = false;
|
||||
|
||||
@@ -239,10 +239,7 @@ function ListView({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
selectedColumns={options?.selectColumns}
|
||||
/>
|
||||
<DownloadOptionsMenu dataSource={DataSource.TRACES} />
|
||||
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
|
||||
@@ -52,44 +52,37 @@ export const useGetQueryRange: UseGetQueryRange = (
|
||||
!firstQueryData?.filters?.items.some((filter) => filter.key?.key === 'id') &&
|
||||
firstQueryData?.orderBy[0].columnName === 'timestamp';
|
||||
|
||||
if (
|
||||
isListWithSingleTimestampOrder &&
|
||||
firstQueryData?.dataSource === DataSource.LOGS
|
||||
) {
|
||||
return {
|
||||
...requestData,
|
||||
graphType:
|
||||
requestData.graphType === PANEL_TYPES.BAR
|
||||
? PANEL_TYPES.TIME_SERIES
|
||||
: requestData.graphType,
|
||||
query: {
|
||||
...requestData.query,
|
||||
builder: {
|
||||
...requestData.query.builder,
|
||||
queryData: [
|
||||
{
|
||||
...firstQueryData,
|
||||
orderBy: [
|
||||
...(firstQueryData?.orderBy || []),
|
||||
{
|
||||
columnName: 'id',
|
||||
order: firstQueryData?.orderBy[0]?.order,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
const modifiedRequestData = {
|
||||
...requestData,
|
||||
graphType:
|
||||
requestData.graphType === PANEL_TYPES.BAR
|
||||
? PANEL_TYPES.TIME_SERIES
|
||||
: requestData.graphType,
|
||||
};
|
||||
|
||||
// If the query is a list with a single timestamp order, we need to add the id column to the order by clause
|
||||
if (
|
||||
isListWithSingleTimestampOrder &&
|
||||
firstQueryData?.dataSource === DataSource.LOGS
|
||||
) {
|
||||
modifiedRequestData.query.builder = {
|
||||
...requestData.query.builder,
|
||||
queryData: [
|
||||
{
|
||||
...firstQueryData,
|
||||
orderBy: [
|
||||
...(firstQueryData?.orderBy || []),
|
||||
{
|
||||
columnName: 'id',
|
||||
order: firstQueryData?.orderBy[0]?.order,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return modifiedRequestData;
|
||||
}, [requestData]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { message } from 'antd';
|
||||
import { downloadExportData } from 'api/v1/download/downloadExportData';
|
||||
import { prepareQueryRangePayloadV5, TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -14,7 +14,6 @@ interface ExportOptions {
|
||||
format: string;
|
||||
rowLimit: number;
|
||||
clearSelectColumns: boolean;
|
||||
selectedColumns?: TelemetryFieldKey[];
|
||||
}
|
||||
|
||||
interface UseExportRawDataProps {
|
||||
@@ -43,7 +42,6 @@ export function useExportRawData({
|
||||
format,
|
||||
rowLimit,
|
||||
clearSelectColumns,
|
||||
selectedColumns,
|
||||
}: ExportOptions): Promise<void> => {
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
@@ -52,12 +50,6 @@ export function useExportRawData({
|
||||
try {
|
||||
setIsDownloading(true);
|
||||
|
||||
const selectColumnsOverride = clearSelectColumns
|
||||
? {}
|
||||
: selectedColumns?.length
|
||||
? { selectColumns: selectedColumns }
|
||||
: {};
|
||||
|
||||
const exportQuery = {
|
||||
...stagedQuery,
|
||||
builder: {
|
||||
@@ -67,7 +59,7 @@ export function useExportRawData({
|
||||
groupBy: [],
|
||||
having: { expression: '' },
|
||||
limit: rowLimit,
|
||||
...selectColumnsOverride,
|
||||
...(clearSelectColumns && { selectColumns: [] }),
|
||||
})),
|
||||
queryTraceOperator: (stagedQuery.builder.queryTraceOperator || []).map(
|
||||
(traceOp) => ({
|
||||
@@ -75,7 +67,7 @@ export function useExportRawData({
|
||||
groupBy: [],
|
||||
having: { expression: '' },
|
||||
limit: rowLimit,
|
||||
...selectColumnsOverride,
|
||||
...(clearSelectColumns && { selectColumns: [] }),
|
||||
}),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ import logEvent from 'api/common/logEvent';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
|
||||
import { routeConfig } from 'container/SideNav/config';
|
||||
import { getQueryString } from 'container/SideNav/helper';
|
||||
import { settingsNavSections } from 'container/SideNav/menuItems';
|
||||
@@ -86,8 +85,7 @@ function SettingsPage(): JSX.Element {
|
||||
item.key === ROUTES.INGESTION_SETTINGS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
(IS_SERVICE_ACCOUNTS_ENABLED &&
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS) ||
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
|
||||
item.key === ROUTES.SHORTCUTS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
@@ -119,8 +117,7 @@ function SettingsPage(): JSX.Element {
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
(IS_SERVICE_ACCOUNTS_ENABLED &&
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS) ||
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
|
||||
item.key === ROUTES.INGESTION_SETTINGS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
@@ -147,8 +144,7 @@ function SettingsPage(): JSX.Element {
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
(IS_SERVICE_ACCOUNTS_ENABLED &&
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS)
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
|
||||
import { TFunction } from 'i18next';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
|
||||
@@ -64,11 +63,11 @@ export const getRoutes = (
|
||||
settings.push(...alertChannels(t));
|
||||
|
||||
if (isAdmin) {
|
||||
settings.push(...apiKeys(t), ...membersSettings(t));
|
||||
|
||||
if (IS_SERVICE_ACCOUNTS_ENABLED) {
|
||||
settings.push(...serviceAccountsSettings(t));
|
||||
}
|
||||
settings.push(
|
||||
...apiKeys(t),
|
||||
...membersSettings(t),
|
||||
...serviceAccountsSettings(t),
|
||||
);
|
||||
}
|
||||
|
||||
// todo: Sagar - check the condition for role list and details page, to whom we want to serve
|
||||
|
||||
@@ -2,7 +2,6 @@ package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
@@ -191,24 +190,6 @@ func NewBaseRule(id string, orgID valuer.UUID, p *ruletypes.PostableRule, reader
|
||||
return baseRule, nil
|
||||
}
|
||||
|
||||
func (r *BaseRule) String() string {
|
||||
ar := ruletypes.PostableRule{
|
||||
AlertName: r.name,
|
||||
RuleCondition: r.ruleCondition,
|
||||
EvalWindow: r.evalWindow,
|
||||
Labels: r.labels.Map(),
|
||||
Annotations: r.annotations.Map(),
|
||||
PreferredChannels: r.preferredChannels,
|
||||
}
|
||||
|
||||
byt, err := json.Marshal(ar)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error marshaling alerting rule: %s", err.Error())
|
||||
}
|
||||
|
||||
return string(byt)
|
||||
}
|
||||
|
||||
func (r *BaseRule) matchType() ruletypes.MatchType {
|
||||
if r.ruleCondition == nil {
|
||||
return ruletypes.AtleastOnce
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
)
|
||||
|
||||
type EvalVectorOptions struct {
|
||||
DeleteLabels []string
|
||||
ExtraAnnotations func(ctx context.Context, ts time.Time, metric labels.Labels) []labels.Label
|
||||
}
|
||||
|
||||
func (r *BaseRule) EvalVector(ctx context.Context, ts time.Time, res ruletypes.Vector, opts EvalVectorOptions) (int, error) {
|
||||
prevState := r.State()
|
||||
valueFormatter := units.FormatterFromUnit(r.Unit())
|
||||
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
resultFPs := map[uint64]struct{}{}
|
||||
alerts := make(map[uint64]*ruletypes.Alert, len(res))
|
||||
|
||||
ruleReceivers := r.Threshold.GetRuleReceivers()
|
||||
ruleReceiverMap := make(map[string][]string)
|
||||
for _, value := range ruleReceivers {
|
||||
ruleReceiverMap[value.Name] = value.Channels
|
||||
}
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
for _, lbl := range smpl.Metric {
|
||||
l[lbl.Name] = lbl.Value
|
||||
}
|
||||
|
||||
r.logger.DebugContext(ctx, "alerting for series", "rule_name", r.Name(), "series", smpl)
|
||||
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
|
||||
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
// who are not used to Go's templating system.
|
||||
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
|
||||
|
||||
// utility function to apply go template on labels and annotations
|
||||
expand := func(text string) string {
|
||||
tmpl := ruletypes.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
tmplData,
|
||||
times.Time(timestamp.FromTime(ts)),
|
||||
nil,
|
||||
)
|
||||
result, err := tmpl.Expand()
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("<error expanding template: %s>", err)
|
||||
r.logger.ErrorContext(ctx, "Expanding alert template failed", "rule_name", r.Name(), errors.Attr(err), "data", tmplData)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
lb := labels.NewBuilder(smpl.Metric).Del(opts.DeleteLabels...).Del()
|
||||
resultLabels := labels.NewBuilder(smpl.Metric).Del(opts.DeleteLabels...).Labels()
|
||||
|
||||
for name, value := range r.labels.Map() {
|
||||
lb.Set(name, expand(value))
|
||||
}
|
||||
|
||||
lb.Set(labels.AlertNameLabel, r.Name())
|
||||
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
||||
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(labels.Labels, 0, len(r.annotations.Map()))
|
||||
for name, value := range r.annotations.Map() {
|
||||
annotations = append(annotations, labels.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
if smpl.IsMissing {
|
||||
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(labels.NoDataLabel, "true")
|
||||
}
|
||||
|
||||
if opts.ExtraAnnotations != nil {
|
||||
extra := opts.ExtraAnnotations(ctx, ts, smpl.Metric)
|
||||
annotations = append(annotations, extra...)
|
||||
}
|
||||
|
||||
lbs := lb.Labels()
|
||||
h := lbs.Hash()
|
||||
resultFPs[h] = struct{}{}
|
||||
|
||||
if _, ok := alerts[h]; ok {
|
||||
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h])
|
||||
err := fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
// We have already acquired the lock above hence using SetHealth and SetLastError will deadlock.
|
||||
r.health = ruletypes.HealthBad
|
||||
r.lastError = err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
ActiveAt: ts,
|
||||
State: model.StatePending,
|
||||
Value: smpl.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
Missing: smpl.IsMissing,
|
||||
IsRecovering: smpl.IsRecovering,
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
|
||||
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
// Update the last value and annotations if so, create a new alert entry otherwise.
|
||||
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
// Update the recovering and missing state of existing alert
|
||||
alert.IsRecovering = a.IsRecovering
|
||||
alert.Missing = a.Missing
|
||||
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
|
||||
alert.Receivers = ruleReceiverMap[v]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
r.Active[h] = a
|
||||
}
|
||||
|
||||
itemsToAdd := []model.RuleStateHistory{}
|
||||
|
||||
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
|
||||
for fp, a := range r.Active {
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLables)
|
||||
if err != nil {
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "rule_name", r.Name(), "labels", a.Labels)
|
||||
}
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
r.logger.DebugContext(ctx, "converting firing alert to inActive", "name", r.Name())
|
||||
a.State = model.StateInactive
|
||||
a.ResolvedAt = ts
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: model.StateInactive,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration.Duration() {
|
||||
r.logger.DebugContext(ctx, "converting pending alert to firing", "name", r.Name())
|
||||
a.State = model.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := model.StateFiring
|
||||
if a.Missing {
|
||||
state = model.StateNoData
|
||||
}
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// We need to change firing alert to recovering if the returned sample meets recovery threshold
|
||||
changeFiringToRecovering := a.State == model.StateFiring && a.IsRecovering
|
||||
// We need to change recovering alerts to firing if the returned sample meets target threshold
|
||||
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
// in any of the above case we need to update the status of alert
|
||||
if changeFiringToRecovering || changeRecoveringToFiring {
|
||||
state := model.StateRecovering
|
||||
if changeRecoveringToFiring {
|
||||
state = model.StateFiring
|
||||
}
|
||||
a.State = state
|
||||
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
currentState := r.State()
|
||||
|
||||
overallStateChanged := currentState != prevState
|
||||
for idx, item := range itemsToAdd {
|
||||
item.OverallStateChanged = overallStateChanged
|
||||
item.OverallState = currentState
|
||||
itemsToAdd[idx] = item
|
||||
}
|
||||
|
||||
r.RecordRuleStateHistory(ctx, prevState, currentState, itemsToAdd)
|
||||
|
||||
r.health = ruletypes.HealthGood
|
||||
r.lastError = nil
|
||||
|
||||
return len(r.Active), nil
|
||||
}
|
||||
@@ -2,20 +2,25 @@ package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
plabels "github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
qslabels "github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -181,16 +186,232 @@ func (r *PromRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletype
|
||||
}
|
||||
|
||||
func (r *PromRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
prevState := r.State()
|
||||
valueFormatter := units.FormatterFromUnit(r.Unit())
|
||||
|
||||
// prepare query, run query get data and filter the data based on the threshold
|
||||
res, err := r.buildAndRunQuery(ctx, ts)
|
||||
results, err := r.buildAndRunQuery(ctx, ts)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
opts := EvalVectorOptions{
|
||||
DeleteLabels: []string{labels.MetricNameLabel},
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
resultFPs := map[uint64]struct{}{}
|
||||
|
||||
alerts := make(map[uint64]*ruletypes.Alert, len(results))
|
||||
|
||||
ruleReceivers := r.Threshold.GetRuleReceivers()
|
||||
ruleReceiverMap := make(map[string][]string)
|
||||
for _, value := range ruleReceivers {
|
||||
ruleReceiverMap[value.Name] = value.Channels
|
||||
}
|
||||
return r.EvalVector(ctx, ts, res, opts)
|
||||
|
||||
for _, result := range results {
|
||||
l := make(map[string]string, len(result.Metric))
|
||||
for _, lbl := range result.Metric {
|
||||
l[lbl.Name] = lbl.Value
|
||||
}
|
||||
r.logger.DebugContext(ctx, "alerting for series", "rule_name", r.Name(), "series", result)
|
||||
|
||||
threshold := valueFormatter.Format(result.Target, result.TargetUnit)
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, valueFormatter.Format(result.V, r.Unit()), threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
// who are not used to Go's templating system.
|
||||
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
|
||||
|
||||
expand := func(text string) string {
|
||||
tmpl := ruletypes.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
tmplData,
|
||||
times.Time(timestamp.FromTime(ts)),
|
||||
nil,
|
||||
)
|
||||
result, err := tmpl.Expand()
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("<error expanding template: %s>", err)
|
||||
r.logger.WarnContext(ctx, "Expanding alert template failed", "rule_name", r.Name(), errors.Attr(err), "data", tmplData)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
lb := qslabels.NewBuilder(result.Metric).Del(qslabels.MetricNameLabel)
|
||||
resultLabels := qslabels.NewBuilder(result.Metric).Del(qslabels.MetricNameLabel).Labels()
|
||||
|
||||
for name, value := range r.labels.Map() {
|
||||
lb.Set(name, expand(value))
|
||||
}
|
||||
|
||||
lb.Set(qslabels.AlertNameLabel, r.Name())
|
||||
lb.Set(qslabels.AlertRuleIdLabel, r.ID())
|
||||
lb.Set(qslabels.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(qslabels.Labels, 0, len(r.annotations.Map()))
|
||||
for name, value := range r.annotations.Map() {
|
||||
annotations = append(annotations, qslabels.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
if result.IsMissing {
|
||||
lb.Set(qslabels.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(qslabels.NoDataLabel, "true")
|
||||
}
|
||||
|
||||
lbs := lb.Labels()
|
||||
h := lbs.Hash()
|
||||
resultFPs[h] = struct{}{}
|
||||
|
||||
if _, ok := alerts[h]; ok {
|
||||
err = fmt.Errorf("vector contains metrics with the same labelset after applying alert labels")
|
||||
// We have already acquired the lock above hence using SetHealth and
|
||||
// SetLastError will deadlock.
|
||||
r.health = ruletypes.HealthBad
|
||||
r.lastError = err
|
||||
return 0, err
|
||||
}
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
ActiveAt: ts,
|
||||
State: model.StatePending,
|
||||
Value: result.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
Missing: result.IsMissing,
|
||||
IsRecovering: result.IsRecovering,
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
// Update the last value and annotations if so, create a new alert entry otherwise.
|
||||
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
// Update the recovering and missing state of existing alert
|
||||
alert.IsRecovering = a.IsRecovering
|
||||
alert.Missing = a.Missing
|
||||
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
|
||||
alert.Receivers = ruleReceiverMap[v]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
r.Active[h] = a
|
||||
|
||||
}
|
||||
|
||||
itemsToAdd := []model.RuleStateHistory{}
|
||||
|
||||
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
|
||||
for fp, a := range r.Active {
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLables)
|
||||
if err != nil {
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "rule_name", r.Name())
|
||||
}
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
a.State = model.StateInactive
|
||||
a.ResolvedAt = ts
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: model.StateInactive,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration.Duration() {
|
||||
a.State = model.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := model.StateFiring
|
||||
if a.Missing {
|
||||
state = model.StateNoData
|
||||
}
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// We need to change firing alert to recovering if the returned sample meets recovery threshold
|
||||
changeAlertingToRecovering := a.State == model.StateFiring && a.IsRecovering
|
||||
// We need to change recovering alerts to firing if the returned sample meets target threshold
|
||||
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
// in any of the above case we need to update the status of alert
|
||||
if changeAlertingToRecovering || changeRecoveringToFiring {
|
||||
state := model.StateRecovering
|
||||
if changeRecoveringToFiring {
|
||||
state = model.StateFiring
|
||||
}
|
||||
a.State = state
|
||||
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
r.health = ruletypes.HealthGood
|
||||
r.lastError = err
|
||||
|
||||
currentState := r.State()
|
||||
|
||||
overallStateChanged := currentState != prevState
|
||||
for idx, item := range itemsToAdd {
|
||||
item.OverallStateChanged = overallStateChanged
|
||||
item.OverallState = currentState
|
||||
itemsToAdd[idx] = item
|
||||
}
|
||||
|
||||
r.RecordRuleStateHistory(ctx, prevState, currentState, itemsToAdd)
|
||||
|
||||
return len(r.Active), nil
|
||||
}
|
||||
|
||||
func (r *PromRule) String() string {
|
||||
ar := ruletypes.PostableRule{
|
||||
AlertName: r.name,
|
||||
RuleCondition: r.ruleCondition,
|
||||
EvalWindow: r.evalWindow,
|
||||
Labels: r.labels.Map(),
|
||||
Annotations: r.annotations.Map(),
|
||||
PreferredChannels: r.preferredChannels,
|
||||
}
|
||||
|
||||
byt, err := json.Marshal(ar)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error marshaling alerting rule: %s", err.Error())
|
||||
}
|
||||
|
||||
return string(byt)
|
||||
}
|
||||
|
||||
func (r *PromRule) RunAlertQuery(ctx context.Context, qs string, start, end time.Time, interval time.Duration) (promql.Matrix, error) {
|
||||
@@ -242,7 +463,7 @@ func toCommonSeries(series promql.Series) v3.Series {
|
||||
Points: make([]v3.Point, 0),
|
||||
}
|
||||
|
||||
series.Metric.Range(func(lbl plabels.Label) {
|
||||
series.Metric.Range(func(lbl labels.Label) {
|
||||
commonSeries.Labels[lbl.Name] = lbl.Value
|
||||
commonSeries.LabelsArray = append(commonSeries.LabelsArray, map[string]string{
|
||||
lbl.Name: lbl.Value,
|
||||
|
||||
@@ -3,6 +3,7 @@ package rules
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
@@ -23,16 +24,21 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
querierV5 "github.com/SigNoz/signoz/pkg/querier"
|
||||
logsv3 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/querier"
|
||||
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
tracesV4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
querytemplate "github.com/SigNoz/signoz/pkg/query-service/utils/queryTemplate"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
|
||||
logsv3 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v3"
|
||||
tracesV4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
|
||||
querierV5 "github.com/SigNoz/signoz/pkg/querier"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
@@ -277,7 +283,7 @@ func (r *ThresholdRule) prepareLinksToTraces(ctx context.Context, ts time.Time,
|
||||
return contextlinks.PrepareLinksToTraces(start, end, filterItems)
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) *qbtypes.QueryRangeRequest {
|
||||
func (r *ThresholdRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*qbtypes.QueryRangeRequest, error) {
|
||||
r.logger.InfoContext(
|
||||
ctx, "prepare query range request v5", "ts", ts.UnixMilli(), "eval_window", r.evalWindow.Milliseconds(), "eval_delay", r.evalDelay.Milliseconds(),
|
||||
)
|
||||
@@ -296,13 +302,16 @@ func (r *ThresholdRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) *
|
||||
}
|
||||
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(r.Condition().CompositeQuery.Queries))
|
||||
copy(req.CompositeQuery.Queries, r.Condition().CompositeQuery.Queries)
|
||||
return req
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) prepareLinksToLogsV5(ctx context.Context, ts time.Time, lbls labels.Labels) string {
|
||||
selectedQuery := r.GetSelectedQuery()
|
||||
|
||||
qr := r.prepareQueryRangeV5(ctx, ts)
|
||||
qr, err := r.prepareQueryRangeV5(ctx, ts)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
start := time.UnixMilli(int64(qr.Start))
|
||||
end := time.UnixMilli(int64(qr.End))
|
||||
|
||||
@@ -339,7 +348,10 @@ func (r *ThresholdRule) prepareLinksToLogsV5(ctx context.Context, ts time.Time,
|
||||
func (r *ThresholdRule) prepareLinksToTracesV5(ctx context.Context, ts time.Time, lbls labels.Labels) string {
|
||||
selectedQuery := r.GetSelectedQuery()
|
||||
|
||||
qr := r.prepareQueryRangeV5(ctx, ts)
|
||||
qr, err := r.prepareQueryRangeV5(ctx, ts)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
start := time.UnixMilli(int64(qr.Start))
|
||||
end := time.UnixMilli(int64(qr.End))
|
||||
|
||||
@@ -488,7 +500,10 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
|
||||
params := r.prepareQueryRangeV5(ctx, ts)
|
||||
params, err := r.prepareQueryRangeV5(ctx, ts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []*v3.Result
|
||||
|
||||
@@ -565,8 +580,13 @@ func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUI
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
prevState := r.State()
|
||||
|
||||
valueFormatter := units.FormatterFromUnit(r.Unit())
|
||||
|
||||
var res ruletypes.Vector
|
||||
var err error
|
||||
|
||||
if r.version == "v5" {
|
||||
r.logger.InfoContext(ctx, "running v5 query")
|
||||
res, err = r.buildAndRunQueryV5(ctx, r.orgID, ts)
|
||||
@@ -574,35 +594,247 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
r.logger.InfoContext(ctx, "running v4 query")
|
||||
res, err = r.buildAndRunQuery(ctx, r.orgID, ts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
opts := EvalVectorOptions{
|
||||
DeleteLabels: []string{labels.MetricNameLabel, labels.TemporalityLabel},
|
||||
ExtraAnnotations: func(ctx context.Context, ts time.Time, smpl labels.Labels) []labels.Label {
|
||||
host := r.hostFromSource()
|
||||
if host == "" {
|
||||
return nil
|
||||
}
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
//// Links with timestamps should go in annotations since labels
|
||||
//// is used alert grouping, and we want to group alerts with the same
|
||||
//// label set, but different timestamps, together.
|
||||
switch r.typ { // DIFF
|
||||
case ruletypes.AlertTypeTraces:
|
||||
if link := r.prepareLinksToTraces(ctx, ts, smpl); link != "" {
|
||||
r.logger.InfoContext(ctx, "adding traces link to annotations", "link", fmt.Sprintf("%s/traces-explorer?%s", host, link))
|
||||
return []labels.Label{{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", host, link)}}
|
||||
}
|
||||
case ruletypes.AlertTypeLogs:
|
||||
if link := r.prepareLinksToLogs(ctx, ts, smpl); link != "" {
|
||||
r.logger.InfoContext(ctx, "adding logs link to annotations", "link", fmt.Sprintf("%s/logs/logs-explorer?%s", host, link))
|
||||
return []labels.Label{{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", host, link)}}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
resultFPs := map[uint64]struct{}{}
|
||||
alerts := make(map[uint64]*ruletypes.Alert, len(res))
|
||||
|
||||
ruleReceivers := r.Threshold.GetRuleReceivers()
|
||||
ruleReceiverMap := make(map[string][]string)
|
||||
for _, value := range ruleReceivers {
|
||||
ruleReceiverMap[value.Name] = value.Channels
|
||||
}
|
||||
return r.EvalVector(ctx, ts, res, opts)
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
for _, lbl := range smpl.Metric {
|
||||
l[lbl.Name] = lbl.Value
|
||||
}
|
||||
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
// todo(aniket): handle different threshold
|
||||
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
|
||||
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
// who are not used to Go's templating system.
|
||||
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
|
||||
|
||||
// utility function to apply go template on labels and annotations
|
||||
expand := func(text string) string {
|
||||
tmpl := ruletypes.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
tmplData,
|
||||
times.Time(timestamp.FromTime(ts)),
|
||||
nil,
|
||||
)
|
||||
result, err := tmpl.Expand()
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("<error expanding template: %s>", err)
|
||||
r.logger.ErrorContext(ctx, "Expanding alert template failed", errors.Attr(err), "data", tmplData)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
|
||||
resultLabels := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
|
||||
|
||||
for name, value := range r.labels.Map() {
|
||||
lb.Set(name, expand(value))
|
||||
}
|
||||
|
||||
lb.Set(labels.AlertNameLabel, r.Name())
|
||||
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
||||
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(labels.Labels, 0, len(r.annotations.Map()))
|
||||
for name, value := range r.annotations.Map() {
|
||||
annotations = append(annotations, labels.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
if smpl.IsMissing {
|
||||
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(labels.NoDataLabel, "true")
|
||||
}
|
||||
|
||||
// Links with timestamps should go in annotations since labels
|
||||
// is used alert grouping, and we want to group alerts with the same
|
||||
// label set, but different timestamps, together.
|
||||
switch r.typ {
|
||||
case ruletypes.AlertTypeTraces:
|
||||
link := r.prepareLinksToTraces(ctx, ts, smpl.Metric)
|
||||
if link != "" && r.hostFromSource() != "" {
|
||||
r.logger.InfoContext(ctx, "adding traces link to annotations", "link", fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link))
|
||||
annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)})
|
||||
}
|
||||
case ruletypes.AlertTypeLogs:
|
||||
link := r.prepareLinksToLogs(ctx, ts, smpl.Metric)
|
||||
if link != "" && r.hostFromSource() != "" {
|
||||
r.logger.InfoContext(ctx, "adding logs link to annotations", "link", fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link))
|
||||
annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)})
|
||||
}
|
||||
}
|
||||
|
||||
lbs := lb.Labels()
|
||||
h := lbs.Hash()
|
||||
resultFPs[h] = struct{}{}
|
||||
|
||||
if _, ok := alerts[h]; ok {
|
||||
return 0, fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
ActiveAt: ts,
|
||||
State: model.StatePending,
|
||||
Value: smpl.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
Missing: smpl.IsMissing,
|
||||
IsRecovering: smpl.IsRecovering,
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
|
||||
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
// Update the last value and annotations if so, create a new alert entry otherwise.
|
||||
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
|
||||
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
// Update the recovering and missing state of existing alert
|
||||
alert.IsRecovering = a.IsRecovering
|
||||
alert.Missing = a.Missing
|
||||
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
|
||||
alert.Receivers = ruleReceiverMap[v]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
r.Active[h] = a
|
||||
}
|
||||
|
||||
itemsToAdd := []model.RuleStateHistory{}
|
||||
|
||||
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
|
||||
for fp, a := range r.Active {
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLables)
|
||||
if err != nil {
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "labels", a.Labels)
|
||||
}
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
r.logger.DebugContext(ctx, "converting firing alert to inActive", "name", r.Name())
|
||||
a.State = model.StateInactive
|
||||
a.ResolvedAt = ts
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: model.StateInactive,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration.Duration() {
|
||||
r.logger.DebugContext(ctx, "converting pending alert to firing", "name", r.Name())
|
||||
a.State = model.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := model.StateFiring
|
||||
if a.Missing {
|
||||
state = model.StateNoData
|
||||
}
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// We need to change firing alert to recovering if the returned sample meets recovery threshold
|
||||
changeAlertingToRecovering := a.State == model.StateFiring && a.IsRecovering
|
||||
// We need to change recovering alerts to firing if the returned sample meets target threshold
|
||||
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
// in any of the above case we need to update the status of alert
|
||||
if changeAlertingToRecovering || changeRecoveringToFiring {
|
||||
state := model.StateRecovering
|
||||
if changeRecoveringToFiring {
|
||||
state = model.StateFiring
|
||||
}
|
||||
a.State = state
|
||||
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
currentState := r.State()
|
||||
|
||||
overallStateChanged := currentState != prevState
|
||||
for idx, item := range itemsToAdd {
|
||||
item.OverallStateChanged = overallStateChanged
|
||||
item.OverallState = currentState
|
||||
itemsToAdd[idx] = item
|
||||
}
|
||||
|
||||
r.RecordRuleStateHistory(ctx, prevState, currentState, itemsToAdd)
|
||||
|
||||
r.health = ruletypes.HealthGood
|
||||
r.lastError = err
|
||||
|
||||
return len(r.Active), nil
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) String() string {
|
||||
ar := ruletypes.PostableRule{
|
||||
AlertName: r.name,
|
||||
RuleCondition: r.ruleCondition,
|
||||
EvalWindow: r.evalWindow,
|
||||
Labels: r.labels.Map(),
|
||||
Annotations: r.annotations.Map(),
|
||||
PreferredChannels: r.preferredChannels,
|
||||
}
|
||||
|
||||
byt, err := json.Marshal(ar)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error marshaling alerting rule: %s", err.Error())
|
||||
}
|
||||
|
||||
return string(byt)
|
||||
}
|
||||
|
||||
@@ -206,11 +206,8 @@ func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// At least one aggregation required for aggregation queries, even if
|
||||
// they are disabled, usually because they are used in formula
|
||||
// regardless of use in formula, it's invalid to have empty Aggregations
|
||||
// for aggregation request
|
||||
if len(q.Aggregations) == 0 {
|
||||
// At least one aggregation required for non-disabled queries
|
||||
if len(q.Aggregations) == 0 && !q.Disabled {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"at least one aggregation is required",
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
@@ -32,14 +31,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "A",
|
||||
Disabled: true,
|
||||
Aggregations: []MetricAggregation{
|
||||
{
|
||||
MetricName: "test",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationMax,
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -47,12 +39,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[LogAggregation]{
|
||||
Name: "B",
|
||||
Disabled: true,
|
||||
Aggregations: []LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -74,14 +61,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "A",
|
||||
Disabled: true,
|
||||
Aggregations: []MetricAggregation{
|
||||
{
|
||||
MetricName: "test",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationMax,
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -214,14 +194,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "A",
|
||||
Disabled: true,
|
||||
Aggregations: []MetricAggregation{
|
||||
{
|
||||
MetricName: "test",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationMax,
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -259,12 +232,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[LogAggregation]{
|
||||
Name: "A",
|
||||
Disabled: true,
|
||||
Aggregations: []LogAggregation{
|
||||
{
|
||||
Expression: "sum(duration)",
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -398,12 +366,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[LogAggregation]{
|
||||
Name: "A",
|
||||
Disabled: true,
|
||||
Aggregations: []LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -411,12 +374,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[TraceAggregation]{
|
||||
Name: "A",
|
||||
Disabled: true,
|
||||
Aggregations: []TraceAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -438,12 +396,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[LogAggregation]{
|
||||
Name: "X",
|
||||
Disabled: true,
|
||||
Aggregations: []LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -451,14 +404,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "X",
|
||||
Disabled: true,
|
||||
Aggregations: []MetricAggregation{
|
||||
{
|
||||
MetricName: "test",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationMax,
|
||||
},
|
||||
},
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -481,9 +427,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
{Expression: "count()"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -637,9 +581,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
{Expression: "count()"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user