Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[eem] top_value metadata aggregation #188243

Merged
merged 15 commits into from
Sep 16, 2024

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 32 additions & 2 deletions x-pack/packages/kbn-entities-schema/src/schema/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('schemas', () => {
const result = metadataSchema.safeParse({
source: 'host.name',
destination: 'host.name',
limit: 0,
aggregation: { type: 'terms', limit: 0 },
});
expect(result.success).toBeFalsy();
expect(result).toMatchSnapshot();
Expand All @@ -52,11 +52,41 @@ describe('schemas', () => {
const result = metadataSchema.safeParse({
source: 'host.name',
destination: 'hostName',
size: 1,
});
expect(result.success).toBeTruthy();
expect(result).toMatchSnapshot();
});

it('should default to terms aggregation when none provided', () => {
const result = metadataSchema.safeParse({
source: 'host.name',
destination: 'hostName',
});
expect(result.success).toBeTruthy();
expect(result.data).toEqual({
source: 'host.name',
destination: 'hostName',
aggregation: { type: 'terms', limit: 1000 },
});
});

it('should parse supported aggregations', () => {
const result = metadataSchema.safeParse({
source: 'host.name',
destination: 'hostName',
aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } },
});
expect(result.success).toBeTruthy();
});

it('should reject unsupported aggregation', () => {
const result = metadataSchema.safeParse({
source: 'host.name',
destination: 'hostName',
aggregation: { type: 'unknown_agg', limit: 10 },
});
expect(result.success).toBeFalsy();
});
});

describe('durationSchema', () => {
Expand Down
28 changes: 23 additions & 5 deletions x-pack/packages/kbn-entities-schema/src/schema/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,40 @@ export const keyMetricSchema = z.object({

export type KeyMetric = z.infer<typeof keyMetricSchema>;

export const metadataAggregation = z.union([
z.object({ type: z.literal('terms'), limit: z.number().default(1000) }),
z.object({
type: z.literal('top_value'),
sort: z.record(z.string(), z.union([z.literal('asc'), z.literal('desc')])),
lookbackPeriod: z.optional(durationSchema),
}),
]);

export const metadataSchema = z
.object({
source: z.string(),
destination: z.optional(z.string()),
limit: z.optional(z.number().default(1000)),
aggregation: z
.optional(metadataAggregation)
.default({ type: z.literal('terms').value, limit: 1000 }),
})
.or(
z.string().transform((value) => ({
source: value,
destination: value,
aggregation: { type: z.literal('terms').value, limit: 1000 },
}))
)
.transform((metadata) => ({
...metadata,
destination: metadata.destination ?? metadata.source,
limit: metadata.limit ?? 1000,
}))
.or(z.string().transform((value) => ({ source: value, destination: value, limit: 1000 })))
.superRefine((value, ctx) => {
if (value.limit < 1) {
if (value.aggregation.type === 'terms' && value.aggregation.limit < 1) {
ctx.addIssue({
path: ['limit'],
code: z.ZodIssueCode.custom,
message: 'limit should be greater than 1',
message: 'limit for terms aggregation should be greater than 1',
});
}
if (value.source.length === 0) {
Expand All @@ -120,6 +136,8 @@ export const metadataSchema = z
}
});

export type MetadataField = z.infer<typeof metadataSchema>;

export const identityFieldsSchema = z
.object({
field: z.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const builtInServicesFromLogsEntityDefinition: EntityDefinition =
displayNameTemplate: '{{service.name}}{{#service.environment}}:{{.}}{{/service.environment}}',
metadata: [
{ source: '_index', destination: 'sourceIndex' },
{ source: 'agent.name', limit: 100 },
{ source: 'agent.name', aggregation: { type: 'terms', limit: 100 } },
'data_stream.type',
'service.environment',
'service.name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@
* 2.0.
*/

import { EntityDefinition, ENTITY_SCHEMA_VERSION_V1 } from '@kbn/entities-schema';
import { EntityDefinition, ENTITY_SCHEMA_VERSION_V1, MetadataField } from '@kbn/entities-schema';
import {
initializePathScript,
cleanScript,
} from '../helpers/ingest_pipeline_script_processor_helpers';
import { generateHistoryIndexName } from '../helpers/generate_component_id';
import { isBuiltinDefinition } from '../helpers/is_builtin_definition';

function mapDestinationToPainless(field: string) {
function getMetadataSourceField({ aggregation, destination, source }: MetadataField) {
if (aggregation.type === 'terms') {
return `ctx.entity.metadata.${destination}.keySet()`;
} else if (aggregation.type === 'top_value') {
return `ctx.entity.metadata.${destination}.top_value["${source}"]`;
}
}

function mapDestinationToPainless(metadata: MetadataField) {
const field = metadata.destination;
return `
${initializePathScript(field)}
ctx.${field} = ctx.entity.metadata.${field}.keySet();
ctx.${field} = ${getMetadataSourceField(metadata)};
`;
}

Expand All @@ -25,15 +34,27 @@ function createMetadataPainlessScript(definition: EntityDefinition) {
return '';
}

return definition.metadata.reduce((acc, def) => {
const destination = def.destination;
return definition.metadata.reduce((acc, metadata) => {
const { destination, source } = metadata;
const optionalFieldPath = destination.replaceAll('.', '?.');
const next = `
if (ctx.entity?.metadata?.${optionalFieldPath} != null) {
${mapDestinationToPainless(destination)}
}
`;
return `${acc}\n${next}`;

if (metadata.aggregation.type === 'terms') {
const next = `
if (ctx.entity?.metadata?.${optionalFieldPath} != null) {
${mapDestinationToPainless(metadata)}
}
`;
return `${acc}\n${next}`;
} else if (metadata.aggregation.type === 'top_value') {
const next = `
if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${source}"] != null) {
${mapDestinationToPainless(metadata)}
}
`;
return `${acc}\n${next}`;
}

return acc;
}, '');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@
* 2.0.
*/

import { EntityDefinition, ENTITY_SCHEMA_VERSION_V1 } from '@kbn/entities-schema';
import { EntityDefinition, ENTITY_SCHEMA_VERSION_V1, MetadataField } from '@kbn/entities-schema';
import {
initializePathScript,
cleanScript,
} from '../helpers/ingest_pipeline_script_processor_helpers';
import { generateLatestIndexName } from '../helpers/generate_component_id';
import { isBuiltinDefinition } from '../helpers/is_builtin_definition';

function mapDestinationToPainless(field: string) {
function getMetadataSourceField({ aggregation, destination, source }: MetadataField) {
if (aggregation.type === 'terms') {
return `ctx.entity.metadata.${destination}.data.keySet()`;
} else if (aggregation.type === 'top_value') {
return `ctx.entity.metadata.${destination}.top_value["${destination}"]`;
}
}

function mapDestinationToPainless(metadata: MetadataField) {
const field = metadata.destination;
return `
${initializePathScript(field)}
ctx.${field} = ctx.entity.metadata.${field}.data.keySet();
ctx.${field} = ${getMetadataSourceField(metadata)};
`;
}

Expand All @@ -25,15 +34,27 @@ function createMetadataPainlessScript(definition: EntityDefinition) {
return '';
}

return definition.metadata.reduce((acc, def) => {
const destination = def.destination || def.source;
return definition.metadata.reduce((acc, metadata) => {
const destination = metadata.destination;
const optionalFieldPath = destination.replaceAll('.', '?.');
const next = `
if (ctx.entity?.metadata?.${optionalFieldPath}.data != null) {
${mapDestinationToPainless(destination)}
}
`;
return `${acc}\n${next}`;

if (metadata.aggregation.type === 'terms') {
const next = `
if (ctx.entity?.metadata?.${optionalFieldPath}.data != null) {
${mapDestinationToPainless(metadata)}
}
`;
return `${acc}\n${next}`;
} else if (metadata.aggregation.type === 'top_value') {
const next = `
if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${destination}"] != null) {
${mapDestinationToPainless(metadata)}
}
`;
return `${acc}\n${next}`;
}

return acc;
}, '');
}

Expand Down
Loading