Skip to content

Commit

Permalink
[Asset Manager] services endpoint (elastic#160294)
Browse files Browse the repository at this point in the history
Closes elastic#159641

Implements `/assets/services` endpoint that returns service assets found
in the configured source (signals or assets indices). Consumer can
provide a `parent` query to filter the returned services. While the
_assets_ mode supports any kind of parent/depth thanks to its common
interface, the _signals_ mode only supports host parent for the moment.

1. pull this branch and point it at an oblt-cli created cluster that
uses cross-cluster search to read from the edge cluster
2. add the following[1] to your kibana.yml file
3. hit
`/api/asset-manager/assets/services?from=<from>&to=<to>&(parent=<host>)?`.
services should be returned. Add/remove parent query string to filter
services only running on specific host.
4. update `lockedSource: assets` to read from assets generated by
implicit collection
5. repeat 3.

[1]
```
xpack.assetManager:
  alphaEnabled: true
  sourceIndices:
    metrics: remote_cluster:metricbeat*,remote_cluster:metrics-*
    logs: remote_cluster:filebeat*,remote_cluster:logs-*
    traces: remote_cluster:traces-*
    serviceMetrics: remote_cluster:metrics-apm*
    serviceLogs: remote_cluster:logs-apm*
  lockedSource: signals
```

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
klacabane and kibanamachine committed Aug 17, 2023
1 parent a96785c commit 16e3c34
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import { AccessorOptions, OptionsWithInjectedValues } from '..';

export interface GetHostsOptions extends AccessorOptions {
from: number;
to: number;
from: string;
to: string;
}
export type GetHostsOptionsInjected = OptionsWithInjectedValues<GetHostsOptions>;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.
*/

import { Asset } from '../../../../common/types_api';
import { GetServicesOptionsInjected } from '.';
import { getAssets } from '../../get_assets';
import { getAllRelatedAssets } from '../../get_all_related_assets';

export async function getServicesByAssets(
options: GetServicesOptionsInjected
): Promise<{ services: Asset[] }> {
if (options.parent) {
return getServicesByParent(options);
}

const services = await getAssets({
esClient: options.esClient,
filters: {
kind: 'service',
from: options.from,
to: options.to,
},
});

return { services };
}

async function getServicesByParent(
options: GetServicesOptionsInjected
): Promise<{ services: Asset[] }> {
const { descendants } = await getAllRelatedAssets(options.esClient, {
from: options.from,
to: options.to,
maxDistance: 5,
kind: ['service'],
size: 100,
relation: 'descendants',
ean: options.parent!,
});

return { services: descendants as Asset[] };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.
*/

import { Asset } from '../../../../common/types_api';
import { GetServicesOptionsInjected } from '.';
import { collectServices } from '../../collectors/services';

export async function getServicesBySignals(
options: GetServicesOptionsInjected
): Promise<{ services: Asset[] }> {
const filters = [];

if (options.parent) {
filters.push({
bool: {
should: [
{ term: { 'host.name': options.parent } },
{ term: { 'host.hostname': options.parent } },
],
minimum_should_match: 1,
},
});
}

const { assets } = await collectServices({
client: options.esClient,
from: options.from,
to: options.to,
sourceIndices: options.sourceIndices,
filters,
});

return { services: assets };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.
*/

import { AccessorOptions, OptionsWithInjectedValues } from '..';

export interface GetServicesOptions extends AccessorOptions {
from: string;
to: string;
parent?: string;
}
export type GetServicesOptionsInjected = OptionsWithInjectedValues<GetServicesOptions>;

export interface ServiceIdentifier {
'asset.ean': string;
'asset.id': string;
'asset.name'?: string;
'service.environment'?: string;
}
12 changes: 12 additions & 0 deletions x-pack/plugins/asset_manager/server/lib/asset_accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { Asset } from '../../common/types_api';
import { AssetManagerConfig } from '../types';
import { OptionsWithInjectedValues } from './accessors';
import { GetHostsOptions } from './accessors/hosts';
import { GetServicesOptions } from './accessors/services';
import { getHostsByAssets } from './accessors/hosts/get_hosts_by_assets';
import { getHostsBySignals } from './accessors/hosts/get_hosts_by_signals';
import { getServicesByAssets } from './accessors/services/get_services_by_assets';
import { getServicesBySignals } from './accessors/services/get_services_by_signals';

interface AssetAccessorClassOptions {
sourceIndices: AssetManagerConfig['sourceIndices'];
Expand All @@ -35,4 +38,13 @@ export class AssetAccessor {
return await getHostsBySignals(withInjected);
}
}

async getServices(options: GetServicesOptions): Promise<{ services: Asset[] }> {
const withInjected = this.injectOptions(options);
if (this.options.source === 'assets') {
return await getServicesByAssets(withInjected);
} else {
return await getServicesBySignals(withInjected);
}
}
}
5 changes: 3 additions & 2 deletions x-pack/plugins/asset_manager/server/lib/collectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ export type Collector = (opts: CollectorOptions) => Promise<CollectorResult>;

export interface CollectorOptions {
client: ElasticsearchClient;
from: number;
to: number;
from: string;
to: string;
sourceIndices: AssetManagerConfig['sourceIndices'];
afterKey?: estypes.SortResults;
filters?: estypes.QueryDslQueryContainer[];
}

export interface CollectorResult {
Expand Down
29 changes: 17 additions & 12 deletions x-pack/plugins/asset_manager/server/lib/collectors/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@ export async function collectServices({
to,
sourceIndices,
afterKey,
filters = [],
}: CollectorOptions) {
const { traces, serviceMetrics, serviceLogs } = sourceIndices;
const musts: estypes.QueryDslQueryContainer[] = [
...filters,
{
exists: {
field: 'service.name',
},
},
];

const dsl: estypes.SearchRequest = {
index: [traces, serviceMetrics, serviceLogs],
size: 0,
Expand All @@ -33,13 +43,7 @@ export async function collectServices({
},
},
],
must: [
{
exists: {
field: 'service.name',
},
},
],
must: musts,
},
},
aggs: {
Expand All @@ -58,6 +62,7 @@ export async function collectServices({
serviceEnvironment: {
terms: {
field: 'service.environment',
missing_bucket: true,
},
},
},
Expand Down Expand Up @@ -112,14 +117,14 @@ export async function collectServices({
}

containerHosts.buckets?.forEach((containerBucket: any) => {
const [containerId, hostname] = containerBucket.key;
if (containerId) {
(service['asset.parents'] as string[]).push(`container:${containerId}`);
}

const [hostname, containerId] = containerBucket.key;
if (hostname) {
(service['asset.references'] as string[]).push(`host:${hostname}`);
}

if (containerId) {
(service['asset.parents'] as string[]).push(`container:${containerId}`);
}
});

acc.push(service);
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/asset_manager/server/routes/assets/hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export function hostsRoutes<T extends RequestHandlerContext>({

try {
const response = await assetAccessor.getHosts({
from: datemath.parse(from)!.valueOf(),
to: datemath.parse(to)!.valueOf(),
from: datemath.parse(from)!.toISOString(),
to: datemath.parse(to)!.toISOString(),
esClient,
});

Expand Down
70 changes: 70 additions & 0 deletions x-pack/plugins/asset_manager/server/routes/assets/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.
*/

import * as rt from 'io-ts';
import datemath from '@kbn/datemath';
import {
dateRt,
inRangeFromStringRt,
datemathStringRt,
createRouteValidationFunction,
createLiteralValueFromUndefinedRT,
} from '@kbn/io-ts-utils';
import { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import { debug } from '../../../common/debug_log';
import { SetupRouteOptions } from '../types';
import { ASSET_MANAGER_API_BASE } from '../../constants';
import { getEsClientFromContext } from '../utils';

const sizeRT = rt.union([inRangeFromStringRt(1, 100), createLiteralValueFromUndefinedRT(10)]);
const assetDateRT = rt.union([dateRt, datemathStringRt]);
const getServiceAssetsQueryOptionsRT = rt.exact(
rt.partial({
from: assetDateRT,
to: assetDateRT,
size: sizeRT,
parent: rt.string,
})
);

export type GetServiceAssetsQueryOptions = rt.TypeOf<typeof getServiceAssetsQueryOptionsRT>;

export function servicesRoutes<T extends RequestHandlerContext>({
router,
assetAccessor,
}: SetupRouteOptions<T>) {
// GET /assets/services
router.get<unknown, GetServiceAssetsQueryOptions, unknown>(
{
path: `${ASSET_MANAGER_API_BASE}/assets/services`,
validate: {
query: createRouteValidationFunction(getServiceAssetsQueryOptionsRT),
},
},
async (context, req, res) => {
const { from = 'now-24h', to = 'now', parent } = req.query || {};
const esClient = await getEsClientFromContext(context);

try {
const response = await assetAccessor.getServices({
from: datemath.parse(from)!.toISOString(),
to: datemath.parse(to)!.toISOString(),
parent,
esClient,
});

return res.ok({ body: response });
} catch (error: unknown) {
debug('Error while looking up SERVICE asset records', error);
return res.customError({
statusCode: 500,
body: { message: 'Error while looking up service asset records - ' + `${error}` },
});
}
}
);
}
2 changes: 2 additions & 0 deletions x-pack/plugins/asset_manager/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { pingRoute } from './ping';
import { assetsRoutes } from './assets';
import { sampleAssetsRoutes } from './sample_assets';
import { hostsRoutes } from './assets/hosts';
import { servicesRoutes } from './assets/services';

export function setupRoutes<T extends RequestHandlerContext>({
router,
Expand All @@ -20,4 +21,5 @@ export function setupRoutes<T extends RequestHandlerContext>({
assetsRoutes<T>({ router, assetAccessor });
sampleAssetsRoutes<T>({ router, assetAccessor });
hostsRoutes<T>({ router, assetAccessor });
servicesRoutes<T>({ router, assetAccessor });
}
Loading

0 comments on commit 16e3c34

Please sign in to comment.