Skip to content

Commit fd03f11

Browse files
[SLO] Add structured key/value labels to SLO definitions
Adds an optional `labels` field (Record<string, string>) to SLO definitions so users can enrich SLOs with business context (e.g. team, cost_center, product). Aligns with the ECS `labels` convention and the existing Synthetics monitor `labels` field. - Schema: new `labelsSchema`; `labels` added to the SLO definition and the create/update route bodies. - Saved object: `labels` flattened mapping + model version 2 (mappings_addition + data_backfill defaulting to `{}`). - ES resources: `slo.labels` flattened mapping in SLI/summary component templates and a `set` processor in the SLI/summary ingest pipelines; SLO_RESOURCES_VERSION bumped to 3.7. - UI: key/value labels editor on the SLO edit form, labels badges on the SLO details Definition tab, and form <-> API conversion helpers. - Backfills `labels` to `{}` across repository decode, remote summary docs, and composite member decode for backward compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a067907 commit fd03f11

51 files changed

Lines changed: 550 additions & 69 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
174174
"siem-ui-timeline": "439d5deaa90cc74d10a13804db1b40b9c66ccadfb9c6b34b2bdfcedab8d80e41",
175175
"siem-ui-timeline-note": "c81b789cea05ba59973639194825838d48c571b851cd8c359c1e4aacf2c8d2c5",
176176
"siem-ui-timeline-pinned-event": "abd9cf88c47bd4662a898ba8f8fb736b4d3da14975f0532830b17896983bf83d",
177-
"slo": "682c6d9e3ba7a489def2b824da547e26f67a16343747425a1efdf618c7fdb3e7",
177+
"slo": "bc2837e33d6ab1e4dccc756aa091298ad0a3baafce65b7057c6b1dea7d232442",
178178
"slo-composite": "a3b2a8829b62bd23e2e711324a3b6b365d888ccb00929ce3b1e0f8e023c064e4",
179179
"slo-settings": "eaee24c76b1c02ba4ae1bf3742c1f5eca942a1662978f3420ec1b7f951746a32",
180180
"slo_template": "7c3298ec68d2104642a0ea3c320d961c90e51216327a6491a88c2688906e24bd",
@@ -1245,8 +1245,9 @@ describe('checking migration metadata changes on all registered SO types', () =>
12451245
"siem-ui-timeline-pinned-event|7.16.0: 91da406ec7758e5787f971ac10d62d7006d5cde5",
12461246
"==============================================================================",
12471247
"slo|global: 3bb1282c625b0cbdaf1317157a973f0eb263b13d",
1248-
"slo|mappings: 98c4bcb86ae664a5d21cd03fd7df79068b378caa",
1248+
"slo|mappings: bdd3ded97d616a91774629398f893f0ccbe0d363",
12491249
"slo|schemas: da39a3ee5e6b4b0d3255bfef95601890afd80709",
1250+
"slo|10.2.0: 75b6f88427f0fca000e7b448562f4bebfa81e46f81a297ef01ad278af2d50931",
12501251
"slo|10.1.0: 0079a5a79a3d5a70615113b6341d1b8378ff217e1adfc5afe5e281d0ed8a9816",
12511252
"slo|8.9.0: ea1a38eb158103aca3ad810d1282c7d0c05d27c3",
12521253
"===================================================",
@@ -1596,7 +1597,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
15961597
"siem-ui-timeline": "10.1.0",
15971598
"siem-ui-timeline-note": "10.0.0",
15981599
"siem-ui-timeline-pinned-event": "10.0.0",
1599-
"slo": "10.1.0",
1600+
"slo": "10.2.0",
16001601
"slo-composite": "10.1.0",
16011602
"slo-settings": "10.0.0",
16021603
"slo_template": "10.2.0",
@@ -1769,7 +1770,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
17691770
"siem-ui-timeline": "10.1.0",
17701771
"siem-ui-timeline-note": "7.16.0",
17711772
"siem-ui-timeline-pinned-event": "7.16.0",
1772-
"slo": "10.1.0",
1773+
"slo": "10.2.0",
17731774
"slo-composite": "10.1.0",
17741775
"slo-settings": "10.0.0",
17751776
"slo_template": "10.2.0",

x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/create.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { indicatorSchema, timeWindowSchema } from '../../schema';
99
import { allOrAnyStringOrArray } from '../../schema/common';
1010
import {
1111
budgetingMethodSchema,
12+
labelsSchema,
1213
objectiveSchema,
1314
optionalSettingsSchema,
1415
sloIdSchema,
@@ -29,6 +30,7 @@ const createSLOParamsSchema = t.type({
2930
id: sloIdSchema,
3031
settings: optionalSettingsSchema,
3132
tags: tagsSchema,
33+
labels: labelsSchema,
3234
groupBy: allOrAnyStringOrArray,
3335
revision: t.number,
3436
artifacts: t.partial({

x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/update.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { indicatorSchema, timeWindowSchema } from '../../schema';
99
import { allOrAnyStringOrArray } from '../../schema/common';
1010
import {
1111
budgetingMethodSchema,
12+
labelsSchema,
1213
objectiveSchema,
1314
optionalSettingsSchema,
1415
sloDefinitionSchema,
@@ -29,6 +30,7 @@ const updateSLOParamsSchema = t.type({
2930
objective: objectiveSchema,
3031
settings: optionalSettingsSchema,
3132
tags: tagsSchema,
33+
labels: labelsSchema,
3234
groupBy: allOrAnyStringOrArray,
3335
artifacts: t.partial({
3436
dashboards: t.array(t.type({ id: t.string })),

x-pack/platform/packages/shared/kbn-slo-schema/src/schema/slo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ const optionalSettingsSchema = t.partial({
4747

4848
const tagsSchema = t.array(t.string);
4949

50+
// Structured key/value labels to enrich SLO definitions with business context
51+
// (e.g. team, cost_center, product). Stored as a flattened field in Elasticsearch.
52+
const labelsSchema = t.record(t.string, t.string);
53+
5054
// id cannot contain special characters and spaces
5155
const sloIdSchema = new t.Type<string, string, unknown>(
5256
'sloIdSchema',
@@ -89,6 +93,7 @@ const requiredSloFields = t.type({
8993
revision: t.number,
9094
enabled: t.boolean,
9195
tags: tagsSchema,
96+
labels: labelsSchema,
9297
createdAt: dateType,
9398
updatedAt: dateType,
9499
groupBy: groupBySchema,
@@ -115,6 +120,7 @@ export {
115120
budgetingMethodSchema,
116121
dashboardsWithIdSchema,
117122
groupBySchema,
123+
labelsSchema,
118124
objectiveSchema,
119125
occurrencesBudgetingMethodSchema,
120126
optionalSettingsSchema,

x-pack/solutions/observability/plugins/slo/common/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const SUPPRESSED_PRIORITY_ACTION = {
5656
export const LOCK_ID_RESOURCE_INSTALLER = 'slo:resource_installer';
5757

5858
export const SLO_MODEL_VERSION = 2;
59-
export const SLO_RESOURCES_VERSION = 3.6;
59+
export const SLO_RESOURCES_VERSION = 3.7;
6060
export const SLO_RESOURCES_VERSION_MAJOR = 3;
6161

6262
export const SLI_COMPONENT_TEMPLATE_MAPPINGS_NAME = `.slo-observability.sli-mappings-v${SLO_RESOURCES_VERSION}`;

x-pack/solutions/observability/plugins/slo/public/data/slo/slo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export const baseSlo: Omit<SLOWithSummaryResponse, 'id'> = {
7676
groupings: {},
7777
instanceId: ALL_VALUE,
7878
tags: ['k8s', 'production', 'critical'],
79+
labels: { team: 'platform', cost_center: 'engineering' },
7980
enabled: true,
8081
createdAt: now,
8182
updatedAt: now,

x-pack/solutions/observability/plugins/slo/public/locators/slo_edit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('SloEditLocator', () => {
2525
it('returns the correct url when partial slo input is provided', async () => {
2626
const location = await locator.getLocation(buildSlo({ id: undefined }));
2727
expect(location.path).toEqual(
28-
"/create?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',groupings:(),indicator:(params:(dataViewId:some-data-view-id,filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',meta:(),name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',preventInitialBackfill:!f,syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z',version:2)"
28+
"/create?_a=(budgetingMethod:occurrences,createdAt:'2022-12-29T10:11:12.000Z',description:'some%20description%20useful',enabled:!t,groupBy:'*',groupings:(),indicator:(params:(dataViewId:some-data-view-id,filter:'baz:%20foo%20and%20bar%20%3E%202',good:'http_status:%202xx',index:some-index,timestampField:custom_timestamp,total:'a%20query'),type:sli.kql.custom),instanceId:'*',labels:(cost_center:engineering,team:platform),meta:(),name:'super%20important%20level%20service',objective:(target:0.98),revision:1,settings:(frequency:'1m',preventInitialBackfill:!f,syncDelay:'1m'),summary:(errorBudget:(consumed:0.064,initial:0.02,isEstimated:!f,remaining:0.936),fiveMinuteBurnRate:0,oneDayBurnRate:0,oneHourBurnRate:0,sliValue:0.99872,status:HEALTHY),tags:!(k8s,production,critical),timeWindow:(duration:'30d',type:rolling),updatedAt:'2022-12-29T10:11:12.000Z',version:2)"
2929
);
3030
});
3131
});

x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/definition/settings_panel.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export function SettingsPanel({ slo }: Props) {
4141
const { uiSettings } = useKibana().services;
4242
const percentFormat = uiSettings.get('format:percent:defaultPattern');
4343
const hasTags = slo.tags && slo.tags.length > 0;
44+
const labelsEntries = Object.entries(slo.labels ?? {});
45+
const hasLabels = labelsEntries.length > 0;
4446

4547
return (
4648
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
@@ -171,6 +173,25 @@ export function SettingsPanel({ slo }: Props) {
171173
</EuiDescriptionListDescription>
172174
</>
173175
)}
176+
177+
{hasLabels && (
178+
<>
179+
<EuiDescriptionListTitle>
180+
{i18n.translate('xpack.slo.sloDetails.definition.labelsTitle', {
181+
defaultMessage: 'Labels',
182+
})}
183+
</EuiDescriptionListTitle>
184+
<EuiDescriptionListDescription>
185+
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
186+
{labelsEntries.map(([key, value]) => (
187+
<EuiFlexItem key={key} grow={false}>
188+
<EuiBadge color="hollow">{`${key}: ${value}`}</EuiBadge>
189+
</EuiFlexItem>
190+
))}
191+
</EuiFlexGroup>
192+
</EuiDescriptionListDescription>
193+
</>
194+
)}
174195
</EuiDescriptionList>
175196
</EuiPanel>
176197
);

x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/slo_edit_form_description_section.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { useKibana } from '../../../hooks/use_kibana';
2222
import { useFetchSLOSuggestions } from '../hooks/use_fetch_suggestions';
2323
import type { CreateSLOForm } from '../types';
2424
import { OptionalText } from './common/optional_text';
25+
import { SloEditFormLabelsField } from './slo_edit_form_labels_field';
2526
import { MAX_WIDTH } from '../constants';
2627

2728
export function SloEditFormDescriptionSection() {
@@ -151,6 +152,7 @@ export function SloEditFormDescriptionSection() {
151152
)}
152153
/>
153154
</EuiFormRow>
155+
<SloEditFormLabelsField />
154156
<EuiFormRow
155157
fullWidth
156158
label={i18n.translate('xpack.slo.sloEdit.dashboards.label', {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
EuiButtonEmpty,
10+
EuiButtonIcon,
11+
EuiFieldText,
12+
EuiFlexGroup,
13+
EuiFlexItem,
14+
EuiFormRow,
15+
EuiSpacer,
16+
} from '@elastic/eui';
17+
import { i18n } from '@kbn/i18n';
18+
import React from 'react';
19+
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
20+
import type { CreateSLOForm } from '../types';
21+
import { OptionalText } from './common/optional_text';
22+
23+
export function SloEditFormLabelsField() {
24+
const { control } = useFormContext<CreateSLOForm>();
25+
const { fields, append, remove } = useFieldArray({ control, name: 'labels' });
26+
27+
return (
28+
<EuiFormRow
29+
fullWidth
30+
label={i18n.translate('xpack.slo.sloEdit.labels.label', {
31+
defaultMessage: 'Labels',
32+
})}
33+
labelAppend={<OptionalText />}
34+
helpText={i18n.translate('xpack.slo.sloEdit.labels.helpText', {
35+
defaultMessage:
36+
'Add structured key/value pairs to enrich this SLO with business context (e.g. team, cost_center).',
37+
})}
38+
>
39+
<div>
40+
{fields.map((field, index) => (
41+
<React.Fragment key={field.id}>
42+
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
43+
<EuiFlexItem>
44+
<Controller
45+
name={`labels.${index}.key`}
46+
control={control}
47+
defaultValue={field.key}
48+
rules={{ required: true }}
49+
render={({ field: { ref, ...keyField }, fieldState }) => (
50+
<EuiFieldText
51+
{...keyField}
52+
fullWidth
53+
isInvalid={fieldState.invalid}
54+
data-test-subj={`sloEditLabelsKeyInput${index}`}
55+
aria-label={i18n.translate('xpack.slo.sloEdit.labels.keyAriaLabel', {
56+
defaultMessage: 'Label key',
57+
})}
58+
placeholder={i18n.translate('xpack.slo.sloEdit.labels.keyPlaceholder', {
59+
defaultMessage: 'Key',
60+
})}
61+
/>
62+
)}
63+
/>
64+
</EuiFlexItem>
65+
<EuiFlexItem>
66+
<Controller
67+
name={`labels.${index}.value`}
68+
control={control}
69+
defaultValue={field.value}
70+
render={({ field: { ref, ...valueField } }) => (
71+
<EuiFieldText
72+
{...valueField}
73+
fullWidth
74+
data-test-subj={`sloEditLabelsValueInput${index}`}
75+
aria-label={i18n.translate('xpack.slo.sloEdit.labels.valueAriaLabel', {
76+
defaultMessage: 'Label value',
77+
})}
78+
placeholder={i18n.translate('xpack.slo.sloEdit.labels.valuePlaceholder', {
79+
defaultMessage: 'Value',
80+
})}
81+
/>
82+
)}
83+
/>
84+
</EuiFlexItem>
85+
<EuiFlexItem grow={false}>
86+
<EuiButtonIcon
87+
iconType="trash"
88+
color="danger"
89+
display="base"
90+
size="m"
91+
data-test-subj={`sloEditLabelsRemoveButton${index}`}
92+
aria-label={i18n.translate('xpack.slo.sloEdit.labels.removeAriaLabel', {
93+
defaultMessage: 'Remove label entry',
94+
})}
95+
onClick={() => remove(index)}
96+
/>
97+
</EuiFlexItem>
98+
</EuiFlexGroup>
99+
<EuiSpacer size="xs" />
100+
</React.Fragment>
101+
))}
102+
<EuiButtonEmpty
103+
iconType="plusInCircle"
104+
size="s"
105+
flush="left"
106+
data-test-subj="sloEditLabelsAddButton"
107+
onClick={() => append({ key: '', value: '' })}
108+
>
109+
{i18n.translate('xpack.slo.sloEdit.labels.addButtonLabel', {
110+
defaultMessage: 'Add label',
111+
})}
112+
</EuiButtonEmpty>
113+
</div>
114+
</EuiFormRow>
115+
);
116+
}

0 commit comments

Comments
 (0)