[apm-config-loader] refactor config loading (#104197)

Co-authored-by: spalger <spalger@users.noreply.github.com>
This commit is contained in:
Spencer 2021-07-06 15:00:20 -07:00 committed by GitHub
parent b17ad5d316
commit 99b9a2bc5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 224 additions and 96 deletions

View file

@ -17,10 +17,11 @@ import {
import { ApmConfiguration } from './config';
const initialEnv = { ...process.env };
describe('ApmConfiguration', () => {
beforeEach(() => {
// start with an empty env to avoid CI from spoiling snapshots, env is unique for each jest file
process.env = {};
packageMock.raw = {
version: '8.0.0',
build: {
@ -30,7 +31,6 @@ describe('ApmConfiguration', () => {
});
afterEach(() => {
process.env = { ...initialEnv };
resetAllMocks();
});
@ -46,7 +46,7 @@ describe('ApmConfiguration', () => {
it('sets the git revision from `git rev-parse` command in non distribution mode', () => {
gitRevExecMock.mockReturnValue('some-git-rev');
const config = new ApmConfiguration(mockedRootDir, {}, false);
expect(config.getConfig('serviceName').globalLabels.git_rev).toBe('some-git-rev');
expect(config.getConfig('serviceName').globalLabels?.git_rev).toBe('some-git-rev');
});
it('sets the git revision from `pkg.build.sha` in distribution mode', () => {
@ -58,13 +58,13 @@ describe('ApmConfiguration', () => {
},
};
const config = new ApmConfiguration(mockedRootDir, {}, true);
expect(config.getConfig('serviceName').globalLabels.git_rev).toBe('distribution-sha');
expect(config.getConfig('serviceName').globalLabels?.git_rev).toBe('distribution-sha');
});
it('reads the kibana uuid from the uuid file', () => {
readUuidFileMock.mockReturnValue('instance-uuid');
const config = new ApmConfiguration(mockedRootDir, {}, false);
expect(config.getConfig('serviceName').globalLabels.kibana_uuid).toBe('instance-uuid');
expect(config.getConfig('serviceName').globalLabels?.kibana_uuid).toBe('instance-uuid');
});
it('uses the uuid from the kibana config if present', () => {
@ -75,23 +75,51 @@ describe('ApmConfiguration', () => {
},
};
const config = new ApmConfiguration(mockedRootDir, kibanaConfig, false);
expect(config.getConfig('serviceName').globalLabels.kibana_uuid).toBe('uuid-from-config');
expect(config.getConfig('serviceName').globalLabels?.kibana_uuid).toBe('uuid-from-config');
});
it('uses the correct default config depending on the `isDistributable` parameter', () => {
it('overrides metricsInterval, breakdownMetrics, captureHeaders, and captureBody when `isDistributable` is true', () => {
let config = new ApmConfiguration(mockedRootDir, {}, false);
expect(config.getConfig('serviceName')).toEqual(
expect.objectContaining({
breakdownMetrics: true,
})
);
expect(config.getConfig('serviceName')).toMatchInlineSnapshot(`
Object {
"active": false,
"breakdownMetrics": true,
"captureSpanStackTraces": false,
"centralConfig": false,
"environment": "development",
"globalLabels": Object {},
"logUncaughtExceptions": true,
"metricsInterval": "30s",
"secretToken": "ZQHYvrmXEx04ozge8F",
"serverUrl": "https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io",
"serviceName": "serviceName",
"serviceVersion": "8.0.0",
"transactionSampleRate": 1,
}
`);
config = new ApmConfiguration(mockedRootDir, {}, true);
expect(config.getConfig('serviceName')).toEqual(
expect.objectContaining({
breakdownMetrics: false,
})
);
expect(config.getConfig('serviceName')).toMatchInlineSnapshot(`
Object {
"active": false,
"breakdownMetrics": false,
"captureBody": "off",
"captureHeaders": false,
"captureSpanStackTraces": false,
"centralConfig": false,
"environment": "development",
"globalLabels": Object {
"git_rev": "sha",
},
"logUncaughtExceptions": true,
"metricsInterval": "120s",
"secretToken": "ZQHYvrmXEx04ozge8F",
"serverUrl": "https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io",
"serviceName": "serviceName",
"serviceVersion": "8.0.0",
"transactionSampleRate": 1,
}
`);
});
it('loads the configuration from the kibana config file', () => {
@ -119,7 +147,7 @@ describe('ApmConfiguration', () => {
active: true,
serverUrl: 'https://dev-url.co',
};
const config = new ApmConfiguration(mockedRootDir, {}, true);
const config = new ApmConfiguration(mockedRootDir, {}, false);
expect(config.getConfig('serviceName')).toEqual(
expect.objectContaining({
active: true,
@ -128,7 +156,20 @@ describe('ApmConfiguration', () => {
);
});
it('respect the precedence of the dev config', () => {
it('does not load the configuration from the dev config in distributable', () => {
devConfigMock.raw = {
active: true,
serverUrl: 'https://dev-url.co',
};
const config = new ApmConfiguration(mockedRootDir, {}, true);
expect(config.getConfig('serviceName')).toEqual(
expect.objectContaining({
active: false,
})
);
});
it('overwrites the standard config file with the dev config', () => {
const kibanaConfig = {
elastic: {
apm: {
@ -142,7 +183,7 @@ describe('ApmConfiguration', () => {
active: true,
serverUrl: 'https://dev-url.co',
};
const config = new ApmConfiguration(mockedRootDir, kibanaConfig, true);
const config = new ApmConfiguration(mockedRootDir, kibanaConfig, false);
expect(config.getConfig('serviceName')).toEqual(
expect.objectContaining({
active: true,
@ -152,7 +193,7 @@ describe('ApmConfiguration', () => {
);
});
it('correctly sets environment', () => {
it('correctly sets environment by reading env vars', () => {
delete process.env.ELASTIC_APM_ENVIRONMENT;
delete process.env.NODE_ENV;

View file

@ -7,45 +7,47 @@
*/
import { join } from 'path';
import { merge, get } from 'lodash';
import { merge } from 'lodash';
import { execSync } from 'child_process';
// deep import to avoid loading the whole package
import { getDataPath } from '@kbn/utils/target/path';
import { readFileSync } from 'fs';
import { ApmAgentConfig } from './types';
const getDefaultConfig = (isDistributable: boolean): ApmAgentConfig => {
// https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html
// https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html
const DEFAULT_CONFIG: ApmAgentConfig = {
active: false,
environment: 'development',
logUncaughtExceptions: true,
globalLabels: {},
};
return {
active: process.env.ELASTIC_APM_ACTIVE === 'true' || false,
environment: process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV || 'development',
const CENTRALIZED_SERVICE_BASE_CONFIG: ApmAgentConfig = {
serverUrl: 'https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io',
serverUrl: 'https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io',
// The secretToken below is intended to be hardcoded in this file even though
// it makes it public. This is not a security/privacy issue. Normally we'd
// instead disable the need for a secretToken in the APM Server config where
// the data is transmitted to, but due to how it's being hosted, it's easier,
// for now, to simply leave it in.
secretToken: 'ZQHYvrmXEx04ozge8F',
// The secretToken below is intended to be hardcoded in this file even though
// it makes it public. This is not a security/privacy issue. Normally we'd
// instead disable the need for a secretToken in the APM Server config where
// the data is transmitted to, but due to how it's being hosted, it's easier,
// for now, to simply leave it in.
secretToken: 'ZQHYvrmXEx04ozge8F',
centralConfig: false,
metricsInterval: '30s',
captureSpanStackTraces: false,
transactionSampleRate: 1.0,
breakdownMetrics: true,
};
logUncaughtExceptions: true,
globalLabels: {},
centralConfig: false,
metricsInterval: isDistributable ? '120s' : '30s',
captureSpanStackTraces: false,
transactionSampleRate: process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE
? parseFloat(process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE)
: 1.0,
// Can be performance intensive, disabling by default
breakdownMetrics: isDistributable ? false : true,
};
const CENTRALIZED_SERVICE_DIST_CONFIG: ApmAgentConfig = {
metricsInterval: '120s',
captureBody: 'off',
captureHeaders: false,
breakdownMetrics: false,
};
export class ApmConfiguration {
private baseConfig?: any;
private baseConfig?: ApmAgentConfig;
private kibanaVersion: string;
private pkgBuild: Record<string, any>;
@ -69,52 +71,77 @@ export class ApmConfiguration {
private getBaseConfig() {
if (!this.baseConfig) {
const apmConfig = merge(
getDefaultConfig(this.isDistributable),
this.baseConfig = merge(
{
serviceVersion: this.kibanaVersion,
},
DEFAULT_CONFIG,
this.getUuidConfig(),
this.getGitConfig(),
this.getCiConfig(),
this.getConfigFromKibanaConfig(),
this.getDevConfig(),
this.getDistConfig(),
this.getCIConfig()
this.getConfigFromEnv()
);
const rev = this.getGitRev();
if (rev !== null) {
apmConfig.globalLabels.git_rev = rev;
}
/**
* When the user doesn't override the serverUrl we define our central APM service
* as the serverUrl along with a few other overrides to prevent potentially
* sensitive data from being sent to this service.
*/
const centralizedConfig = this.isDistributable
? merge({}, CENTRALIZED_SERVICE_BASE_CONFIG, CENTRALIZED_SERVICE_DIST_CONFIG)
: CENTRALIZED_SERVICE_BASE_CONFIG;
const uuid = this.getKibanaUuid();
if (uuid) {
apmConfig.globalLabels.kibana_uuid = uuid;
if (
!this.baseConfig?.serverUrl ||
this.baseConfig.serverUrl === centralizedConfig.serverUrl
) {
this.baseConfig = merge(this.baseConfig, centralizedConfig);
}
apmConfig.serviceVersion = this.kibanaVersion;
this.baseConfig = apmConfig;
}
return this.baseConfig;
}
private getConfigFromKibanaConfig(): ApmAgentConfig {
return get(this.rawKibanaConfig, 'elastic.apm', {});
}
/**
* Override some config values when specific environment variables are used
*/
private getConfigFromEnv(): ApmAgentConfig {
const config: ApmAgentConfig = {};
private getKibanaUuid() {
// try to access the `server.uuid` value from the config file first.
// if not manually defined, we will then read the value from the `{DATA_FOLDER}/uuid` file.
// note that as the file is created by the platform AFTER apm init, the file
// will not be present at first startup, but there is nothing we can really do about that.
if (get(this.rawKibanaConfig, 'server.uuid')) {
return this.rawKibanaConfig.server.uuid;
if (process.env.ELASTIC_APM_ACTIVE === 'true') {
config.active = true;
}
const dataPath: string = get(this.rawKibanaConfig, 'path.data') || getDataPath();
try {
const filename = join(dataPath, 'uuid');
return readFileSync(filename, 'utf-8');
} catch (e) {} // eslint-disable-line no-empty
if (process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV) {
config.environment = process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV;
}
if (process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE) {
config.transactionSampleRate = parseFloat(process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE);
}
return config;
}
/**
* Get the elastic.apm configuration from the --config file, supersedes the
* default config.
*/
private getConfigFromKibanaConfig(): ApmAgentConfig {
return this.rawKibanaConfig?.elastic?.apm ?? {};
}
/**
* Get the configuration from the apm.dev.js file, supersedes config
* from the --config file, disabled when running the distributable
*/
private getDevConfig(): ApmAgentConfig {
if (this.isDistributable) {
return {};
}
try {
const apmDevConfigPath = join(this.rootDir, 'config', 'apm.dev.js');
return require(apmDevConfigPath);
@ -123,20 +150,51 @@ export class ApmConfiguration {
}
}
/** Config keys that cannot be overridden in production builds */
private getDistConfig(): ApmAgentConfig {
if (!this.isDistributable) {
return {};
/**
* Determine the Kibana UUID, initialized the value of `globalLabels.kibana_uuid`
* when the UUID can be determined.
*/
private getUuidConfig(): ApmAgentConfig {
// try to access the `server.uuid` value from the config file first.
// if not manually defined, we will then read the value from the `{DATA_FOLDER}/uuid` file.
// note that as the file is created by the platform AFTER apm init, the file
// will not be present at first startup, but there is nothing we can really do about that.
const uuidFromConfig = this.rawKibanaConfig?.server?.uuid;
if (uuidFromConfig) {
return {
globalLabels: {
kibana_uuid: uuidFromConfig,
},
};
}
return {
// Headers & body may contain sensitive info
captureHeaders: false,
captureBody: 'off',
};
const dataPath: string = this.rawKibanaConfig?.path?.data || getDataPath();
try {
const filename = join(dataPath, 'uuid');
const uuid = readFileSync(filename, 'utf-8');
if (!uuid) {
return {};
}
return {
globalLabels: {
kibana_uuid: uuid,
},
};
} catch (e) {
if (e.code === 'ENOENT') {
return {};
}
throw e;
}
}
private getCIConfig(): ApmAgentConfig {
/**
* When running Kibana with ELASTIC_APM_ENVIRONMENT=ci we attempt to grab
* some environment variables we populate in CI related to the build under test
*/
private getCiConfig(): ApmAgentConfig {
if (process.env.ELASTIC_APM_ENVIRONMENT !== 'ci') {
return {};
}
@ -152,17 +210,30 @@ export class ApmConfiguration {
};
}
private getGitRev() {
/**
* When running from the distributable pull the build sha from the package.json
* file. Otherwise attempt to read the current HEAD sha using `git`.
*/
private getGitConfig() {
if (this.isDistributable) {
return this.pkgBuild.sha;
return {
globalLabels: {
git_rev: this.pkgBuild.sha,
},
};
}
try {
return execSync('git rev-parse --short HEAD', {
encoding: 'utf-8' as BufferEncoding,
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch (e) {
return null;
return {
globalLabels: {
git_rev: execSync('git rev-parse --short HEAD', {
encoding: 'utf-8' as BufferEncoding,
stdio: ['ignore', 'pipe', 'ignore'],
}).trim(),
},
};
} catch {
return {};
}
}
}

View file

@ -10,4 +10,20 @@
// but it's not exported, and using ts tricks to retrieve the type via Parameters<ApmAgent['start']>[0]
// causes errors in the generated .d.ts file because of esModuleInterop and the fact that the apm module
// is just exporting an instance of the `ApmAgent` type.
export type ApmAgentConfig = Record<string, any>;
export interface ApmAgentConfig {
active?: boolean;
environment?: string;
serviceName?: string;
serviceVersion?: string;
serverUrl?: string;
secretToken?: string;
logUncaughtExceptions?: boolean;
globalLabels?: Record<string, string | boolean>;
centralConfig?: boolean;
metricsInterval?: string;
captureSpanStackTraces?: boolean;
transactionSampleRate?: number;
breakdownMetrics?: boolean;
captureHeaders?: boolean;
captureBody?: 'off' | 'all' | 'errors' | 'transactions';
}