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

[Inventory][ECO] Replace Entity with InventoryEntityLatest type #198760

Merged
merged 34 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ad95291
Improve typing
crespocarlos Oct 23, 2024
99cd0d4
Improve types
crespocarlos Oct 25, 2024
b23fae2
update type considering the latest changes
crespocarlos Nov 4, 2024
8ba7131
Adjust group by feature
crespocarlos Nov 4, 2024
9321024
Fix imports
crespocarlos Nov 4, 2024
5155dcf
Create an inteface between API and UI independent from index mapping
crespocarlos Nov 5, 2024
cb96542
Clean up
crespocarlos Nov 5, 2024
b0d66b1
Merge branch 'main' into 196142-adjust-types
elasticmachine Nov 5, 2024
bd28149
Missing change
crespocarlos Nov 5, 2024
0334bfc
Fix tests and zod schema
crespocarlos Nov 5, 2024
8a49c1c
Fix infra get_latest_entity
crespocarlos Nov 6, 2024
be5cc41
Revert get_entity_types change
crespocarlos Nov 6, 2024
8ef786e
Merge branch 'main' of github.com:elastic/kibana into 196142-adjust-t…
crespocarlos Nov 6, 2024
de12d05
Merge branch 'main' of github.com:elastic/kibana into 196142-adjust-t…
crespocarlos Nov 6, 2024
8d8df1c
Fix cypress tests
crespocarlos Nov 6, 2024
29551e2
CR fixes
crespocarlos Nov 8, 2024
595ac73
CR adjustments
crespocarlos Nov 8, 2024
185696f
More CR adjustments
crespocarlos Nov 8, 2024
b463ccc
More CR adjustments
crespocarlos Nov 8, 2024
33832dc
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Nov 8, 2024
1620988
Typo
crespocarlos Nov 8, 2024
a186a60
Fix join_by_key
crespocarlos Nov 8, 2024
b046148
Clean up
crespocarlos Nov 8, 2024
3f06d21
Merge branch 'main' of github.com:elastic/kibana into 196142-adjust-t…
crespocarlos Nov 8, 2024
3ccdac2
Fix after merge
crespocarlos Nov 8, 2024
38ffdc2
Type fix
crespocarlos Nov 8, 2024
ebf9c58
Merge branch 'main' into 196142-adjust-types
elasticmachine Nov 11, 2024
423b2a8
Replace API return type with InventoryEntity
crespocarlos Nov 11, 2024
f087c7d
Rename type guard function
crespocarlos Nov 11, 2024
38c7370
Fix after isEntityOfType was renamed
crespocarlos Nov 11, 2024
a370ab3
Merge branch 'main' of github.com:elastic/kibana into 196142-adjust-t…
crespocarlos Nov 12, 2024
535568b
Improve esql function type
crespocarlos Nov 12, 2024
21dd1a9
Fix after merge
crespocarlos Nov 12, 2024
3b5540d
Merge branch 'main' into 196142-adjust-types
elasticmachine Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export class K8sEntity extends Serializable<EntityFields> {
'entity.definition_id': `builtin_${entityTypeWithSchema}`,
'entity.identity_fields': identityFields,
'entity.display_name': getDisplayName({ identityFields, fields }),
'entity.definition_version': '1.0.0',
'entity.schema_version': '1.0',
});
}
}
Expand Down
18 changes: 13 additions & 5 deletions x-pack/packages/kbn-entities-schema/src/schema/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { arrayOfStringsSchema } from './common';
export const entityBaseSchema = z.object({
id: z.string(),
type: z.string(),
identity_fields: arrayOfStringsSchema,
identity_fields: z.union([arrayOfStringsSchema, z.string()]),
Copy link
Contributor Author

@crespocarlos crespocarlos Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esql returns a string when fields hold a single value.

display_name: z.string(),
metrics: z.record(z.string(), z.number()),
metrics: z.optional(z.record(z.string(), z.number())),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we should remove this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The framework still supports metrics even if the Inventory doesn't use it, let's leave this for now

definition_version: z.string(),
schema_version: z.string(),
definition_id: z.string(),
Expand All @@ -24,10 +24,18 @@ export interface MetadataRecord {
}

const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);

type Literal = z.infer<typeof literalSchema>;
type Metadata = Literal | { [key: string]: Metadata } | Metadata[];
export const entityMetadataSchema: z.ZodType<Metadata> = z.lazy(() =>
z.union([literalSchema, z.array(entityMetadataSchema), z.record(entityMetadataSchema)])
interface Metadata {
[key: string]: Literal | Literal[] | Metadata;
}

// Define the recursive schema using z.lazy
export const entityMetadataSchema: z.ZodSchema<Metadata> = z.lazy(() =>
z.union([
z.record(literalSchema), // Object with string keys and values of type string, number, or boolean
entityMetadataSchema, // Recursive reference to SomeType
])
);

export const entityLatestSchema = z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,50 @@ describe('joinByKey', () => {
},
});
});

it('deeply merges by unflatten keys', () => {
const joined = joinByKey(
[
{
service: {
name: 'opbeans-node',
metrics: {
cpu: 0.1,
},
},
properties: {
foo: 'bar',
},
},
{
service: {
environment: 'prod',
metrics: {
memory: 0.5,
},
},
properties: {
foo: 'bar',
},
},
],
'properties.foo'
);

expect(joined).toEqual([
{
service: {
name: 'opbeans-node',
environment: 'prod',
metrics: {
cpu: 0.1,
memory: 0.5,
},
},
properties: {
foo: 'bar',
},
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,29 @@ export type JoinedReturnType<
}
>;

type ArrayOrSingle<T> = T | T[];
function getValueByPath(obj: any, path: string): any {
return path.split('.').reduce((acc, keyPart) => {
// Check if acc is a valid object and has the key
return acc && acc[keyPart] !== undefined ? acc[keyPart] : undefined;
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved
}, obj);
}

type NestedKeys<T> = T extends object
? { [K in keyof T]: K extends string ? `${K}` | `${K}.${NestedKeys<T[K]>}` : never }[keyof T]
: never;

type ArrayOrSingle<T> = T | T[];
type CombinedNestedKeys<T, U> = (NestedKeys<T> & NestedKeys<U>) | (keyof T & keyof U);
export function joinByKey<
T extends Record<string, any>,
U extends UnionToIntersection<T>,
V extends ArrayOrSingle<keyof T & keyof U>
V extends ArrayOrSingle<CombinedNestedKeys<T, U>>
>(items: T[], key: V): JoinedReturnType<T, U>;

export function joinByKey<
T extends Record<string, any>,
U extends UnionToIntersection<T>,
V extends ArrayOrSingle<keyof T & keyof U>,
V extends ArrayOrSingle<CombinedNestedKeys<T, U>>,
W extends JoinedReturnType<T, U>,
X extends (a: T, b: T) => ValuesType<W>
>(items: T[], key: V, mergeFn: X): W;
Expand All @@ -45,7 +56,7 @@ export function joinByKey(
items.forEach((current) => {
// The key of the map is a stable JSON string of the values from given keys.
// We need stable JSON string to support plain object values.
const stableKey = stableStringify(keys.map((k) => current[k]));
const stableKey = stableStringify(keys.map((k) => current[k] ?? getValueByPath(current, k)));

if (map.has(stableKey)) {
const item = map.get(stableKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { ESQLSearchResponse, ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { withSpan } from '@kbn/apm-utils';
import type { EsqlQueryRequest } from '@elastic/elasticsearch/lib/api/types';
import { esqlResultToPlainObjects } from '../utils/esql_result_to_plain_objects';

type SearchRequest = ESSearchRequest & {
index: string | string[];
Expand All @@ -25,7 +26,10 @@ export interface ObservabilityElasticsearchClient {
operationName: string,
parameters: TSearchRequest
): Promise<InferSearchResponseOf<TDocument, TSearchRequest>>;
esql(operationName: string, parameters: EsqlQueryRequest): Promise<ESQLSearchResponse>;
esql<TDocument = unknown>(
operationName: string,
parameters: EsqlQueryRequest
): Promise<TDocument[]>;
client: ElasticsearchClient;
}

Expand All @@ -40,7 +44,7 @@ export function createObservabilityEsClient({
}): ObservabilityElasticsearchClient {
return {
client,
esql(operationName: string, parameters: EsqlQueryRequest) {
esql<TDocument = unknown>(operationName: string, parameters: EsqlQueryRequest) {
logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`);
return withSpan({ name: operationName, labels: { plugin } }, () => {
return client.esql.query(
Expand All @@ -54,7 +58,7 @@ export function createObservabilityEsClient({
})
.then((response) => {
logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`);
return response as unknown as ESQLSearchResponse;
return esqlResultToPlainObjects<TDocument>(response as unknown as ESQLSearchResponse);
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved
})
.catch((error) => {
throw error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,28 @@
*/

import type { ESQLSearchResponse } from '@kbn/es-types';
import { unflattenObject } from '../../object/unflatten_object';

export function esqlResultToPlainObjects<T extends Record<string, any>>(
export function esqlResultToPlainObjects<TDocument = unknown>(
result: ESQLSearchResponse
): T[] {
): TDocument[] {
return result.values.map((row) => {
return row.reduce<Record<string, unknown>>((acc, value, index) => {
const column = result.columns[index];
return unflattenObject(
row.reduce<Record<string, any>>((acc, value, index) => {
const column = result.columns[index];

if (!column) {
return acc;
}
if (!column) {
return acc;
}

// Removes the type suffix from the column name
const name = column.name.replace(/\.(text|keyword)$/, '');
if (!acc[name]) {
acc[name] = value;
}
// Removes the type suffix from the column name
const name = column.name.replace(/\.(text|keyword)$/, '');
if (!acc[name]) {
acc[name] = value;
}

return acc;
}, {});
}) as T[];
return acc;
}, {})
) as TDocument;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@

import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common';
import {
EntityDataStreamType,
SOURCE_DATA_STREAM_TYPE,
} from '@kbn/observability-shared-plugin/common';
import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
import { castArray } from 'lodash';
import { type InfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client';
import { getHasMetricsData } from './get_has_metrics_data';
import { getLatestEntity } from './get_latest_entity';
Expand Down Expand Up @@ -45,15 +43,15 @@ export async function getDataStreamTypes({
return Array.from(sourceDataStreams);
}

const entity = await getLatestEntity({
const latestEntity = await getLatestEntity({
inventoryEsClient: obsEsClient,
entityId,
entityType,
entityManagerClient,
});

if (entity?.[SOURCE_DATA_STREAM_TYPE]) {
[entity[SOURCE_DATA_STREAM_TYPE]].flat().forEach((item) => {
if (latestEntity) {
castArray(latestEntity.source_data_stream.type).forEach((item) => {
sourceDataStreams.add(item as EntityDataStreamType);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import {
SOURCE_DATA_STREAM_TYPE,
} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch';
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects';

const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({
type: '*',
dataset: ENTITY_LATEST,
});

interface Entity {
[SOURCE_DATA_STREAM_TYPE]: string | string[];
interface LatestEntityResponse {
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved
source_data_stream: { type?: string | string[] };
}

export async function getLatestEntity({
Expand All @@ -33,18 +32,18 @@ export async function getLatestEntity({
entityType: 'host' | 'container';
entityId: string;
entityManagerClient: EntityClient;
}): Promise<Entity | undefined> {
}): Promise<LatestEntityResponse | undefined> {
const { definitions } = await entityManagerClient.getEntityDefinitions({
builtIn: true,
type: entityType,
});

const hostOrContainerIdentityField = definitions[0]?.identityFields?.[0]?.field;
if (hostOrContainerIdentityField === undefined) {
return { [SOURCE_DATA_STREAM_TYPE]: [] };
return undefined;
}

const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', {
const response = await inventoryEsClient.esql<LatestEntityResponse>('get_latest_entities', {
query: `FROM ${ENTITIES_LATEST_ALIAS}
| WHERE ${ENTITY_TYPE} == ?
| WHERE ${hostOrContainerIdentityField} == ?
Expand All @@ -53,5 +52,5 @@ export async function getLatestEntity({
params: [entityType, entityId],
});

return esqlResultToPlainObjects<Entity>(latestEntitiesEsqlResponse)[0];
return response[0];
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ export function getMockInventoryContext(): InventoryKibanaContext {

return {
...coreStart,
entityManager: {} as unknown as EntityManagerPublicPluginStart,
entityManager: {
entityClient: {
asKqlFilter: jest.fn(),
getIdentityFieldsValue() {
return 'entity_id';
},
},
} as unknown as EntityManagerPublicPluginStart,
observabilityShared: {} as unknown as ObservabilitySharedPluginStart,
inference: {} as unknown as InferencePublicStart,
share: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@
* 2.0.
*/
import { z } from '@kbn/zod';
import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema';
import {
ENTITY_DEFINITION_ID,
ENTITY_DISPLAY_NAME,
ENTITY_ID,
ENTITY_IDENTITY_FIELDS,
ENTITY_LAST_SEEN,
ENTITY_TYPE,
} from '@kbn/observability-shared-plugin/common';
ENTITY_LATEST,
entitiesAliasPattern,
entityLatestSchema,
entityMetadataSchema,
} from '@kbn/entities-schema';
import { decode, encode } from '@kbn/rison';
import { isRight } from 'fp-ts/lib/Either';
import * as t from 'io-ts';

export const entityColumnIdsRt = t.union([
t.literal(ENTITY_DISPLAY_NAME),
t.literal(ENTITY_LAST_SEEN),
t.literal(ENTITY_TYPE),
t.literal('entityDisplayName'),
t.literal('entityLastSeenTimestamp'),
t.literal('entityType'),
t.literal('alertsCount'),
]);

Expand Down Expand Up @@ -102,23 +99,22 @@ export const entityTypesRt = new t.Type<string[], string, unknown>(
(arr) => arr.join()
);

export interface Entity {
[ENTITY_LAST_SEEN]: string;
[ENTITY_ID]: string;
[ENTITY_TYPE]: string;
[ENTITY_DISPLAY_NAME]: string;
[ENTITY_DEFINITION_ID]: string;
[ENTITY_IDENTITY_FIELDS]: string | string[];
alertsCount?: number;
[key: string]: any;
}

export type EntityGroup = {
count: number;
} & {
[key: string]: any;
};

export type InventoryEntityLatest = z.infer<typeof entityLatestSchema> & {
export type EntityLatest = z.infer<typeof entityLatestSchema>;
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved

export type InventoryEntityLatest = {
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved
entityId: string;
entityType: string;
entityIdentityFields: string | string[];
entityDisplayName: string;
entityDefinitionId: string;
entityLastSeenTimestamp: string;
entityDefinitionVersion: string;
entitySchemaVersion: string;
alertsCount?: number;
};
} & z.infer<typeof entityMetadataSchema>;
Copy link
Contributor Author

@crespocarlos crespocarlos Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is to make the API return an interface that doesn't depend in the index mapping. The Metadata, however, will return the objects as they are returned from the query

Loading