Revert "[APM] Collect telemetry about data/API performance (#51612)"

This reverts commit 13baa51561.
This commit is contained in:
spalger 2020-03-23 22:26:15 -07:00
parent 35b222a840
commit 6de7f2a62b
31 changed files with 210 additions and 2391 deletions

View file

@ -36,8 +36,6 @@ const IGNORE_FILE_GLOBS = [
'**/*fixtures*/**/*',
// cypress isn't used in production, ignore it
'x-pack/legacy/plugins/apm/e2e/*',
// apm scripts aren't used in production, ignore them
'x-pack/legacy/plugins/apm/scripts/*',
];
run(async ({ log }) => {

View file

@ -14,13 +14,7 @@ import mappings from './mappings.json';
export const apm: LegacyPluginInitializer = kibana => {
return new kibana.Plugin({
require: [
'kibana',
'elasticsearch',
'xpack_main',
'apm_oss',
'task_manager'
],
require: ['kibana', 'elasticsearch', 'xpack_main', 'apm_oss'],
id: 'apm',
configPrefix: 'xpack.apm',
publicDir: resolve(__dirname, 'public'),
@ -77,10 +71,7 @@ export const apm: LegacyPluginInitializer = kibana => {
autocreateApmIndexPattern: Joi.boolean().default(true),
// service map
serviceMapEnabled: Joi.boolean().default(true),
// telemetry
telemetryCollectionEnabled: Joi.boolean().default(true)
serviceMapEnabled: Joi.boolean().default(true)
}).default();
},
@ -116,12 +107,10 @@ export const apm: LegacyPluginInitializer = kibana => {
}
}
});
const apmPlugin = server.newPlatform.setup.plugins
.apm as APMPluginContract;
apmPlugin.registerLegacyAPI({
server
});
apmPlugin.registerLegacyAPI({ server });
}
});
};

View file

@ -1,655 +1,12 @@
{
"apm-telemetry": {
"apm-services-telemetry": {
"properties": {
"agents": {
"properties": {
"dotnet": {
"properties": {
"agent": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"service": {
"properties": {
"framework": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"properties": {
"name": {
"type": "keyword",
"ignore_above": 256
}
}
},
"runtime": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
},
"go": {
"properties": {
"agent": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"service": {
"properties": {
"framework": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"runtime": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
},
"java": {
"properties": {
"agent": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"service": {
"properties": {
"framework": {
"type": "object"
},
"language": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"runtime": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
},
"js-base": {
"properties": {
"agent": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"service": {
"properties": {
"framework": {
"type": "object"
},
"language": {
"properties": {
"name": {
"type": "keyword",
"ignore_above": 256
}
}
},
"runtime": {
"type": "object"
}
}
}
}
},
"nodejs": {
"properties": {
"agent": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"service": {
"properties": {
"framework": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"properties": {
"name": {
"type": "keyword",
"ignore_above": 256
}
}
},
"runtime": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
},
"python": {
"properties": {
"agent": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"service": {
"properties": {
"framework": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"runtime": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
},
"ruby": {
"properties": {
"agent": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"service": {
"properties": {
"framework": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
},
"runtime": {
"properties": {
"composite": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"version": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
},
"rum-js": {
"properties": {
"agent": {
"type": "object"
},
"service": {
"properties": {
"framework": {
"type": "object"
},
"language": {
"type": "object"
},
"runtime": {
"type": "object"
}
}
}
}
}
}
},
"counts": {
"properties": {
"agent_configuration": {
"properties": {
"all": {
"type": "long"
}
}
},
"error": {
"properties": {
"1d": {
"type": "long"
},
"all": {
"type": "long"
}
}
},
"max_error_groups_per_service": {
"properties": {
"1d": {
"type": "long"
}
}
},
"max_transaction_groups_per_service": {
"properties": {
"1d": {
"type": "long"
}
}
},
"metric": {
"properties": {
"1d": {
"type": "long"
},
"all": {
"type": "long"
}
}
},
"onboarding": {
"properties": {
"1d": {
"type": "long"
},
"all": {
"type": "long"
}
}
},
"services": {
"properties": {
"1d": {
"type": "long"
}
}
},
"sourcemap": {
"properties": {
"1d": {
"type": "long"
},
"all": {
"type": "long"
}
}
},
"span": {
"properties": {
"1d": {
"type": "long"
},
"all": {
"type": "long"
}
}
},
"traces": {
"properties": {
"1d": {
"type": "long"
}
}
},
"transaction": {
"properties": {
"1d": {
"type": "long"
},
"all": {
"type": "long"
}
}
}
}
},
"cardinality": {
"properties": {
"user_agent": {
"properties": {
"original": {
"properties": {
"all_agents": {
"properties": {
"1d": {
"type": "long"
}
}
},
"rum": {
"properties": {
"1d": {
"type": "long"
}
}
}
}
}
}
},
"transaction": {
"properties": {
"name": {
"properties": {
"all_agents": {
"properties": {
"1d": {
"type": "long"
}
}
},
"rum": {
"properties": {
"1d": {
"type": "long"
}
}
}
}
}
}
}
}
},
"has_any_services": {
"type": "boolean"
},
"indices": {
"properties": {
"all": {
"properties": {
"total": {
"properties": {
"docs": {
"properties": {
"count": {
"type": "long"
}
}
},
"store": {
"properties": {
"size_in_bytes": {
"type": "long"
}
}
}
}
}
}
},
"shards": {
"properties": {
"total": {
"type": "long"
}
}
}
}
},
"integrations": {
"properties": {
"ml": {
"properties": {
"all_jobs_count": {
"type": "long"
}
}
}
}
},
"retainment": {
"properties": {
"error": {
"properties": {
"ms": {
"type": "long"
}
}
},
"metric": {
"properties": {
"ms": {
"type": "long"
}
}
},
"onboarding": {
"properties": {
"ms": {
"type": "long"
}
}
},
"span": {
"properties": {
"ms": {
"type": "long"
}
}
},
"transaction": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"services_per_agent": {
"properties": {
"dotnet": {
"type": "long",
"null_value": 0
},
"go": {
"python": {
"type": "long",
"null_value": 0
},
@ -657,15 +14,19 @@
"type": "long",
"null_value": 0
},
"js-base": {
"type": "long",
"null_value": 0
},
"nodejs": {
"type": "long",
"null_value": 0
},
"python": {
"js-base": {
"type": "long",
"null_value": 0
},
"rum-js": {
"type": "long",
"null_value": 0
},
"dotnet": {
"type": "long",
"null_value": 0
},
@ -673,131 +34,11 @@
"type": "long",
"null_value": 0
},
"rum-js": {
"go": {
"type": "long",
"null_value": 0
}
}
},
"tasks": {
"properties": {
"agent_configuration": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"agents": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"cardinality": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"groupings": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"indices_stats": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"integrations": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"processor_events": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"services": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
},
"versions": {
"properties": {
"took": {
"properties": {
"ms": {
"type": "long"
}
}
}
}
}
}
},
"version": {
"properties": {
"apm_server": {
"properties": {
"major": {
"type": "long"
},
"minor": {
"type": "long"
},
"patch": {
"type": "long"
}
}
}
}
}
}
},

View file

@ -1 +0,0 @@
yarn.lock

View file

@ -1,10 +0,0 @@
{
"name": "apm-scripts",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^16.35.0",
"console-stamp": "^0.2.9"
}
}

View file

@ -16,7 +16,6 @@
******************************/
// compile typescript on the fly
// eslint-disable-next-line import/no-extraneous-dependencies
require('@babel/register')({
extensions: ['.ts'],
plugins: ['@babel/plugin-proposal-optional-chaining'],

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// compile typescript on the fly
// eslint-disable-next-line import/no-extraneous-dependencies
require('@babel/register')({
extensions: ['.ts'],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator'
],
presets: [
'@babel/typescript',
['@babel/preset-env', { targets: { node: 'current' } }]
]
});
require('./upload-telemetry-data/index.ts');

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
import { Octokit } from '@octokit/rest';
export async function downloadTelemetryTemplate(octokit: Octokit) {
const file = await octokit.repos.getContents({
owner: 'elastic',
repo: 'telemetry',
path: 'config/templates/xpack-phone-home.json',
// @ts-ignore
mediaType: {
format: 'application/vnd.github.VERSION.raw'
}
});
if (Array.isArray(file.data)) {
throw new Error('Expected single response, got array');
}
return JSON.parse(Buffer.from(file.data.content!, 'base64').toString());
}

View file

@ -1,124 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DeepPartial } from 'utility-types';
import {
merge,
omit,
defaultsDeep,
range,
mapValues,
isPlainObject,
flatten
} from 'lodash';
import uuid from 'uuid';
import {
CollectTelemetryParams,
collectDataTelemetry
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../plugins/apm/server/lib/apm_telemetry/collect_data_telemetry';
interface GenerateOptions {
days: number;
instances: number;
variation: {
min: number;
max: number;
};
}
const randomize = (
value: unknown,
instanceVariation: number,
dailyGrowth: number
) => {
if (typeof value === 'boolean') {
return Math.random() > 0.5;
}
if (typeof value === 'number') {
return Math.round(instanceVariation * dailyGrowth * value);
}
return value;
};
const mapValuesDeep = (
obj: Record<string, any>,
iterator: (value: unknown, key: string, obj: Record<string, any>) => unknown
): Record<string, any> =>
mapValues(obj, (val, key) =>
isPlainObject(val) ? mapValuesDeep(val, iterator) : iterator(val, key!, obj)
);
export async function generateSampleDocuments(
options: DeepPartial<GenerateOptions> & {
collectTelemetryParams: CollectTelemetryParams;
}
) {
const { collectTelemetryParams, ...preferredOptions } = options;
const opts: GenerateOptions = defaultsDeep(
{
days: 100,
instances: 50,
variation: {
min: 0.1,
max: 4
}
},
preferredOptions
);
const sample = await collectDataTelemetry(collectTelemetryParams);
console.log('Collected telemetry'); // eslint-disable-line no-console
console.log('\n' + JSON.stringify(sample, null, 2)); // eslint-disable-line no-console
const dateOfScriptExecution = new Date();
return flatten(
range(0, opts.instances).map(instanceNo => {
const instanceId = uuid.v4();
const defaults = {
cluster_uuid: instanceId,
stack_stats: {
kibana: {
versions: {
version: '8.0.0'
}
}
}
};
const instanceVariation =
Math.random() * (opts.variation.max - opts.variation.min) +
opts.variation.min;
return range(0, opts.days).map(dayNo => {
const dailyGrowth = Math.pow(1.005, opts.days - 1 - dayNo);
const timestamp = Date.UTC(
dateOfScriptExecution.getFullYear(),
dateOfScriptExecution.getMonth(),
-dayNo
);
const generated = mapValuesDeep(omit(sample, 'versions'), value =>
randomize(value, instanceVariation, dailyGrowth)
);
return merge({}, defaults, {
timestamp,
stack_stats: {
kibana: {
plugins: {
apm: merge({}, sample, generated)
}
}
}
});
});
})
);
}

View file

@ -1,208 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// This script downloads the telemetry mapping, runs the APM telemetry tasks,
// generates a bunch of randomized data based on the downloaded sample,
// and uploads it to a cluster of your choosing in the same format as it is
// stored in the telemetry cluster. Its purpose is twofold:
// - Easier testing of the telemetry tasks
// - Validate whether we can run the queries we want to on the telemetry data
import fs from 'fs';
import path from 'path';
// @ts-ignore
import { Octokit } from '@octokit/rest';
import { merge, chunk, flatten, pick, identity } from 'lodash';
import axios from 'axios';
import yaml from 'js-yaml';
import { Client } from 'elasticsearch';
import { argv } from 'yargs';
import { promisify } from 'util';
import { Logger } from 'kibana/server';
// @ts-ignore
import consoleStamp from 'console-stamp';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { CollectTelemetryParams } from '../../../../../plugins/apm/server/lib/apm_telemetry/collect_data_telemetry';
import { downloadTelemetryTemplate } from './download-telemetry-template';
import mapping from '../../mappings.json';
import { generateSampleDocuments } from './generate-sample-documents';
consoleStamp(console, '[HH:MM:ss.l]');
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
throw new Error('GITHUB_TOKEN was not provided.');
}
const kibanaConfigDir = path.join(__filename, '../../../../../../../config');
const kibanaDevConfig = path.join(kibanaConfigDir, 'kibana.dev.yml');
const kibanaConfig = path.join(kibanaConfigDir, 'kibana.yml');
const xpackTelemetryIndexName = 'xpack-phone-home';
const loadedKibanaConfig = (yaml.safeLoad(
fs.readFileSync(
fs.existsSync(kibanaDevConfig) ? kibanaDevConfig : kibanaConfig,
'utf8'
)
) || {}) as {};
const cliEsCredentials = pick(
{
'elasticsearch.username': process.env.ELASTICSEARCH_USERNAME,
'elasticsearch.password': process.env.ELASTICSEARCH_PASSWORD,
'elasticsearch.hosts': process.env.ELASTICSEARCH_HOST
},
identity
) as {
'elasticsearch.username': string;
'elasticsearch.password': string;
'elasticsearch.hosts': string;
};
const config = {
'apm_oss.transactionIndices': 'apm-*',
'apm_oss.metricsIndices': 'apm-*',
'apm_oss.errorIndices': 'apm-*',
'apm_oss.spanIndices': 'apm-*',
'apm_oss.onboardingIndices': 'apm-*',
'apm_oss.sourcemapIndices': 'apm-*',
'elasticsearch.hosts': 'http://localhost:9200',
...loadedKibanaConfig,
...cliEsCredentials
};
async function uploadData() {
const octokit = new Octokit({
auth: githubToken
});
const telemetryTemplate = await downloadTelemetryTemplate(octokit);
const kibanaMapping = mapping['apm-telemetry'];
const httpAuth =
config['elasticsearch.username'] && config['elasticsearch.password']
? {
username: config['elasticsearch.username'],
password: config['elasticsearch.password']
}
: null;
const client = new Client({
host: config['elasticsearch.hosts'],
...(httpAuth
? {
httpAuth: `${httpAuth.username}:${httpAuth.password}`
}
: {})
});
if (argv.clear) {
try {
await promisify(client.indices.delete.bind(client))({
index: xpackTelemetryIndexName
});
} catch (err) {
// 404 = index not found, totally okay
if (err.status !== 404) {
throw err;
}
}
}
const axiosInstance = axios.create({
baseURL: config['elasticsearch.hosts'],
...(httpAuth ? { auth: httpAuth } : {})
});
const newTemplate = merge(telemetryTemplate, {
settings: {
index: { mapping: { total_fields: { limit: 10000 } } }
}
});
// override apm mapping instead of merging
newTemplate.mappings.properties.stack_stats.properties.kibana.properties.plugins.properties.apm = kibanaMapping;
await axiosInstance.put(`/_template/xpack-phone-home`, newTemplate);
const sampleDocuments = await generateSampleDocuments({
collectTelemetryParams: {
logger: (console as unknown) as Logger,
indices: {
...config,
apmCustomLinkIndex: '.apm-custom-links',
apmAgentConfigurationIndex: '.apm-agent-configuration'
},
search: body => {
return promisify(client.search.bind(client))({
...body,
requestTimeout: 120000
}) as any;
},
indicesStats: body => {
return promisify(client.indices.stats.bind(client))({
...body,
requestTimeout: 120000
}) as any;
},
transportRequest: (params => {
return axiosInstance[params.method](params.path);
}) as CollectTelemetryParams['transportRequest']
}
});
const chunks = chunk(sampleDocuments, 250);
await chunks.reduce<Promise<any>>((prev, documents) => {
return prev.then(async () => {
const body = flatten(
documents.map(doc => [{ index: { _index: 'xpack-phone-home' } }, doc])
);
return promisify(client.bulk.bind(client))({
body,
refresh: true
}).then((response: any) => {
if (response.errors) {
const firstError = response.items.filter(
(item: any) => item.index.status >= 400
)[0].index.error;
throw new Error(`Failed to upload documents: ${firstError.reason} `);
}
});
});
}, Promise.resolve());
}
uploadData()
.catch(e => {
if ('response' in e) {
if (typeof e.response === 'string') {
// eslint-disable-next-line no-console
console.log(e.response);
} else {
// eslint-disable-next-line no-console
console.log(
JSON.stringify(
e.response,
['status', 'statusText', 'headers', 'data'],
2
)
);
}
} else {
// eslint-disable-next-line no-console
console.log(e);
}
process.exit(1);
})
.then(() => {
// eslint-disable-next-line no-console
console.log('Finished uploading generated telemetry data');
});

View file

@ -2,8 +2,6 @@
exports[`Error AGENT_NAME 1`] = `"java"`;
exports[`Error AGENT_VERSION 1`] = `"agent version"`;
exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Error CONTAINER_ID 1`] = `undefined`;
@ -58,7 +56,7 @@ exports[`Error METRIC_SYSTEM_TOTAL_MEMORY 1`] = `undefined`;
exports[`Error OBSERVER_LISTENING 1`] = `undefined`;
exports[`Error OBSERVER_VERSION_MAJOR 1`] = `8`;
exports[`Error OBSERVER_VERSION_MAJOR 1`] = `undefined`;
exports[`Error PARENT_ID 1`] = `"parentId"`;
@ -70,20 +68,10 @@ exports[`Error SERVICE_ENVIRONMENT 1`] = `undefined`;
exports[`Error SERVICE_FRAMEWORK_NAME 1`] = `undefined`;
exports[`Error SERVICE_FRAMEWORK_VERSION 1`] = `undefined`;
exports[`Error SERVICE_LANGUAGE_NAME 1`] = `"nodejs"`;
exports[`Error SERVICE_LANGUAGE_VERSION 1`] = `"v1337"`;
exports[`Error SERVICE_NAME 1`] = `"service name"`;
exports[`Error SERVICE_NODE_NAME 1`] = `undefined`;
exports[`Error SERVICE_RUNTIME_NAME 1`] = `undefined`;
exports[`Error SERVICE_RUNTIME_VERSION 1`] = `undefined`;
exports[`Error SERVICE_VERSION 1`] = `undefined`;
exports[`Error SPAN_ACTION 1`] = `undefined`;
@ -124,14 +112,10 @@ exports[`Error URL_FULL 1`] = `undefined`;
exports[`Error USER_AGENT_NAME 1`] = `undefined`;
exports[`Error USER_AGENT_ORIGINAL 1`] = `undefined`;
exports[`Error USER_ID 1`] = `undefined`;
exports[`Span AGENT_NAME 1`] = `"java"`;
exports[`Span AGENT_VERSION 1`] = `"agent version"`;
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Span CONTAINER_ID 1`] = `undefined`;
@ -186,7 +170,7 @@ exports[`Span METRIC_SYSTEM_TOTAL_MEMORY 1`] = `undefined`;
exports[`Span OBSERVER_LISTENING 1`] = `undefined`;
exports[`Span OBSERVER_VERSION_MAJOR 1`] = `8`;
exports[`Span OBSERVER_VERSION_MAJOR 1`] = `undefined`;
exports[`Span PARENT_ID 1`] = `"parentId"`;
@ -198,20 +182,10 @@ exports[`Span SERVICE_ENVIRONMENT 1`] = `undefined`;
exports[`Span SERVICE_FRAMEWORK_NAME 1`] = `undefined`;
exports[`Span SERVICE_FRAMEWORK_VERSION 1`] = `undefined`;
exports[`Span SERVICE_LANGUAGE_NAME 1`] = `undefined`;
exports[`Span SERVICE_LANGUAGE_VERSION 1`] = `undefined`;
exports[`Span SERVICE_NAME 1`] = `"service name"`;
exports[`Span SERVICE_NODE_NAME 1`] = `undefined`;
exports[`Span SERVICE_RUNTIME_NAME 1`] = `undefined`;
exports[`Span SERVICE_RUNTIME_VERSION 1`] = `undefined`;
exports[`Span SERVICE_VERSION 1`] = `undefined`;
exports[`Span SPAN_ACTION 1`] = `"my action"`;
@ -252,14 +226,10 @@ exports[`Span URL_FULL 1`] = `undefined`;
exports[`Span USER_AGENT_NAME 1`] = `undefined`;
exports[`Span USER_AGENT_ORIGINAL 1`] = `undefined`;
exports[`Span USER_ID 1`] = `undefined`;
exports[`Transaction AGENT_NAME 1`] = `"java"`;
exports[`Transaction AGENT_VERSION 1`] = `"agent version"`;
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
@ -314,7 +284,7 @@ exports[`Transaction METRIC_SYSTEM_TOTAL_MEMORY 1`] = `undefined`;
exports[`Transaction OBSERVER_LISTENING 1`] = `undefined`;
exports[`Transaction OBSERVER_VERSION_MAJOR 1`] = `8`;
exports[`Transaction OBSERVER_VERSION_MAJOR 1`] = `undefined`;
exports[`Transaction PARENT_ID 1`] = `"parentId"`;
@ -326,20 +296,10 @@ exports[`Transaction SERVICE_ENVIRONMENT 1`] = `undefined`;
exports[`Transaction SERVICE_FRAMEWORK_NAME 1`] = `undefined`;
exports[`Transaction SERVICE_FRAMEWORK_VERSION 1`] = `undefined`;
exports[`Transaction SERVICE_LANGUAGE_NAME 1`] = `"nodejs"`;
exports[`Transaction SERVICE_LANGUAGE_VERSION 1`] = `"v1337"`;
exports[`Transaction SERVICE_NAME 1`] = `"service name"`;
exports[`Transaction SERVICE_NODE_NAME 1`] = `undefined`;
exports[`Transaction SERVICE_RUNTIME_NAME 1`] = `undefined`;
exports[`Transaction SERVICE_RUNTIME_VERSION 1`] = `undefined`;
exports[`Transaction SERVICE_VERSION 1`] = `undefined`;
exports[`Transaction SPAN_ACTION 1`] = `undefined`;
@ -380,6 +340,4 @@ exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`;
exports[`Transaction USER_AGENT_NAME 1`] = `"Other"`;
exports[`Transaction USER_AGENT_ORIGINAL 1`] = `"test original"`;
exports[`Transaction USER_ID 1`] = `"1337"`;

View file

@ -4,40 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AgentName } from '../typings/es_schemas/ui/fields/agent';
/*
* Agent names can be any string. This list only defines the official agents
* that we might want to target specifically eg. linking to their documentation
* & telemetry reporting. Support additional agent types by appending
* definitions in mappings.json (for telemetry), the AgentName type, and the
* AGENT_NAMES array.
* agentNames object.
*/
import { AgentName } from '../typings/es_schemas/ui/fields/agent';
export const AGENT_NAMES: AgentName[] = [
'java',
'js-base',
'rum-js',
'dotnet',
'go',
'java',
'nodejs',
'python',
'ruby'
];
const agentNames: { [agentName in AgentName]: agentName } = {
python: 'python',
java: 'java',
nodejs: 'nodejs',
'js-base': 'js-base',
'rum-js': 'rum-js',
dotnet: 'dotnet',
ruby: 'ruby',
go: 'go'
};
export function isAgentName(agentName: string): agentName is AgentName {
return AGENT_NAMES.includes(agentName as AgentName);
export function isAgentName(agentName: string): boolean {
return Object.values(agentNames).includes(agentName as AgentName);
}
export function isRumAgentName(
agentName: string | undefined
): agentName is 'js-base' | 'rum-js' {
return agentName === 'js-base' || agentName === 'rum-js';
export function isRumAgentName(agentName: string | undefined) {
return (
agentName === agentNames['js-base'] || agentName === agentNames['rum-js']
);
}
export function isJavaAgentName(
agentName: string | undefined
): agentName is 'java' {
return agentName === 'java';
export function isJavaAgentName(agentName: string | undefined) {
return agentName === agentNames.java;
}

View file

@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
// the types have to match the names of the saved object mappings
// in /x-pack/legacy/plugins/apm/mappings.json
// APM Services telemetry
export const APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE =
'apm-services-telemetry';
export const APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID = 'apm-services-telemetry';
// APM indices
export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices';
export const APM_INDICES_SAVED_OBJECT_ID = 'apm-indices';
// APM telemetry
export const APM_TELEMETRY_SAVED_OBJECT_TYPE = 'apm-telemetry';
export const APM_TELEMETRY_SAVED_OBJECT_ID = 'apm-telemetry';

View file

@ -15,10 +15,7 @@ describe('Transaction', () => {
const transaction: AllowUnknownProperties<Transaction> = {
'@timestamp': new Date().toString(),
'@metadata': 'whatever',
observer: {
version: 'whatever',
version_major: 8
},
observer: 'whatever',
agent: {
name: 'java',
version: 'agent version'
@ -66,10 +63,7 @@ describe('Span', () => {
const span: AllowUnknownProperties<Span> = {
'@timestamp': new Date().toString(),
'@metadata': 'whatever',
observer: {
version: 'whatever',
version_major: 8
},
observer: 'whatever',
agent: {
name: 'java',
version: 'agent version'
@ -113,10 +107,7 @@ describe('Span', () => {
describe('Error', () => {
const errorDoc: AllowUnknownProperties<APMError> = {
'@metadata': 'whatever',
observer: {
version: 'whatever',
version_major: 8
},
observer: 'whatever',
agent: {
name: 'java',
version: 'agent version'

View file

@ -4,24 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const AGENT_NAME = 'agent.name';
export const SERVICE_NAME = 'service.name';
export const SERVICE_ENVIRONMENT = 'service.environment';
export const SERVICE_FRAMEWORK_NAME = 'service.framework.name';
export const SERVICE_FRAMEWORK_VERSION = 'service.framework.version';
export const SERVICE_LANGUAGE_NAME = 'service.language.name';
export const SERVICE_LANGUAGE_VERSION = 'service.language.version';
export const SERVICE_RUNTIME_NAME = 'service.runtime.name';
export const SERVICE_RUNTIME_VERSION = 'service.runtime.version';
export const SERVICE_NODE_NAME = 'service.node.name';
export const SERVICE_VERSION = 'service.version';
export const AGENT_NAME = 'agent.name';
export const AGENT_VERSION = 'agent.version';
export const URL_FULL = 'url.full';
export const HTTP_REQUEST_METHOD = 'http.request.method';
export const USER_ID = 'user.id';
export const USER_AGENT_ORIGINAL = 'user_agent.original';
export const USER_AGENT_NAME = 'user_agent.name';
export const DESTINATION_ADDRESS = 'destination.address';

View file

@ -3,11 +3,8 @@
"server": true,
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": [
"xpack",
"apm"
],
"configPath": ["xpack", "apm"],
"ui": false,
"requiredPlugins": ["apm_oss", "data", "home", "licensing"],
"optionalPlugins": ["cloud", "usageCollection", "taskManager"]
"optionalPlugins": ["cloud", "usageCollection"]
}

View file

@ -29,8 +29,7 @@ export const config = {
enabled: schema.boolean({ defaultValue: true }),
transactionGroupBucketSize: schema.number({ defaultValue: 100 }),
maxTraceItems: schema.number({ defaultValue: 1000 })
}),
telemetryCollectionEnabled: schema.boolean({ defaultValue: true })
})
})
};
@ -63,8 +62,7 @@ export function mergeConfigs(
'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems,
'xpack.apm.ui.transactionGroupBucketSize':
apmConfig.ui.transactionGroupBucketSize,
'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern,
'xpack.apm.telemetryCollectionEnabled': apmConfig.telemetryCollectionEnabled
'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern
};
}

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectAttributes } from '../../../../../../../src/core/server';
import { createApmTelementry, storeApmServicesTelemetry } from '../index';
import {
APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE,
APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID
} from '../../../../common/apm_saved_object_constants';
describe('apm_telemetry', () => {
describe('createApmTelementry', () => {
it('should create a ApmTelemetry object with boolean flag and frequency map of the given list of AgentNames', () => {
const apmTelemetry = createApmTelementry([
'go',
'nodejs',
'go',
'js-base'
]);
expect(apmTelemetry.has_any_services).toBe(true);
expect(apmTelemetry.services_per_agent).toMatchObject({
go: 2,
nodejs: 1,
'js-base': 1
});
});
it('should ignore undefined or unknown AgentName values', () => {
const apmTelemetry = createApmTelementry([
'go',
'nodejs',
'go',
'js-base',
'example-platform' as any,
undefined as any
]);
expect(apmTelemetry.services_per_agent).toMatchObject({
go: 2,
nodejs: 1,
'js-base': 1
});
});
});
describe('storeApmServicesTelemetry', () => {
let apmTelemetry: SavedObjectAttributes;
let savedObjectsClient: any;
beforeEach(() => {
apmTelemetry = {
has_any_services: true,
services_per_agent: {
go: 2,
nodejs: 1,
'js-base': 1
}
};
savedObjectsClient = { create: jest.fn() };
});
it('should call savedObjectsClient create with the given ApmTelemetry object', () => {
storeApmServicesTelemetry(savedObjectsClient, apmTelemetry);
expect(savedObjectsClient.create.mock.calls[0][1]).toBe(apmTelemetry);
});
it('should call savedObjectsClient create with the apm-telemetry document type and ID', () => {
storeApmServicesTelemetry(savedObjectsClient, apmTelemetry);
expect(savedObjectsClient.create.mock.calls[0][0]).toBe(
APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE
);
expect(savedObjectsClient.create.mock.calls[0][2].id).toBe(
APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID
);
});
it('should call savedObjectsClient create with overwrite: true', () => {
storeApmServicesTelemetry(savedObjectsClient, apmTelemetry);
expect(savedObjectsClient.create.mock.calls[0][2].overwrite).toBe(true);
});
});
});

View file

@ -1,77 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { merge } from 'lodash';
import { Logger, CallAPIOptions } from 'kibana/server';
import { IndicesStatsParams, Client } from 'elasticsearch';
import {
ESSearchRequest,
ESSearchResponse
} from '../../../../typings/elasticsearch';
import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
import { tasks } from './tasks';
import { APMDataTelemetry } from '../types';
type TelemetryTaskExecutor = (params: {
indices: ApmIndicesConfig;
search<TSearchRequest extends ESSearchRequest>(
params: TSearchRequest
): Promise<ESSearchResponse<unknown, TSearchRequest>>;
indicesStats(
params: IndicesStatsParams,
options?: CallAPIOptions
): ReturnType<Client['indices']['stats']>;
transportRequest: (params: {
path: string;
method: 'get';
}) => Promise<unknown>;
}) => Promise<APMDataTelemetry>;
export interface TelemetryTask {
name: string;
executor: TelemetryTaskExecutor;
}
export type CollectTelemetryParams = Parameters<TelemetryTaskExecutor>[0] & {
logger: Logger;
};
export function collectDataTelemetry({
search,
indices,
logger,
indicesStats,
transportRequest
}: CollectTelemetryParams) {
return tasks.reduce((prev, task) => {
return prev.then(async data => {
logger.debug(`Executing APM telemetry task ${task.name}`);
try {
const time = process.hrtime();
const next = await task.executor({
search,
indices,
indicesStats,
transportRequest
});
const took = process.hrtime(time);
return merge({}, data, next, {
tasks: {
[task.name]: {
took: {
ms: Math.round(took[0] * 1000 + took[1] / 1e6)
}
}
}
});
} catch (err) {
logger.warn(`Failed executing APM telemetry task ${task.name}`);
logger.warn(err);
return data;
}
});
}, Promise.resolve({} as APMDataTelemetry));
}

View file

@ -1,725 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { flatten, merge, sortBy, sum } from 'lodash';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import { AGENT_NAMES } from '../../../../common/agent_name';
import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
AGENT_NAME,
AGENT_VERSION,
ERROR_GROUP_ID,
TRANSACTION_NAME,
PARENT_ID,
SERVICE_FRAMEWORK_NAME,
SERVICE_FRAMEWORK_VERSION,
SERVICE_LANGUAGE_NAME,
SERVICE_LANGUAGE_VERSION,
SERVICE_RUNTIME_NAME,
SERVICE_RUNTIME_VERSION,
USER_AGENT_ORIGINAL
} from '../../../../common/elasticsearch_fieldnames';
import { Span } from '../../../../typings/es_schemas/ui/span';
import { APMError } from '../../../../typings/es_schemas/ui/apm_error';
import { TelemetryTask } from '.';
import { APMTelemetry } from '../types';
const TIME_RANGES = ['1d', 'all'] as const;
type TimeRange = typeof TIME_RANGES[number];
export const tasks: TelemetryTask[] = [
{
name: 'processor_events',
executor: async ({ indices, search }) => {
const indicesByProcessorEvent = {
error: indices['apm_oss.errorIndices'],
metric: indices['apm_oss.metricsIndices'],
span: indices['apm_oss.spanIndices'],
transaction: indices['apm_oss.transactionIndices'],
onboarding: indices['apm_oss.onboardingIndices'],
sourcemap: indices['apm_oss.sourcemapIndices']
};
type ProcessorEvent = keyof typeof indicesByProcessorEvent;
const jobs: Array<{
processorEvent: ProcessorEvent;
timeRange: TimeRange;
}> = flatten(
(Object.keys(
indicesByProcessorEvent
) as ProcessorEvent[]).map(processorEvent =>
TIME_RANGES.map(timeRange => ({ processorEvent, timeRange }))
)
);
const allData = await jobs.reduce((prevJob, current) => {
return prevJob.then(async data => {
const { processorEvent, timeRange } = current;
const response = await search({
index: indicesByProcessorEvent[processorEvent],
body: {
size: 1,
query: {
bool: {
filter: [
{ term: { [PROCESSOR_EVENT]: processorEvent } },
...(timeRange !== 'all'
? [
{
range: {
'@timestamp': {
gte: `now-${timeRange}`
}
}
}
]
: [])
]
}
},
sort: {
'@timestamp': 'asc'
},
_source: ['@timestamp'],
track_total_hits: true
}
});
const event = response.hits.hits[0]?._source as {
'@timestamp': number;
};
return merge({}, data, {
counts: {
[processorEvent]: {
[timeRange]: response.hits.total.value
}
},
...(timeRange === 'all' && event
? {
retainment: {
[processorEvent]: {
ms:
new Date().getTime() -
new Date(event['@timestamp']).getTime()
}
}
}
: {})
});
});
}, Promise.resolve({} as Record<string, { counts: Record<ProcessorEvent, Record<TimeRange, number>> }>));
return allData;
}
},
{
name: 'agent_configuration',
executor: async ({ indices, search }) => {
const agentConfigurationCount = (
await search({
index: indices.apmAgentConfigurationIndex,
body: {
size: 0,
track_total_hits: true
}
})
).hits.total.value;
return {
counts: {
agent_configuration: {
all: agentConfigurationCount
}
}
};
}
},
{
name: 'services',
executor: async ({ indices, search }) => {
const servicesPerAgent = await AGENT_NAMES.reduce(
(prevJob, agentName) => {
return prevJob.then(async data => {
const response = await search({
index: [
indices['apm_oss.errorIndices'],
indices['apm_oss.spanIndices'],
indices['apm_oss.metricsIndices'],
indices['apm_oss.transactionIndices']
],
body: {
size: 0,
query: {
bool: {
filter: [
{
term: {
[AGENT_NAME]: agentName
}
},
{
range: {
'@timestamp': {
gte: 'now-1d'
}
}
}
]
}
},
aggs: {
services: {
cardinality: {
field: SERVICE_NAME
}
}
}
}
});
return {
...data,
[agentName]: response.aggregations?.services.value || 0
};
});
},
Promise.resolve({} as Record<AgentName, number>)
);
return {
has_any_services: sum(Object.values(servicesPerAgent)) > 0,
services_per_agent: servicesPerAgent
};
}
},
{
name: 'versions',
executor: async ({ search, indices }) => {
const response = await search({
index: [
indices['apm_oss.transactionIndices'],
indices['apm_oss.spanIndices'],
indices['apm_oss.errorIndices']
],
terminateAfter: 1,
body: {
query: {
exists: {
field: 'observer.version'
}
},
size: 1,
sort: {
'@timestamp': 'desc'
}
}
});
const hit = response.hits.hits[0]?._source as Pick<
Transaction | Span | APMError,
'observer'
>;
if (!hit || !hit.observer?.version) {
return {};
}
const [major, minor, patch] = hit.observer.version
.split('.')
.map(part => Number(part));
return {
versions: {
apm_server: {
major,
minor,
patch
}
}
};
}
},
{
name: 'groupings',
executor: async ({ search, indices }) => {
const range1d = { range: { '@timestamp': { gte: 'now-1d' } } };
const errorGroupsCount = (
await search({
index: indices['apm_oss.errorIndices'],
body: {
size: 0,
query: {
bool: {
filter: [{ term: { [PROCESSOR_EVENT]: 'error' } }, range1d]
}
},
aggs: {
top_service: {
terms: {
field: SERVICE_NAME,
order: {
error_groups: 'desc'
},
size: 1
},
aggs: {
error_groups: {
cardinality: {
field: ERROR_GROUP_ID
}
}
}
}
}
}
})
).aggregations?.top_service.buckets[0]?.error_groups.value;
const transactionGroupsCount = (
await search({
index: indices['apm_oss.transactionIndices'],
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
range1d
]
}
},
aggs: {
top_service: {
terms: {
field: SERVICE_NAME,
order: {
transaction_groups: 'desc'
},
size: 1
},
aggs: {
transaction_groups: {
cardinality: {
field: TRANSACTION_NAME
}
}
}
}
}
}
})
).aggregations?.top_service.buckets[0]?.transaction_groups.value;
const tracesPerDayCount = (
await search({
index: indices['apm_oss.transactionIndices'],
body: {
query: {
bool: {
filter: [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
range1d
],
must_not: {
exists: { field: PARENT_ID }
}
}
},
track_total_hits: true,
size: 0
}
})
).hits.total.value;
const servicesCount = (
await search({
index: [
indices['apm_oss.transactionIndices'],
indices['apm_oss.errorIndices'],
indices['apm_oss.metricsIndices']
],
body: {
size: 0,
query: {
bool: {
filter: [range1d]
}
},
aggs: {
service_name: {
cardinality: {
field: SERVICE_NAME
}
}
}
}
})
).aggregations?.service_name.value;
return {
counts: {
max_error_groups_per_service: {
'1d': errorGroupsCount || 0
},
max_transaction_groups_per_service: {
'1d': transactionGroupsCount || 0
},
traces: {
'1d': tracesPerDayCount || 0
},
services: {
'1d': servicesCount || 0
}
}
};
}
},
{
name: 'integrations',
executor: async ({ transportRequest }) => {
const apmJobs = ['*-high_mean_response_time'];
const response = (await transportRequest({
method: 'get',
path: `/_ml/anomaly_detectors/${apmJobs.join(',')}`
})) as { data?: { count: number } };
return {
integrations: {
ml: {
all_jobs_count: response.data?.count ?? 0
}
}
};
}
},
{
name: 'agents',
executor: async ({ search, indices }) => {
const size = 3;
const agentData = await AGENT_NAMES.reduce(async (prevJob, agentName) => {
const data = await prevJob;
const response = await search({
index: [
indices['apm_oss.errorIndices'],
indices['apm_oss.metricsIndices'],
indices['apm_oss.transactionIndices']
],
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [AGENT_NAME]: agentName } },
{ range: { '@timestamp': { gte: 'now-1d' } } }
]
}
},
sort: {
'@timestamp': 'desc'
},
aggs: {
[AGENT_VERSION]: {
terms: {
field: AGENT_VERSION,
size
}
},
[SERVICE_FRAMEWORK_NAME]: {
terms: {
field: SERVICE_FRAMEWORK_NAME,
size
},
aggs: {
[SERVICE_FRAMEWORK_VERSION]: {
terms: {
field: SERVICE_FRAMEWORK_VERSION,
size
}
}
}
},
[SERVICE_FRAMEWORK_VERSION]: {
terms: {
field: SERVICE_FRAMEWORK_VERSION,
size
}
},
[SERVICE_LANGUAGE_NAME]: {
terms: {
field: SERVICE_LANGUAGE_NAME,
size
},
aggs: {
[SERVICE_LANGUAGE_VERSION]: {
terms: {
field: SERVICE_LANGUAGE_VERSION,
size
}
}
}
},
[SERVICE_LANGUAGE_VERSION]: {
terms: {
field: SERVICE_LANGUAGE_VERSION,
size
}
},
[SERVICE_RUNTIME_NAME]: {
terms: {
field: SERVICE_RUNTIME_NAME,
size
},
aggs: {
[SERVICE_RUNTIME_VERSION]: {
terms: {
field: SERVICE_RUNTIME_VERSION,
size
}
}
}
},
[SERVICE_RUNTIME_VERSION]: {
terms: {
field: SERVICE_RUNTIME_VERSION,
size
}
}
}
}
});
const { aggregations } = response;
if (!aggregations) {
return data;
}
const toComposite = (
outerKey: string | number,
innerKey: string | number
) => `${outerKey}/${innerKey}`;
return {
...data,
[agentName]: {
agent: {
version: aggregations[AGENT_VERSION].buckets.map(
bucket => bucket.key as string
)
},
service: {
framework: {
name: aggregations[SERVICE_FRAMEWORK_NAME].buckets
.map(bucket => bucket.key as string)
.slice(0, size),
version: aggregations[SERVICE_FRAMEWORK_VERSION].buckets
.map(bucket => bucket.key as string)
.slice(0, size),
composite: sortBy(
flatten(
aggregations[SERVICE_FRAMEWORK_NAME].buckets.map(bucket =>
bucket[SERVICE_FRAMEWORK_VERSION].buckets.map(
versionBucket => ({
doc_count: versionBucket.doc_count,
name: toComposite(bucket.key, versionBucket.key)
})
)
)
),
'doc_count'
)
.reverse()
.slice(0, size)
.map(composite => composite.name)
},
language: {
name: aggregations[SERVICE_LANGUAGE_NAME].buckets
.map(bucket => bucket.key as string)
.slice(0, size),
version: aggregations[SERVICE_LANGUAGE_VERSION].buckets
.map(bucket => bucket.key as string)
.slice(0, size),
composite: sortBy(
flatten(
aggregations[SERVICE_LANGUAGE_NAME].buckets.map(bucket =>
bucket[SERVICE_LANGUAGE_VERSION].buckets.map(
versionBucket => ({
doc_count: versionBucket.doc_count,
name: toComposite(bucket.key, versionBucket.key)
})
)
)
),
'doc_count'
)
.reverse()
.slice(0, size)
.map(composite => composite.name)
},
runtime: {
name: aggregations[SERVICE_RUNTIME_NAME].buckets
.map(bucket => bucket.key as string)
.slice(0, size),
version: aggregations[SERVICE_RUNTIME_VERSION].buckets
.map(bucket => bucket.key as string)
.slice(0, size),
composite: sortBy(
flatten(
aggregations[SERVICE_RUNTIME_NAME].buckets.map(bucket =>
bucket[SERVICE_RUNTIME_VERSION].buckets.map(
versionBucket => ({
doc_count: versionBucket.doc_count,
name: toComposite(bucket.key, versionBucket.key)
})
)
)
),
'doc_count'
)
.reverse()
.slice(0, size)
.map(composite => composite.name)
}
}
}
};
}, Promise.resolve({} as APMTelemetry['agents']));
return {
agents: agentData
};
}
},
{
name: 'indices_stats',
executor: async ({ indicesStats, indices }) => {
const response = await indicesStats({
index: [
indices.apmAgentConfigurationIndex,
indices['apm_oss.errorIndices'],
indices['apm_oss.metricsIndices'],
indices['apm_oss.onboardingIndices'],
indices['apm_oss.sourcemapIndices'],
indices['apm_oss.spanIndices'],
indices['apm_oss.transactionIndices']
]
});
return {
indices: {
shards: {
total: response._shards.total
},
all: {
total: {
docs: {
count: response._all.total.docs.count
},
store: {
size_in_bytes: response._all.total.store.size_in_bytes
}
}
}
}
};
}
},
{
name: 'cardinality',
executor: async ({ search }) => {
const allAgentsCardinalityResponse = await search({
body: {
size: 0,
query: {
bool: {
filter: [{ range: { '@timestamp': { gte: 'now-1d' } } }]
}
},
aggs: {
[TRANSACTION_NAME]: {
cardinality: {
field: TRANSACTION_NAME
}
},
[USER_AGENT_ORIGINAL]: {
cardinality: {
field: USER_AGENT_ORIGINAL
}
}
}
}
});
const rumAgentCardinalityResponse = await search({
body: {
size: 0,
query: {
bool: {
filter: [
{ range: { '@timestamp': { gte: 'now-1d' } } },
{ terms: { [AGENT_NAME]: ['rum-js', 'js-base'] } }
]
}
},
aggs: {
[TRANSACTION_NAME]: {
cardinality: {
field: TRANSACTION_NAME
}
},
[USER_AGENT_ORIGINAL]: {
cardinality: {
field: USER_AGENT_ORIGINAL
}
}
}
}
});
return {
cardinality: {
transaction: {
name: {
all_agents: {
'1d':
allAgentsCardinalityResponse.aggregations?.[TRANSACTION_NAME]
.value
},
rum: {
'1d':
rumAgentCardinalityResponse.aggregations?.[TRANSACTION_NAME]
.value
}
}
},
user_agent: {
original: {
all_agents: {
'1d':
allAgentsCardinalityResponse.aggregations?.[
USER_AGENT_ORIGINAL
].value
},
rum: {
'1d':
rumAgentCardinalityResponse.aggregations?.[
USER_AGENT_ORIGINAL
].value
}
}
}
}
};
}
}
];

View file

@ -3,127 +3,60 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, Logger } from 'src/core/server';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { countBy } from 'lodash';
import { SavedObjectAttributes } from '../../../../../../src/core/server';
import { isAgentName } from '../../../common/agent_name';
import {
TaskManagerStartContract,
TaskManagerSetupContract
} from '../../../../task_manager/server';
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
import {
APM_TELEMETRY_SAVED_OBJECT_ID,
APM_TELEMETRY_SAVED_OBJECT_TYPE
APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE,
APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID
} from '../../../common/apm_saved_object_constants';
import {
collectDataTelemetry,
CollectTelemetryParams
} from './collect_data_telemetry';
import { APMConfig } from '../..';
import { getInternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client';
import { UsageCollectionSetup } from '../../../../../../src/plugins/usage_collection/server';
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client';
const APM_TELEMETRY_TASK_NAME = 'apm-telemetry-task';
export async function createApmTelemetry({
core,
config$,
usageCollector,
taskManager,
logger
}: {
core: CoreSetup;
config$: Observable<APMConfig>;
usageCollector: UsageCollectionSetup;
taskManager: TaskManagerSetupContract;
logger: Logger;
}) {
const savedObjectsClient = await getInternalSavedObjectsClient(core);
const collectAndStore = async () => {
const config = await config$.pipe(take(1)).toPromise();
const esClient = core.elasticsearch.dataClient;
const indices = await getApmIndices({
config,
savedObjectsClient
});
const search = esClient.callAsInternalUser.bind(
esClient,
'search'
) as CollectTelemetryParams['search'];
const indicesStats = esClient.callAsInternalUser.bind(
esClient,
'indices.stats'
) as CollectTelemetryParams['indicesStats'];
const transportRequest = esClient.callAsInternalUser.bind(
esClient,
'transport.request'
) as CollectTelemetryParams['transportRequest'];
const dataTelemetry = await collectDataTelemetry({
search,
indices,
logger,
indicesStats,
transportRequest
});
await savedObjectsClient.create(
APM_TELEMETRY_SAVED_OBJECT_TYPE,
dataTelemetry,
{ id: APM_TELEMETRY_SAVED_OBJECT_TYPE, overwrite: true }
);
export function createApmTelementry(
agentNames: string[] = []
): SavedObjectAttributes {
const validAgentNames = agentNames.filter(isAgentName);
return {
has_any_services: validAgentNames.length > 0,
services_per_agent: countBy(validAgentNames)
};
}
taskManager.registerTaskDefinitions({
[APM_TELEMETRY_TASK_NAME]: {
title: 'Collect APM telemetry',
type: APM_TELEMETRY_TASK_NAME,
createTaskRunner: () => {
return {
run: async () => {
await collectAndStore();
}
};
}
export async function storeApmServicesTelemetry(
savedObjectsClient: InternalSavedObjectsClient,
apmTelemetry: SavedObjectAttributes
) {
return savedObjectsClient.create(
APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE,
apmTelemetry,
{
id: APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID,
overwrite: true
}
});
);
}
const collector = usageCollector.makeUsageCollector({
export function makeApmUsageCollector(
usageCollector: UsageCollectionSetup,
savedObjectsRepository: InternalSavedObjectsClient
) {
const apmUsageCollector = usageCollector.makeUsageCollector({
type: 'apm',
fetch: async () => {
const data = (
await savedObjectsClient.get(
APM_TELEMETRY_SAVED_OBJECT_TYPE,
APM_TELEMETRY_SAVED_OBJECT_ID
)
).attributes;
return data;
try {
const apmTelemetrySavedObject = await savedObjectsRepository.get(
APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE,
APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID
);
return apmTelemetrySavedObject.attributes;
} catch (err) {
return createApmTelementry();
}
},
isReady: () => true
});
usageCollector.registerCollector(collector);
core.getStartServices().then(([coreStart, pluginsStart]) => {
const { taskManager: taskManagerStart } = pluginsStart as {
taskManager: TaskManagerStartContract;
};
taskManagerStart.ensureScheduled({
id: APM_TELEMETRY_TASK_NAME,
taskType: APM_TELEMETRY_TASK_NAME,
schedule: {
interval: '720m'
},
scope: ['apm'],
params: {},
state: {}
});
});
usageCollector.registerCollector(apmUsageCollector);
}

View file

@ -1,118 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DeepPartial } from 'utility-types';
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
export interface TimeframeMap {
'1d': number;
all: number;
}
export type TimeframeMap1d = Pick<TimeframeMap, '1d'>;
export type TimeframeMapAll = Pick<TimeframeMap, 'all'>;
export type APMDataTelemetry = DeepPartial<{
has_any_services: boolean;
services_per_agent: Record<AgentName, number>;
versions: {
apm_server: {
minor: number;
major: number;
patch: number;
};
};
counts: {
transaction: TimeframeMap;
span: TimeframeMap;
error: TimeframeMap;
metric: TimeframeMap;
sourcemap: TimeframeMap;
onboarding: TimeframeMap;
agent_configuration: TimeframeMapAll;
max_transaction_groups_per_service: TimeframeMap;
max_error_groups_per_service: TimeframeMap;
traces: TimeframeMap;
services: TimeframeMap;
};
cardinality: {
user_agent: {
original: {
all_agents: TimeframeMap1d;
rum: TimeframeMap1d;
};
};
transaction: {
name: {
all_agents: TimeframeMap1d;
rum: TimeframeMap1d;
};
};
};
retainment: Record<
'span' | 'transaction' | 'error' | 'metric' | 'sourcemap' | 'onboarding',
{ ms: number }
>;
integrations: {
ml: {
all_jobs_count: number;
};
};
agents: Record<
AgentName,
{
agent: {
version: string[];
};
service: {
framework: {
name: string[];
version: string[];
composite: string[];
};
language: {
name: string[];
version: string[];
composite: string[];
};
runtime: {
name: string[];
version: string[];
composite: string[];
};
};
}
>;
indices: {
shards: {
total: number;
};
all: {
total: {
docs: {
count: number;
};
store: {
size_in_bytes: number;
};
};
};
};
tasks: Record<
| 'processor_events'
| 'agent_configuration'
| 'services'
| 'versions'
| 'groupings'
| 'integrations'
| 'agents'
| 'indices_stats'
| 'cardinality',
{ took: { ms: number } }
>;
}>;
export type APMTelemetry = APMDataTelemetry;

View file

@ -39,19 +39,6 @@ function getMockRequest() {
_debug: false
}
},
__LEGACY: {
server: {
plugins: {
elasticsearch: {
getCluster: jest.fn().mockReturnValue({ callWithInternalUser: {} })
}
},
savedObjects: {
SavedObjectsClient: jest.fn(),
getSavedObjectsRepository: jest.fn()
}
}
},
core: {
elasticsearch: {
dataClient: {

View file

@ -8,9 +8,9 @@ import { Observable, combineLatest, AsyncSubject } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { Server } from 'hapi';
import { once } from 'lodash';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { TaskManagerSetupContract } from '../../task_manager/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
import { makeApmUsageCollector } from './lib/apm_telemetry';
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index';
import { createApmApi } from './routes/create_apm_api';
@ -21,7 +21,6 @@ import { tutorialProvider } from './tutorial';
import { CloudSetup } from '../../cloud/server';
import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client';
import { LicensingPluginSetup } from '../../licensing/public';
import { createApmTelemetry } from './lib/apm_telemetry';
export interface LegacySetup {
server: Server;
@ -48,10 +47,9 @@ export class APMPlugin implements Plugin<APMPluginContract> {
licensing: LicensingPluginSetup;
cloud?: CloudSetup;
usageCollection?: UsageCollectionSetup;
taskManager?: TaskManagerSetupContract;
}
) {
const logger = this.initContext.logger.get();
const logger = this.initContext.logger.get('apm');
const config$ = this.initContext.config.create<APMXPackConfig>();
const mergedConfig$ = combineLatest(plugins.apm_oss.config$, config$).pipe(
map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig))
@ -63,20 +61,6 @@ export class APMPlugin implements Plugin<APMPluginContract> {
const currentConfig = await mergedConfig$.pipe(take(1)).toPromise();
if (
plugins.taskManager &&
plugins.usageCollection &&
currentConfig['xpack.apm.telemetryCollectionEnabled']
) {
createApmTelemetry({
core,
config$: mergedConfig$,
usageCollector: plugins.usageCollection,
taskManager: plugins.taskManager,
logger
});
}
// create agent configuration index without blocking setup lifecycle
createApmAgentConfigurationIndex({
esClient: core.elasticsearch.dataClient,
@ -105,6 +89,18 @@ export class APMPlugin implements Plugin<APMPluginContract> {
})
);
const usageCollection = plugins.usageCollection;
if (usageCollection) {
getInternalSavedObjectsClient(core)
.then(savedObjectsClient => {
makeApmUsageCollector(usageCollection, savedObjectsClient);
})
.catch(error => {
logger.error('Unable to initialize use collection');
logger.error(error.message);
});
}
return {
config$: mergedConfig$,
registerLegacyAPI: once((__LEGACY: LegacySetup) => {
@ -119,7 +115,6 @@ export class APMPlugin implements Plugin<APMPluginContract> {
};
}
public async start() {}
public start() {}
public stop() {}
}

View file

@ -36,7 +36,6 @@ const getCoreMock = () => {
put,
createRouter,
context: {
measure: () => undefined,
config$: new BehaviorSubject({} as APMConfig),
logger: ({
error: jest.fn()

View file

@ -5,6 +5,11 @@
*/
import * as t from 'io-ts';
import { AgentName } from '../../typings/es_schemas/ui/fields/agent';
import {
createApmTelementry,
storeApmServicesTelemetry
} from '../lib/apm_telemetry';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceAgentName } from '../lib/services/get_service_agent_name';
import { getServices } from '../lib/services/get_services';
@ -13,6 +18,7 @@ import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadat
import { createRoute } from './create_route';
import { uiFiltersRt, rangeRt } from './default_api_types';
import { getServiceAnnotations } from '../lib/services/annotations';
import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client';
export const servicesRoute = createRoute(core => ({
path: '/api/apm/services',
@ -23,6 +29,16 @@ export const servicesRoute = createRoute(core => ({
const setup = await setupRequest(context, request);
const services = await getServices(setup);
// Store telemetry data derived from services
const agentNames = services.items.map(
({ agentName }) => agentName as AgentName
);
const apmTelemetry = createApmTelementry(agentNames);
const savedObjectsClient = await getInternalSavedObjectsClient(core);
storeApmServicesTelemetry(savedObjectsClient, apmTelemetry).catch(error => {
context.logger.error(error.message);
});
return services;
}
}));

View file

@ -126,16 +126,6 @@ export interface AggregationOptionsByType {
combine_script: Script;
reduce_script: Script;
};
date_range: {
field: string;
format?: string;
ranges: Array<
| { from: string | number }
| { to: string | number }
| { from: string | number; to: string | number }
>;
keyed?: boolean;
};
}
type AggregationType = keyof AggregationOptionsByType;
@ -146,15 +136,6 @@ type AggregationOptionsMap = Unionize<
}
> & { aggs?: AggregationInputMap };
interface DateRangeBucket {
key: string;
to?: number;
from?: number;
to_as_string?: string;
from_as_string?: string;
doc_count: number;
}
export interface AggregationInputMap {
[key: string]: AggregationOptionsMap;
}
@ -295,11 +276,6 @@ interface AggregationResponsePart<
scripted_metric: {
value: unknown;
};
date_range: {
buckets: TAggregationOptionsMap extends { date_range: { keyed: true } }
? Record<string, DateRangeBucket>
: { buckets: DateRangeBucket[] };
};
}
// Type for debugging purposes. If you see an error in AggregationResponseMap
@ -309,7 +285,7 @@ interface AggregationResponsePart<
// type MissingAggregationResponseTypes = Exclude<
// AggregationType,
// keyof AggregationResponsePart<{}, unknown>
// keyof AggregationResponsePart<{}>
// >;
export type AggregationResponseMap<

View file

@ -15,7 +15,6 @@ import { Service } from './fields/service';
import { IStackframe } from './fields/stackframe';
import { Url } from './fields/url';
import { User } from './fields/user';
import { Observer } from './fields/observer';
interface Processor {
name: 'error';
@ -62,5 +61,4 @@ export interface ErrorRaw extends APMBaseDoc {
service: Service;
url?: Url;
user?: User;
observer?: Observer;
}

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export interface Observer {
version: string;
version_major: number;
}

View file

@ -6,7 +6,6 @@
import { APMBaseDoc } from './apm_base_doc';
import { IStackframe } from './fields/stackframe';
import { Observer } from './fields/observer';
interface Processor {
name: 'transaction';
@ -51,5 +50,4 @@ export interface SpanRaw extends APMBaseDoc {
transaction?: {
id: string;
};
observer?: Observer;
}

View file

@ -15,7 +15,6 @@ import { Service } from './fields/service';
import { Url } from './fields/url';
import { User } from './fields/user';
import { UserAgent } from './fields/user_agent';
import { Observer } from './fields/observer';
interface Processor {
name: 'transaction';
@ -62,5 +61,4 @@ export interface TransactionRaw extends APMBaseDoc {
url?: Url;
user?: User;
user_agent?: UserAgent;
observer?: Observer;
}