import { join } from 'path' ; import { merge } from 'lodash' ; import { execSync } from 'child_process' ; import { getDataPath } from '@kbn/utils' ; import { readFileSync } from 'fs' ; import type { AgentConfigOptions } from 'elastic-apm-node' ; import type { AgentConfigOptions as RUMAgentConfigOptions } from '@elastic/apm-rum' ; const DEFAULT_CONFIG = { active : true , contextPropagationOnly : true , environment : 'development' , logUncaughtExceptions : true , globalLabels : { } , } ; const CENTRALIZED_SERVICE_BASE_CONFIG = { serverUrl : 'https://kibana-cloud-apm.apm.us-east-1.aws.found.io' , secretToken : 'JpBCcOQxN81D5yucs2' , breakdownMetrics : true , captureSpanStackTraces : false , centralConfig : false , metricsInterval : '30s' , propagateTracestate : true , transactionSampleRate : 1.0 , } ; const CENTRALIZED_SERVICE_DIST_CONFIG = { breakdownMetrics : false , captureBody : 'off' , captureHeaders : false , metricsInterval : '120s' , transactionSampleRate : 0.1 , } ; export class ApmConfiguration { private baseConfig ? : AgentConfigOptions ; private kibanaVersion : string ; private pkgBuild : Record < string , any > ; constructor ( private readonly rootDir , private readonly rawKibanaConfig , private readonly isDistributable ) { const { version , build } = require ( join ( this . rootDir , 'package.json' ) ) ; this . kibanaVersion = version ; this . pkgBuild = build ; } public getConfig ( serviceName ) { return { ... this . getBaseConfig ( ) , serviceName , } ; } private getBaseConfig ( ) { if ( ! this . baseConfig ) { const configFromSources = this . getConfigFromAllSources ( ) ; this . baseConfig = merge ( { serviceVersion : this . kibanaVersion , } , DEFAULT_CONFIG , this . getUuidConfig ( ) , this . getGitConfig ( ) , this . getCiConfig ( ) , configFromSources ) ; /**
             * 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 ; if ( ! this . baseConfig ?. serverUrl || this . baseConfig . serverUrl === centralizedConfig . serverUrl ) { this . baseConfig = merge ( this . baseConfig , centralizedConfig ) ; } } return this . baseConfig ; } /**
     * Override some config values when specific environment variables are used
     */ private getConfigFromEnv ( ) { const config = { } ; if ( process . env . ELASTIC_APM_ACTIVE === 'true' ) { config . active = true ; } if ( process . env . ELASTIC_APM_CONTEXT_PROPAGATION_ONLY === 'true' ) { config . contextPropagationOnly = true ; } else if ( process . env . ELASTIC_APM_CONTEXT_PROPAGATION_ONLY === 'false' ) { config . contextPropagationOnly = false ; } 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 ) ; } if ( process . env . ELASTIC_APM_SERVER_URL ) { config . serverUrl = process . env . ELASTIC_APM_SERVER_URL ; } if ( process . env . ELASTIC_APM_GLOBAL_LABELS ) { config . globalLabels = Object . fromEntries ( process . env . ELASTIC_APM_GLOBAL_LABELS . split ( ',' ) . map ( ( p ) => { const [ key , ... val ] = p . split ( '=' ) ; return [ key , val . join ( '=' ) ] ; } ) ) ; } return config ; } /**
     * Get the elastic.apm configuration from the --config file, supersedes the
     * default config.
     */ private getConfigFromKibanaConfig ( ) { 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 ( ) { if ( this . isDistributable ) { return { } ; } try { const apmDevConfigPath = join ( this . rootDir , 'config' , 'apm.dev.js' ) ; return require ( apmDevConfigPath ) ; } catch ( e ) { return { } ; } } /**
     * Determine the Kibana UUID, initialized the value of `globalLabels.kibana_uuid`
     * when the UUID can be determined.
     */ private getUuidConfig ( ) { const uuidFromConfig = this . rawKibanaConfig ?. server ?. uuid ; if ( uuidFromConfig ) { return { globalLabels : { kibana_uuid : uuidFromConfig , } , } ; } const dataPath = 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 ; } } /**
     * 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 ( ) { if ( process . env . ELASTIC_APM_ENVIRONMENT !== 'ci' ) { return { } ; } return { globalLabels : { branch : process . env . GIT_BRANCH || '' , targetBranch : process . env . GITHUB_PR_TARGET_BRANCH || '' , ciBuildNumber : process . env . BUILDKITE_BUILD_NUMBER || '' , ciBuildId : process . env . BUILDKITE_BUILD_ID || '' , ciBuildJobId : process . env . BUILDKITE_JOB_ID || '' , isPr : process . env . BUILDKITE_PULL_REQUEST ? true : false , prId : process . env . BUILDKITE_PULL_REQUEST || '' , } , } ; } /**
     * 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 { globalLabels : { git_rev : this . pkgBuild . sha , } , } ; } try { return { globalLabels : { git_rev : execSync ( 'git rev-parse --short HEAD' , { encoding : 'utf-8' as BufferEncoding , stdio : [ 'ignore' , 'pipe' , 'ignore' ] , } ) . trim ( ) , } , } ; } catch { return { } ; } } /**
     * Reads APM configuration from different sources and merges them together.
     */ private getConfigFromAllSources ( ) { const config = merge ( { } , this . getConfigFromKibanaConfig ( ) , this . getDevConfig ( ) , this . getConfigFromEnv ( ) ) ; if ( config . active === false && config . contextPropagationOnly !== false ) { throw new Error ( 'APM is disabled, but context propagation is enabled. Please disable context propagation with contextPropagationOnly:false' ) ; } if ( config . active === true ) { config . contextPropagationOnly = config . contextPropagationOnly ?? false ; } return config ; } }