Skip to content

Commit

Permalink
[Fields Metadata] Add metadata fields static source (elastic#188453)
Browse files Browse the repository at this point in the history
## 📓 Summary

Closes elastic#188443 

Adding a static source repository for [metadata
fields](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html#_indexing_metadata_fields)
in the resolution chain, so that it's now possible to retrieve metadata
info for them too.

**GET /internal/fields_metadata?fieldNames=_index,_source**
```json
{
  "fields": {
    "_index": {
      "dashed_name": "index",
      "description": "The index to which the document belongs. This metadata field specifies the exact index name in which the document is stored.",
      "example": "index_1",
      "flat_name": "_index",
      "name": "_index",
      "short": "The index to which the document belongs.",
      "type": "keyword",
      "documentation_url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html",
      "source": "metadata",
      "normalize": []
    },
    "_source": {
      "dashed_name": "source",
      "description": "The original JSON representing the body of the document. This field contains all the source data that was provided at the time of indexing.",
      "example": "{\"user\": \"John Doe\", \"message\": \"Hello\"}",
      "flat_name": "_source",
      "name": "_source",
      "short": "The original JSON representing the body of the document.",
      "documentation_url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html",
      "source": "metadata",
      "normalize": [],
      "type": "unknown"
    }
  }
}
```

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 17, 2024
1 parent 8c6b5ac commit 5e9d2ae
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 17 deletions.
2 changes: 1 addition & 1 deletion docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ activities.
|{kib-repo}blob/{branch}/x-pack/plugins/fields_metadata/README.md[fieldsMetadata]
|The @kbn/fields-metadata-plugin is designed to provide a centralized and asynchronous way to consume field metadata across Kibana. This plugin addresses the need for on-demand retrieval of field metadata from static ECS definitions and integration manifests, with the flexibility to extend to additional resolution sources in the future.
|The @kbn/fields-metadata-plugin is designed to provide a centralized and asynchronous way to consume field metadata across Kibana. This plugin addresses the need for on-demand retrieval of field metadata from static ECS/Metadata definitions and integration manifests, with the flexibility to extend to additional resolution sources in the future.
|{kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload]
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fields_metadata/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Fields Metadata Plugin

The `@kbn/fields-metadata-plugin` is designed to provide a centralized and asynchronous way to consume field metadata across Kibana. This plugin addresses the need for on-demand retrieval of field metadata from static ECS definitions and integration manifests, with the flexibility to extend to additional resolution sources in the future.
The `@kbn/fields-metadata-plugin` is designed to provide a centralized and asynchronous way to consume field metadata across Kibana. This plugin addresses the need for on-demand retrieval of field metadata from static ECS/Metadata definitions and integration manifests, with the flexibility to extend to additional resolution sources in the future.

## Components and Mechanisms

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export type FieldsMetadataMap = Record<string, FieldMetadata>;
export class FieldsMetadataDictionary {
private constructor(private readonly fields: FieldsMetadataMap) {}

getFields() {
return this.fields;
}

pick(attributes: FieldAttribute[]): Record<string, PartialFieldMetadataPlain> {
return mapValues(this.fields, (field) => field.pick(attributes));
}
Expand Down
10 changes: 8 additions & 2 deletions x-pack/plugins/fields_metadata/common/fields_metadata/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import { EcsFlat } from '@elastic/ecs';
import * as rt from 'io-ts';
import { MetadataFields } from '../metadata_fields';

export const fieldSourceRT = rt.keyof({
ecs: null,
integration: null,
metadata: null,
unknown: null,
});

Expand Down Expand Up @@ -63,6 +65,7 @@ const optionalMetadataPlainRT = rt.partial({
short: rt.string,
source: fieldSourceRT,
type: rt.string,
documentation_url: rt.string,
});

export const partialFieldMetadataPlainRT = rt.intersection([
Expand All @@ -80,11 +83,14 @@ export const fieldAttributeRT = rt.union([
rt.keyof(optionalMetadataPlainRT.props),
]);

export type AnyFieldName = string & {};
export type TMetadataFields = typeof MetadataFields;
export type MetadataFieldName = keyof TMetadataFields;
export type TEcsFields = typeof EcsFlat;
export type EcsFieldName = keyof TEcsFields;
export type IntegrationFieldName = string;
export type IntegrationFieldName = AnyFieldName;

export type FieldName = EcsFieldName | (IntegrationFieldName & {});
export type FieldName = MetadataFieldName | EcsFieldName | IntegrationFieldName;
export type FieldMetadataPlain = rt.TypeOf<typeof fieldMetadataPlainRT>;
export type PartialFieldMetadataPlain = rt.TypeOf<typeof partialFieldMetadataPlainRT>;

Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fields_metadata/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

export { fieldMetadataPlainRT } from './fields_metadata/types';
export type {
AnyFieldName,
EcsFieldName,
FieldAttribute,
FieldMetadataPlain,
FieldName,
IntegrationFieldName,
PartialFieldMetadataPlain,
TEcsFields,
TMetadataFields,
} from './fields_metadata/types';

export { FieldMetadata } from './fields_metadata/models/field_metadata';
Expand Down
123 changes: 123 additions & 0 deletions x-pack/plugins/fields_metadata/common/metadata_fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const MetadataFields = {
_index: {
dashed_name: 'index',
description:
'The index to which the document belongs. This metadata field specifies the exact index name in which the document is stored.',
example: 'index_1',
flat_name: '_index',
name: '_index',
short: 'The index to which the document belongs.',
type: 'keyword',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html',
},
_id: {
dashed_name: 'id',
description:
'The document’s ID. This unique identifier is used to fetch, update, or delete a document within an index.',
example: '1',
flat_name: '_id',
name: '_id',
short: 'The document’s ID.',
type: 'keyword',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html',
},
_source: {
dashed_name: 'source',
description:
'The original JSON representing the body of the document. This field contains all the source data that was provided at the time of indexing.',
example: '{"user": "John Doe", "message": "Hello"}',
flat_name: '_source',
name: '_source',
short: 'The original JSON representing the body of the document.',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html',
},
_size: {
dashed_name: 'size',
description:
'The size of the _source field in bytes. Provided by the mapper-size plugin, this metadata field helps in understanding the storage impact of the document.',
example: '150',
flat_name: '_size',
name: '_size',
short: 'The size of the _source field in bytes, provided by the mapper-size plugin.',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-size.html',
},
_doc_count: {
dashed_name: 'doc_count',
description:
'A custom field used for storing document counts when a document represents pre-aggregated data. It helps in scenarios involving pre-computed data aggregation.',
example: '42',
flat_name: '_doc_count',
name: '_doc_count',
short:
'A custom field used for storing doc counts when a document represents pre-aggregated data.',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-doc-count-field.html',
},
_field_names: {
dashed_name: 'field_names',
description:
'All fields in the document which contain non-null values. This metadata field lists the field names that have valid data.',
example: '["user", "message"]',
flat_name: '_field_names',
name: '_field_names',
short: 'Fields with non-null values.',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-field-names-field.html',
},
_ignored: {
dashed_name: 'ignored',
description:
'All fields in the document that have been ignored at index time because of ignore_malformed. It indicates fields that were not indexed due to malformation.',
example: '["malformed_field"]',
flat_name: '_ignored',
name: '_ignored',
short: 'Fields ignored during indexing.',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-ignored-field.html',
},
_routing: {
dashed_name: 'routing',
description:
'A custom routing value which routes a document to a particular shard. This field is used to control the shard placement of a document.',
example: 'user_routing_value',
flat_name: '_routing',
name: '_routing',
short: 'Custom shard routing value.',
type: 'keyword',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-routing-field.html',
},
_meta: {
dashed_name: 'meta',
description:
'Application specific metadata. This field can store any custom metadata relevant to the application using Elasticsearch.',
example: '{"app": "my_app"}',
flat_name: '_meta',
name: '_meta',
short: 'Custom application metadata.',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-meta-field.html',
},
_tier: {
dashed_name: 'tier',
description:
'The current data tier preference of the index to which the document belongs. It helps in managing the index’s storage tier.',
example: 'hot',
flat_name: '_tier',
name: '_tier',
short: 'Index data tier preference.',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-tier-field.html',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FieldMetadata, TEcsFields } from '../../../common';
import { FieldMetadata, TEcsFields, TMetadataFields } from '../../../common';
import { loggerMock } from '@kbn/logging-mocks';
import { FieldsMetadataClient } from './fields_metadata_client';
import { EcsFieldsRepository } from './repositories/ecs_fields_repository';
import { IntegrationFieldsRepository } from './repositories/integration_fields_repository';
import { MetadataFieldsRepository } from './repositories/metadata_fields_repository';

const ecsFields = {
'@timestamp': {
Expand All @@ -26,6 +27,21 @@ const ecsFields = {
},
} as TEcsFields;

const metadataFields = {
_index: {
dashed_name: 'index',
description:
'The index to which the document belongs. This metadata field specifies the exact index name in which the document is stored.',
example: 'index_1',
flat_name: '_index',
name: '_index',
short: 'The index to which the document belongs.',
type: 'keyword',
documentation_url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html',
},
} as TMetadataFields;

const integrationFields = {
'1password.item_usages': {
'onepassword.client.platform_version': {
Expand All @@ -46,6 +62,7 @@ const integrationFields = {
describe('FieldsMetadataClient class', () => {
const logger = loggerMock.create();
const ecsFieldsRepository = EcsFieldsRepository.create({ ecsFields });
const metadataFieldsRepository = MetadataFieldsRepository.create({ metadataFields });
const integrationFieldsExtractor = jest.fn();
integrationFieldsExtractor.mockImplementation(() => Promise.resolve(integrationFields));

Expand All @@ -60,12 +77,13 @@ describe('FieldsMetadataClient class', () => {
fieldsMetadataClient = FieldsMetadataClient.create({
ecsFieldsRepository,
integrationFieldsRepository,
metadataFieldsRepository,
logger,
});
});

describe('#getByName', () => {
it('should resolve a single ECS FieldMetadata instance by default', async () => {
it('should resolve a single ECS/Metadata FieldMetadata instance by default', async () => {
const timestampFieldInstance = await fieldsMetadataClient.getByName('@timestamp');

expect(integrationFieldsExtractor).not.toHaveBeenCalled();
Expand All @@ -87,7 +105,7 @@ describe('FieldsMetadataClient class', () => {
expect(timestampField.hasOwnProperty('type')).toBeTruthy();
});

it('should attempt resolving the field from an integration if it does not exist in ECS and the integration and dataset params are provided', async () => {
it('should attempt resolving the field from an integration if it does not exist in ECS/Metadata and the integration and dataset params are provided', async () => {
const onePasswordFieldInstance = await fieldsMetadataClient.getByName(
'onepassword.client.platform_version',
{ integration: '1password', dataset: '1password.item_usages' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ import { Logger } from '@kbn/core/server';
import { FieldName, FieldMetadata, FieldsMetadataDictionary } from '../../../common';
import { EcsFieldsRepository } from './repositories/ecs_fields_repository';
import { IntegrationFieldsRepository } from './repositories/integration_fields_repository';
import { MetadataFieldsRepository } from './repositories/metadata_fields_repository';
import { IntegrationFieldsSearchParams } from './repositories/types';
import { FindFieldsMetadataOptions, IFieldsMetadataClient } from './types';

interface FieldsMetadataClientDeps {
logger: Logger;
ecsFieldsRepository: EcsFieldsRepository;
metadataFieldsRepository: MetadataFieldsRepository;
integrationFieldsRepository: IntegrationFieldsRepository;
}

export class FieldsMetadataClient implements IFieldsMetadataClient {
private constructor(
private readonly logger: Logger,
private readonly ecsFieldsRepository: EcsFieldsRepository,
private readonly metadataFieldsRepository: MetadataFieldsRepository,
private readonly integrationFieldsRepository: IntegrationFieldsRepository
) {}

Expand All @@ -31,8 +34,13 @@ export class FieldsMetadataClient implements IFieldsMetadataClient {
): Promise<FieldMetadata | undefined> {
this.logger.debug(`Retrieving field metadata for: ${fieldName}`);

// 1. Try resolving from ecs static metadata
let field = this.ecsFieldsRepository.getByName(fieldName);
// 1. Try resolving from metadata-fields static metadata
let field = this.metadataFieldsRepository.getByName(fieldName);

// 2. Try resolving from ecs static metadata
if (!field) {
field = this.ecsFieldsRepository.getByName(fieldName);
}

// 2. Try searching for the fiels in the Elastic Package Registry
if (!field && integration) {
Expand All @@ -48,7 +56,10 @@ export class FieldsMetadataClient implements IFieldsMetadataClient {
dataset,
}: FindFieldsMetadataOptions = {}): Promise<FieldsMetadataDictionary> {
if (!fieldNames) {
return this.ecsFieldsRepository.find();
return FieldsMetadataDictionary.create({
...this.metadataFieldsRepository.find().getFields(),
...this.ecsFieldsRepository.find().getFields(),
});
}

const fields: Record<string, FieldMetadata> = {};
Expand All @@ -66,8 +77,14 @@ export class FieldsMetadataClient implements IFieldsMetadataClient {
public static create({
logger,
ecsFieldsRepository,
metadataFieldsRepository,
integrationFieldsRepository,
}: FieldsMetadataClientDeps) {
return new FieldsMetadataClient(logger, ecsFieldsRepository, integrationFieldsRepository);
return new FieldsMetadataClient(
logger,
ecsFieldsRepository,
metadataFieldsRepository,
integrationFieldsRepository
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { Logger } from '@kbn/core/server';
import { FieldsMetadataClient } from './fields_metadata_client';
import { EcsFieldsRepository } from './repositories/ecs_fields_repository';
import { IntegrationFieldsRepository } from './repositories/integration_fields_repository';
import { MetadataFieldsRepository } from './repositories/metadata_fields_repository';
import { IntegrationFieldsExtractor } from './repositories/types';
import { FieldsMetadataServiceSetup, FieldsMetadataServiceStart } from './types';
import { MetadataFields as metadataFields } from '../../../common/metadata_fields';

export class FieldsMetadataService {
private integrationFieldsExtractor: IntegrationFieldsExtractor = () => Promise.resolve({});
Expand All @@ -30,6 +32,7 @@ export class FieldsMetadataService {
const { logger, integrationFieldsExtractor } = this;

const ecsFieldsRepository = EcsFieldsRepository.create({ ecsFields });
const metadataFieldsRepository = MetadataFieldsRepository.create({ metadataFields });
const integrationFieldsRepository = IntegrationFieldsRepository.create({
integrationFieldsExtractor,
});
Expand All @@ -39,6 +42,7 @@ export class FieldsMetadataService {
return FieldsMetadataClient.create({
logger,
ecsFieldsRepository,
metadataFieldsRepository,
integrationFieldsRepository,
});
},
Expand Down
Loading

0 comments on commit 5e9d2ae

Please sign in to comment.