[asset manager] use metrics data plugin (#166756)

## Summary

Closes https://github.com/elastic/kibana/issues/166636

- use metrics_data_access plugin to dynamically retrieve indices where
needed
- add basic `/assets/hosts` api test

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Jason Rhodes <jason.rhodes@elastic.co>
This commit is contained in:
Kevin Lacabane 2023-09-27 23:56:42 +02:00 committed by GitHub
parent 02b7c96247
commit c84248c87d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 141 additions and 58 deletions

View file

@ -12,7 +12,8 @@
"optionalPlugins": [
],
"requiredPlugins": [
"apmDataAccess"
"apmDataAccess",
"metricsDataAccess"
],
"browser": false,
"server": true,

View file

@ -12,11 +12,18 @@ import { collectHosts } from '../../collectors/hosts';
export async function getHostsBySignals(
options: GetHostsOptionsInjected
): Promise<{ hosts: Asset[] }> {
const metricsIndices = await options.metricsClient.getMetricIndices({
savedObjectsClient: options.soClient,
});
const { assets } = await collectHosts({
client: options.esClient,
from: options.from,
to: options.to,
sourceIndices: options.sourceIndices,
sourceIndices: {
metrics: metricsIndices,
logs: options.sourceIndices.logs,
},
});
return {
hosts: assets,

View file

@ -7,12 +7,14 @@
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { APMDataAccessConfig } from '@kbn/apm-data-access-plugin/server';
import { MetricsDataClient } from '@kbn/metrics-data-access-plugin/server';
import { SavedObjectsClientContract } from '@kbn/core/server';
import { AssetManagerConfig } from '../../types';
export interface InjectedValues {
sourceIndices: AssetManagerConfig['sourceIndices'];
getApmIndices: (soClient: SavedObjectsClientContract) => Promise<APMDataAccessConfig['indices']>;
metricsClient: MetricsDataClient;
}
export type OptionsWithInjectedValues<T extends object> = T & InjectedValues;

View file

@ -31,7 +31,9 @@ export async function getServicesBySignals(
client: options.esClient,
from: options.from,
to: options.to,
apmIndices,
sourceIndices: {
apm: apmIndices,
},
filters,
});

View file

@ -6,6 +6,7 @@
*/
import { APMDataAccessConfig } from '@kbn/apm-data-access-plugin/server';
import { MetricsDataClient } from '@kbn/metrics-data-access-plugin/server';
import { SavedObjectsClientContract } from '@kbn/core/server';
import { Asset } from '../../common/types_api';
import { AssetManagerConfig } from '../types';
@ -21,6 +22,7 @@ interface AssetAccessorClassOptions {
sourceIndices: AssetManagerConfig['sourceIndices'];
source: AssetManagerConfig['lockedSource'];
getApmIndices: (soClient: SavedObjectsClientContract) => Promise<APMDataAccessConfig['indices']>;
metricsClient: MetricsDataClient;
}
export class AssetAccessor {
@ -31,6 +33,7 @@ export class AssetAccessor {
...options,
sourceIndices: this.options.sourceIndices,
getApmIndices: this.options.getApmIndices,
metricsClient: this.options.metricsClient,
};
}

View file

@ -16,9 +16,13 @@ export async function collectContainers({
sourceIndices,
afterKey,
}: CollectorOptions) {
if (!sourceIndices?.metrics || !sourceIndices?.logs) {
throw new Error('missing required metrics/logs indices');
}
const { metrics, logs } = sourceIndices;
const dsl: estypes.SearchRequest = {
index: [logs, metrics],
index: [metrics, logs],
size: QUERY_MAX_SIZE,
collapse: {
field: 'container.id',

View file

@ -16,6 +16,10 @@ export async function collectHosts({
sourceIndices,
afterKey,
}: CollectorOptions) {
if (!sourceIndices?.metrics || !sourceIndices?.logs) {
throw new Error('missing required metrics/logs indices');
}
const { metrics, logs } = sourceIndices;
const dsl: estypes.SearchRequest = {
index: [metrics, logs],

View file

@ -8,7 +8,6 @@
import { estypes } from '@elastic/elasticsearch';
import type { APMIndices } from '@kbn/apm-data-access-plugin/server';
import { ElasticsearchClient } from '@kbn/core/server';
import { AssetManagerConfig } from '../../types';
import { Asset } from '../../../common/types_api';
export const QUERY_MAX_SIZE = 10000;
@ -19,17 +18,15 @@ export interface CollectorOptions {
client: ElasticsearchClient;
from: string;
to: string;
sourceIndices: AssetManagerConfig['sourceIndices'];
sourceIndices?: {
apm?: APMIndices;
metrics?: string;
logs?: string;
};
afterKey?: estypes.SortResults;
filters?: estypes.QueryDslQueryContainer[];
}
type OmitSourceIndices<T> = Omit<T, 'sourceIndices'>;
export type ServicesCollectorOptions = OmitSourceIndices<CollectorOptions> & {
apmIndices: APMIndices;
};
export interface CollectorResult {
assets: Asset[];
afterKey?: estypes.SortResults;

View file

@ -10,6 +10,10 @@ import { Asset } from '../../../common/types_api';
import { CollectorOptions, QUERY_MAX_SIZE } from '.';
export async function collectPods({ client, from, to, sourceIndices, afterKey }: CollectorOptions) {
if (!sourceIndices?.metrics || !sourceIndices?.logs) {
throw new Error('missing required metrics/logs indices');
}
const { metrics, logs } = sourceIndices;
const dsl: estypes.SearchRequest = {
index: [metrics, logs],

View file

@ -7,17 +7,21 @@
import { estypes } from '@elastic/elasticsearch';
import { Asset } from '../../../common/types_api';
import { ServicesCollectorOptions, QUERY_MAX_SIZE } from '.';
import { CollectorOptions, QUERY_MAX_SIZE } from '.';
export async function collectServices({
client,
from,
to,
apmIndices,
sourceIndices,
afterKey,
filters = [],
}: ServicesCollectorOptions) {
const { transaction, error, metric } = apmIndices;
}: CollectorOptions) {
if (!sourceIndices?.apm) {
throw new Error('missing required apm indices');
}
const { transaction, error, metric } = sourceIndices.apm;
const musts: estypes.QueryDslQueryContainer[] = [
...filters,
{

View file

@ -59,6 +59,7 @@ export class AssetManagerServerPlugin
source: this.config.lockedSource,
sourceIndices: this.config.sourceIndices,
getApmIndices: plugins.apmDataAccess.getApmIndices,
metricsClient: plugins.metricsDataAccess.client,
});
const router = core.http.createRouter();

View file

@ -36,7 +36,6 @@ export function hostsRoutes<T extends RequestHandlerContext>({
router,
assetAccessor,
}: SetupRouteOptions<T>) {
// GET /assets/hosts
router.get<unknown, GetHostAssetsQueryOptions, unknown>(
{
path: `${ASSET_MANAGER_API_BASE}/assets/hosts`,

View file

@ -11,13 +11,13 @@ import {
ApmDataAccessPluginSetup,
ApmDataAccessPluginStart,
} from '@kbn/apm-data-access-plugin/server';
import { MetricsDataPluginSetup } from '@kbn/metrics-data-access-plugin/server';
export interface ElasticsearchAccessorOptions {
esClient: ElasticsearchClient;
}
export const INDEX_DEFAULTS = {
metrics: 'metricbeat-*,metrics-*',
logs: 'filebeat-*,logs-*',
};
@ -29,7 +29,6 @@ export const configSchema = schema.object({
// that value is propagated everywhere. For now, we duplicate the value here.
sourceIndices: schema.object(
{
metrics: schema.string({ defaultValue: INDEX_DEFAULTS.metrics }),
logs: schema.string({ defaultValue: INDEX_DEFAULTS.logs }),
},
{ defaultValue: INDEX_DEFAULTS }
@ -48,6 +47,7 @@ export type AssetManagerConfig = TypeOf<typeof configSchema>;
export interface AssetManagerPluginSetupDependencies {
apmDataAccess: ApmDataAccessPluginSetup;
metricsDataAccess: MetricsDataPluginSetup;
}
export interface AssetManagerPluginStartDependencies {
apmDataAccess: ApmDataAccessPluginStart;

View file

@ -20,6 +20,7 @@
"@kbn/core-elasticsearch-server",
"@kbn/core-http-request-handler-context-server",
"@kbn/datemath",
"@kbn/apm-data-access-plugin"
"@kbn/apm-data-access-plugin",
"@kbn/metrics-data-access-plugin"
]
}

View file

@ -0,0 +1,54 @@
/*
* 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 { timerange, infra } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { ASSETS_ENDPOINT } from '../constants';
import { FtrProviderContext } from '../../types';
const HOSTS_ASSETS_ENDPOINT = `${ASSETS_ENDPOINT}/hosts`;
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const synthtrace = getService('infraSynthtraceEsClient');
describe('GET /assets/hosts', () => {
beforeEach(async () => {
await synthtrace.clean();
});
it('should return hosts', async () => {
const from = new Date(Date.now() - 1000 * 60 * 2).toISOString();
const to = new Date().toISOString();
await synthtrace.index(generateHostsData({ from, to, count: 5 }));
const response = await supertest
.get(HOSTS_ASSETS_ENDPOINT)
.query({
from,
to,
})
.expect(200);
expect(response.body).to.have.property('hosts');
expect(response.body.hosts.length).to.equal(5);
});
});
}
function generateHostsData({ from, to, count = 1 }: { from: string; to: string; count: number }) {
const range = timerange(from, to);
const hosts = Array(count)
.fill(0)
.map((_, idx) => infra.host(`my-host-${idx}`));
return range
.interval('1m')
.rate(1)
.generator((timestamp, index) => hosts.map((host) => host.metrics().timestamp(timestamp)));
}

View file

@ -9,5 +9,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Asset Manager API Endpoints - with signals source', () => {
loadTestFile(require.resolve('./basics'));
loadTestFile(require.resolve('./hosts'));
loadTestFile(require.resolve('./services'));
});
}

View file

@ -17,53 +17,51 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const synthtrace = getService('apmSynthtraceEsClient');
describe('asset management', () => {
describe('GET /assets/services', () => {
beforeEach(async () => {
await synthtrace.clean();
});
describe('GET /assets/services', () => {
it('should return services', async () => {
const from = new Date(Date.now() - 1000 * 60 * 2).toISOString();
const to = new Date().toISOString();
await synthtrace.index(generateServicesData({ from, to, count: 2 }));
it('should return services', async () => {
const from = new Date(Date.now() - 1000 * 60 * 2).toISOString();
const to = new Date().toISOString();
await synthtrace.index(generateServicesData({ from, to, count: 2 }));
const response = await supertest
.get(SERVICES_ASSETS_ENDPOINT)
.query({
from,
to,
})
.expect(200);
const response = await supertest
.get(SERVICES_ASSETS_ENDPOINT)
.query({
from,
to,
})
.expect(200);
expect(response.body).to.have.property('services');
expect(response.body.services.length).to.equal(2);
});
expect(response.body).to.have.property('services');
expect(response.body.services.length).to.equal(2);
});
it('should return services running on specified host', async () => {
const from = new Date(Date.now() - 1000 * 60 * 2).toISOString();
const to = new Date().toISOString();
await synthtrace.index(generateServicesData({ from, to, count: 5 }));
it('should return services running on specified host', async () => {
const from = new Date(Date.now() - 1000 * 60 * 2).toISOString();
const to = new Date().toISOString();
await synthtrace.index(generateServicesData({ from, to, count: 5 }));
const response = await supertest
.get(SERVICES_ASSETS_ENDPOINT)
.query({
from,
to,
parent: 'my-host-1',
})
.expect(200);
const response = await supertest
.get(SERVICES_ASSETS_ENDPOINT)
.query({
from,
to,
parent: 'my-host-1',
})
.expect(200);
expect(response.body).to.have.property('services');
expect(response.body.services.length).to.equal(1);
expect(omit(response.body.services[0], ['@timestamp'])).to.eql({
'asset.kind': 'service',
'asset.id': 'service-1',
'asset.ean': 'service:service-1',
'asset.references': [],
'asset.parents': [],
'service.environment': 'production',
});
expect(response.body).to.have.property('services');
expect(response.body.services.length).to.equal(1);
expect(omit(response.body.services[0], ['@timestamp'])).to.eql({
'asset.kind': 'service',
'asset.id': 'service-1',
'asset.ean': 'service:service-1',
'asset.references': [],
'asset.parents': [],
'service.environment': 'production',
});
});
});