mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[apm-config-loader] refactor config loading (#104197)
Co-authored-by: spalger <spalger@users.noreply.github.com>
This commit is contained in:
parent
b17ad5d316
commit
99b9a2bc5e
3 changed files with 224 additions and 96 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue