Repository: elastic/synthetics
Pull request #959 (2024-09-19 11:12:21)

feat: add labels meta
Allows the ability to add meta via fields.* fields, data will be appended under labels.* in heartbeat documents !!

fixes https://github.com/elastic/synthetics/issues/729 !!

This will only work with kibana 8.16.0  with PR  https://github.com/elastic/kibana/pull/193250 !!

```

  monitor.use({
    id: 'example-monitor',
    schedule: 10,
    fields: {
      foo: 'bar',
      team: 'synthetics',
    }
  });
```
### lightweight 
```

heartbeat.monitors:
- type: http
  name: Todos Lightweight
  id: "todos- light  weightt
  enabled: true
  urls: "https://elastic.github.io/synthetics-demo"
  schedule: '@every 3m10s'
  timeout: 16s
  alert.status.enabled: true
  fields.environment: "production"
  fields.team: "todos"
```


emilioalvap: @shahzad31 I'm worried this will confusing since the lightweight representation is not a valid heartbeat monitor and effectively undocumented, wdyt?
shahzad31: > @shahzad31 I'm worried this will confusing since the lightweight representation is not a valid heartbeat monitor and effectively undocumented, wdyt?

@emilioalvap i will created a docs issue in a follow up to document this. 
paulb-elastic: @shahzad31 
>This will only work with kibana 8.16.0

If using < 8.16.0, are these just ignored (i.e. it won't fail to `push`)?
shahzad31: > @shahzad31 
> >This will only work with kibana 8.16.0
> 
> If using < 8.16.0, are these just ignored (i.e. it won't fail to `push`)?

@paulb-elastic validation will fail the push. User will need to remove labels.

Patch:
diff --git a/src/common_types.ts b/src/common_types.ts
index 33a59f4..cc12faf 100644
--- a/src/common_types.ts
+++ b/src/common_types.ts
@@ -255,6 +255,7 @@ export type PushOptions = Partial<ProjectSettings> &
     kibanaVersion?: string;
     yes?: boolean;
     tags?: Array<string>;
+    fields?: MonitorConfig['fields'];
     alert?: AlertConfig;
     retestOnFailure?: MonitorConfig['retestOnFailure'];
     enabled?: boolean;
diff --git a/src/dsl/monitor.ts b/src/dsl/monitor.ts
index fdcf098..0e2b943 100644
--- a/src/dsl/monitor.ts
+++ b/src/dsl/monitor.ts
@@ -58,6 +58,7 @@ export type MonitorConfig = {
   name?: string;
   type?: string;
   tags?: string[];
+  fields?: Record<string, string>;
   schedule?: typeof ALLOWED_SCHEDULES[number];
   enabled?: boolean;
   locations?: SyntheticsLocationsType[];
diff --git a/src/push/monitor.ts b/src/push/monitor.ts
index 80996ea..217a87d 100644
--- a/src/push/monitor.ts
+++ b/src/push/monitor.ts
@@ -268,6 +268,7 @@ export function buildMonitorFromYaml(
     enabled: config.enabled ?? options.enabled,
     locations: options.locations,
     tags: options.tags,
+    fields: parseFields(config, options.fields),
     ...normalizeConfig(config),
     retestOnFailure,
     privateLocations,
@@ -311,6 +312,25 @@ export const parseAlertConfig = (
   return Object.keys(result).length > 0 ? result : undefined;
 };
 
+export const parseFields = (
+  config: MonitorConfig,
+  gFields?: Record<string, string>
+) => {
+  // get all keys starting with `label.`
+  const keys = Object.keys(config).filter(key => key.startsWith('fields.'));
+  const fields = {};
+  for (const key of keys) {
+    fields[key.replace('fields.', '')] = config[key];
+    delete config[key];
+  }
+  if (gFields) {
+    for (const key of Object.keys(gFields)) {
+      fields[key] = gFields[key];
+    }
+  }
+  return Object.keys(fields).length > 0 ? fields : undefined;
+};
+
 export function getAlertKeyValue(
   key: 'status' | 'tls',
   config: MonitorConfig,


Test patch:
diff --git a/__tests__/push/monitor.test.ts b/__tests__/push/monitor.test.ts
index 0c0fe9b..0d890c9 100644
--- a/__tests__/push/monitor.test.ts
+++ b/__tests__/push/monitor.test.ts
@@ -83,6 +83,7 @@ describe('Monitors', () => {
       hash: expect.any(String),
       locations: ['europe-west2-a', 'australia-southeast1-a'],
       privateLocations: ['germany'],
+      fields: { area: 'website' },
     });
   });
 
@@ -102,10 +103,15 @@ describe('Monitors', () => {
       filter: {
         match: 'test',
       },
+      fields: { area: 'website' },
     });
-    monitor.update({ locations: ['brazil'] });
+    monitor.update({ locations: ['brazil'], fields: { env: 'dev' } });
     const schema1 = await buildMonitorSchema([monitor], true);
     expect(schema1[0].hash).not.toEqual(schema[0].hash);
+    expect(schema1[0].fields).toEqual({
+      area: 'website',
+      env: 'dev',
+    });
   });
 
   it('parse @every schedule format', async () => {
@@ -472,6 +478,50 @@ heartbeat.monitors:
         hosts: ['elastic.co:443'],
       });
     });
+
+    it('supports fields in config', async () => {
+      await writeHBFile(`
+heartbeat.monitors:
+- type: icmp
+  schedule: @every 5m
+  id: "test-icmp"
+  name: "test-icmp"
+  privateLocations:
+    - baz
+  tags:
+    - ltag1
+    - ltag2
+  fields.foo: bar
+  fields.baz: qux
+      `);
+
+      const [mon] = await createLightweightMonitors(PROJECT_DIR, {
+        auth: 'foo',
+        params: { foo: 'bar' },
+        kibanaVersion: '8.8.0',
+        locations: ['australia_east'],
+        tags: ['gtag1', 'gtag2'],
+        privateLocations: ['gbaz'],
+        schedule: 10,
+        retestOnFailure: false,
+      });
+
+      expect(mon.config).toEqual({
+        id: 'test-icmp',
+        name: 'test-icmp',
+        locations: ['australia_east'],
+        privateLocations: ['baz'],
+        type: 'icmp',
+        params: { foo: 'bar' },
+        schedule: 5,
+        tags: ['ltag1', 'ltag2'],
+        retestOnFailure: false,
+        fields: {
+          baz: 'qux',
+          foo: 'bar',
+        },
+      });
+    });
   });
 
   describe('parseAlertConfig', () => {
diff --git a/__tests__/utils/test-config.ts b/__tests__/utils/test-config.ts
index 079be90..8eef4d6 100644
--- a/__tests__/utils/test-config.ts
+++ b/__tests__/utils/test-config.ts
@@ -39,6 +39,7 @@ export function createTestMonitor(filename: string, type = 'browser') {
     enabled: true,
     locations: ['united_kingdom', 'australia_east'],
     privateLocations: ['germany'],
+    fields: { area: 'website' },
   });
   monitor.setSource({
     file: join(FIXTURES_DIR, filename),