@deliciousmonster/datadog-agent-binary 7.82.1-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/resources.js ADDED
@@ -0,0 +1,288 @@
1
+ // handleApplication(scope) is the only path that starts an agent, reachable only when a component's own
2
+ // config.yaml carries `pluginModule` beside `jsResource`. This is also the only file Harper compiles, so `spawn` and the compartment globals are read here and passed down: a helper importing them itself may get the unconstrained ones.
3
+
4
+ import { spawn } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+ import { basename } from "node:path";
7
+ import { threadId } from "node:worker_threads";
8
+
9
+ import { PACKAGE_NAME, resolveBinary } from "./runtime/binary.js";
10
+ import { prepareRuntime as prepare } from "./runtime/config.js";
11
+ import {
12
+ debugVarsUrl,
13
+ readDeliverySignal as readSignal,
14
+ } from "./runtime/delivery.js";
15
+ import { untraceAgentProbes } from "./runtime/probe.js";
16
+ import { supervisorFor, unstarted } from "./runtime/supervisor.js";
17
+ import {
18
+ currentVerdict,
19
+ expvarUrl,
20
+ receiverInfoUrl,
21
+ verifyLaunch,
22
+ } from "./runtime/verify.js";
23
+
24
+ /** Harper seeds every component compartment with `logger` and `Resource`; stubs keep the module importable in tests. */
25
+ const host = typeof logger === "undefined" ? console : logger;
26
+ const ResourceBase = typeof Resource === "undefined" ? class {} : Resource;
27
+
28
+ // Every method on Harper's Logger is declared optional. Normalised once here rather than defended per call,
29
+ // because the guard calls ctx.log.info and ctx.log.warn unguarded after it has committed an agent's lock.
30
+ const channel =
31
+ (...names) =>
32
+ (message) =>
33
+ (
34
+ names
35
+ .map((name) => host[name])
36
+ .find((write) => typeof write === "function") ?? console.log
37
+ ).call(host, message);
38
+ const log = {
39
+ info: channel("info", "warn"),
40
+ warn: channel("warn", "info"),
41
+ error: channel("error", "warn"),
42
+ };
43
+
44
+ /** Both agents read these variables, so an unparseable value must not be quietly reinterpreted. */
45
+ function resolvePort(name, fallback) {
46
+ const raw = process.env[name];
47
+ if (!raw) return fallback;
48
+ const trimmed = raw.trim();
49
+ if (trimmed === "0") return 0; // upstream's spelling for "serve no endpoint here"
50
+ // Whole-string, because parseInt reads "8126tcp" as 8126 and hands back a port nobody wrote.
51
+ const parsed = /^\d{1,5}$/.test(trimmed) ? Number(trimmed) : Number.NaN;
52
+ if (parsed >= 1 && parsed <= 65535) return parsed;
53
+ log.warn(
54
+ `Datadog supervisor: ${name}="${raw}" is not a port in 1-65535. Using ${fallback}.`
55
+ );
56
+ return fallback;
57
+ }
58
+
59
+ // Read once per module instance, because every worker thread renders the config and probes the endpoints
60
+ // from the same three numbers and a second reading could disagree with the first.
61
+ const ports = {
62
+ receiver: resolvePort("DD_APM_RECEIVER_PORT", 8126),
63
+ expvar: resolvePort("DD_EXPVAR_PORT", 5000),
64
+ debug: resolvePort("DD_APM_DEBUG_PORT", 5012),
65
+ };
66
+
67
+ // Every URL this module polls. probe.js already suppresses these at the call site; this is the public half,
68
+ // and it only holds until some other caller reconfigures the same plugins.
69
+ const PROBE_URLS = [
70
+ receiverInfoUrl(ports.receiver),
71
+ expvarUrl(ports.expvar),
72
+ debugVarsUrl(ports.debug),
73
+ ];
74
+
75
+ // Resolved rather than imported: dd-trace belongs to the host application, and this package does not ship it.
76
+ let tracer;
77
+ try {
78
+ tracer = createRequire(import.meta.url)("dd-trace");
79
+ } catch {
80
+ // No tracer in this process, so there is nothing to keep the probes out of.
81
+ }
82
+ if (tracer) {
83
+ try {
84
+ untraceAgentProbes(tracer, PROBE_URLS);
85
+ } catch (error) {
86
+ // A shape untraceAgentProbes did not expect from tracer.use(): unlike a missing require, this leaves
87
+ // the probes untraced, so it gets its own log line rather than sharing the silent path above.
88
+ log.error(
89
+ `Datadog supervisor: found dd-trace but could not configure it to ignore the agent probes: ${error.stack ?? error.message}. Probe requests may now appear as spans in APM.`
90
+ );
91
+ }
92
+ }
93
+
94
+ // `name` is Harper's spawn name and the PID-lock filename, stated once: a second spelling is a second lock
95
+ // and a second agent per node. The trace-agent comes first because it owns the socket dd-trace is dialing.
96
+ const AGENTS = [
97
+ {
98
+ kind: "trace",
99
+ name: "datadog-trace-agent",
100
+ title: "trace-agent",
101
+ shipsAs: "trace-agent",
102
+ // The trace-agent's `-c` takes the config FILE. Its own help text says directory and is wrong.
103
+ args: (paths) => ["run", "-c", paths.configFile],
104
+ exitHint: `An immediate non-zero exit from the trace-agent usually means something else already holds 127.0.0.1:${ports.receiver}.`,
105
+ },
106
+ {
107
+ kind: "core",
108
+ name: "datadog-agent",
109
+ title: "core agent",
110
+ shipsAs: "datadog-agent",
111
+ args: (paths) => ["run", "-c", paths.runtimeDir],
112
+ },
113
+ ];
114
+
115
+ // The root-config entry Harper needs before it calls the plugin at all. The key is this directory's name,
116
+ // because a root entry resolves to <componentsRoot>/<key>.
117
+ const CONFIG_ENTRY = `${basename(import.meta.dirname)}: { package: "${PACKAGE_NAME}" }`;
118
+
119
+ /** The runtime tree and the config files for this node, rendered against the ports this instance resolved. */
120
+ export const prepareRuntime = () =>
121
+ prepare(import.meta.dirname, { ports, log });
122
+
123
+ /** The trace-agent's delivery counters, off the debug port this instance rendered into datadog.yaml. */
124
+ export const readDeliverySignal = (port = ports.debug) => readSignal(port);
125
+
126
+ /** Never the value itself, so the status endpoint cannot become a second place the key leaks. */
127
+ const apiKeyStatus = () => (process.env.DD_API_KEY ? "set" : "MISSING");
128
+
129
+ /** Per worker thread, set by handleApplication; a request that beats it, or a thread that never ran it, reads NOT_STARTED. */
130
+ let supervisor;
131
+
132
+ /** The fields every status shape starts from, so NOT_STARTED and startAgents's own status object cannot drift apart. */
133
+ const baseStatus = () => ({
134
+ receiverPort: ports.receiver,
135
+ apiKey: apiKeyStatus(),
136
+ processes: [],
137
+ });
138
+
139
+ const NOT_STARTED = {
140
+ ...baseStatus(),
141
+ detail:
142
+ `nothing has started on this thread. Check first that the node's harper-config.yaml carries ` +
143
+ `\`${CONFIG_ENTRY}\`: Harper calls handleApplication only for a component the root config names, and ` +
144
+ `a directory it loaded by scanning componentsRoot never reaches it. Otherwise this thread has not ` +
145
+ `run startup yet, or it ran under a deploy validation load, which starts nothing`,
146
+ };
147
+
148
+ // Never rejects: a throw out of handleApplication plants an ErrorResource at the component's root path,
149
+ // which is worse than running without telemetry and saying so.
150
+ async function startAgents(scope) {
151
+ const supervisor = supervisorFor(scope, { log, spawn });
152
+ const status = {
153
+ supervision: supervisor.kind,
154
+ ...baseStatus(),
155
+ };
156
+ try {
157
+ if (!process.env.DD_API_KEY) {
158
+ // Measured on 7.82.1 rather than inferred from one shared config: the two agents fail differently.
159
+ log.warn(
160
+ "Datadog supervisor: DD_API_KEY is not set. The core agent starts and collects, and the intake " +
161
+ "refuses every payload it sends with a 403. The trace-agent does not start at all: it exits " +
162
+ 'immediately with "you must specify an API Key", so nothing binds the receiver, the supervisor ' +
163
+ "restarts it until it gives up, and dd-trace has nowhere to send spans."
164
+ );
165
+ }
166
+
167
+ const runtime = prepareRuntime();
168
+ Object.assign(status, {
169
+ runtimeDir: runtime.paths.runtimeDir,
170
+ configFile: runtime.paths.configFile,
171
+ coreChecks: runtime.coreChecks,
172
+ });
173
+
174
+ // Resolved up front so the fingerprint can never describe a different binary from the one spawned.
175
+ const failures = [];
176
+ const binaries = await Promise.all(
177
+ AGENTS.map((agent, index) =>
178
+ resolveBinary(agent).catch((error) => {
179
+ failures[index] = error.message;
180
+ log.error(
181
+ `Datadog supervisor: could not resolve the ${agent.title} binary: ${error.message}`
182
+ );
183
+ return "";
184
+ })
185
+ )
186
+ );
187
+
188
+ // The credentials ride in the inherited environment, invisible to the config contents, so a rotated
189
+ // key must be folded in here or a thread joins the agent still posting under the old one.
190
+ const fingerprintParts = [
191
+ ...Object.values(runtime.configFiles),
192
+ process.env.DD_API_KEY ?? "",
193
+ process.env.DD_SITE ?? "",
194
+ process.env.DD_ENV ?? "",
195
+ ...binaries,
196
+ ];
197
+
198
+ const verifyContext = { paths: runtime.paths, ports };
199
+ const declared = AGENTS.map((agent, index) => ({
200
+ ...agent,
201
+ command: binaries[index],
202
+ args: agent.args(runtime.paths),
203
+ verify: (state) => verifyLaunch(agent, state, verifyContext),
204
+ }));
205
+
206
+ // Reported here rather than inside a supervisor, so the two of them cannot describe the same
207
+ // unresolvable binary in different words.
208
+ const startable = declared.filter((agent) => agent.command);
209
+ const started = startable.length
210
+ ? await supervisor.start(startable, {
211
+ runtime,
212
+ configFiles: runtime.configFiles,
213
+ fingerprintParts,
214
+ })
215
+ : { processes: [], report: [] };
216
+
217
+ const states = new Map(
218
+ startable.map((agent, index) => [agent.name, started.processes[index]])
219
+ );
220
+ status.processes = declared.map(
221
+ (agent, index) =>
222
+ states.get(agent.name) ?? unstarted(agent, failures[index])
223
+ );
224
+ if (started.reaper) status.reaper = started.reaper;
225
+ if (started.report?.length) status.supervisionReport = started.report;
226
+ } catch (error) {
227
+ status.error = error.message;
228
+ log.error(
229
+ `Datadog supervisor: startup failed: ${error.stack ?? error.message}`
230
+ );
231
+ }
232
+ return status;
233
+ }
234
+
235
+ // 60s because handleApplication runs behind scope.ready and waitForDeployCompletion, then behind a per-plugin
236
+ // lock whose own wait is Harper's plugin timeout plus 5s: 35s at the 30s default. A shorter window libels a slow node.
237
+ const START_DEADLINE_MS = 60_000;
238
+
239
+ // The one failure this module cannot see from inside: Harper imports it for its resources and never calls
240
+ // the plugin, which is what an auto-scanned component directory gets. Module evaluation is the only vantage point left.
241
+ const startDeadline = setTimeout(() => {
242
+ log.error(
243
+ `Datadog supervisor: Harper has not called handleApplication ${START_DEADLINE_MS / 1000}s after this ` +
244
+ `module loaded, so no agent started and nothing on this node is supervising one. The likeliest ` +
245
+ `cause is a component Harper loaded by scanning componentsRoot: it calls the plugin only for a ` +
246
+ `component the root harper-config.yaml names, and the module it imports for a scanned directory ` +
247
+ `is discarded.`
248
+ );
249
+ log.error(
250
+ `Datadog supervisor: add this to the node's harper-config.yaml (the file settings_path names in ` +
251
+ `~/.harperdb/hdb_boot_properties.file), keyed by this directory's name, then restart Harper: ${CONFIG_ENTRY}`
252
+ );
253
+ }, START_DEADLINE_MS);
254
+ // A diagnostic must not be the reason a worker thread stays up.
255
+ startDeadline.unref?.();
256
+
257
+ /** Harper's plugin entry, once per worker thread, and the only path that starts anything. */
258
+ export function handleApplication(scope) {
259
+ // Being called at all is what the deadline above waits for; a validation load counts, since Harper
260
+ // reached the plugin either way.
261
+ clearTimeout(startDeadline);
262
+ // A deploy pre-flight loads the component against a live node just to validate it; starting agents there
263
+ // re-enters the sweep and spawn path on every `harper deploy`.
264
+ if (scope?.isTransientValidation) return;
265
+ // The single-start guarantee: a second call joins the first promise rather than starting again.
266
+ supervisor ??= startAgents(scope);
267
+ }
268
+
269
+ /** GET /DatadogStatus/, the plugin's one REST resource, reports what startup did. Everything it reports fails silently by default, which is why it gets an endpoint. */
270
+ export class DatadogStatus extends ResourceBase {
271
+ static async get() {
272
+ // The counters belong to the node's trace-agent, not to this thread, so they are read whether or not
273
+ // this thread is the one that started it.
274
+ const [status, delivery] = await Promise.all([
275
+ supervisor ?? NOT_STARTED,
276
+ readDeliverySignal(),
277
+ ]);
278
+ return {
279
+ ...status,
280
+ // Read here rather than copied at boot: a verdict the supervisor took before a restart describes
281
+ // a process this node no longer runs.
282
+ processes: status.processes.map(currentVerdict),
283
+ // Which thread answered; every field above it is per-thread state.
284
+ threadId,
285
+ delivery,
286
+ };
287
+ }
288
+ }
@@ -0,0 +1,60 @@
1
+ // One reading per way an agent can fail to run. "failed to execute" and a zero exit are the two that
2
+ // mislead most, so each gets a sentence naming what actually happened rather than one shared line.
3
+
4
+ import { constants } from "node:os";
5
+
6
+ // Signals a supervisor sends on the way down. Anything else reaching a child is a crash or an OOM kill,
7
+ // and a Go agent that never installed a handler exits with no code at all either way.
8
+ const SHUTDOWN_SIGNALS = new Set(["SIGTERM", "SIGINT", "SIGHUP"]);
9
+
10
+ const SPAWN_FAILURES = {
11
+ // X_OK passes for a binary built for another architecture, so this is the one cause no preflight sees.
12
+ ENOEXEC: (path) =>
13
+ `${path} is not executable code for this machine (ENOEXEC). A platform ` +
14
+ `package filled from another architecture produces exactly this; check with \`file ${path}\`.`,
15
+ EACCES: (path) =>
16
+ `${path} is not executable by this user (EACCES). Check the file mode, ` +
17
+ `then every directory on the path to it, then whether the volume is mounted noexec.`,
18
+ ENOENT: (path) =>
19
+ `${path} does not exist (ENOENT). The platform package resolved a path ` +
20
+ `and nothing is at it, so the package installed without its binary.`,
21
+ };
22
+
23
+ /** Why a spawn was refused, named. Falls back to the thrown message, which is what Harper's own refusals carry. */
24
+ export function describeSpawnFailure(error, binaryPath) {
25
+ const code =
26
+ error instanceof Error && typeof error.code === "string"
27
+ ? error.code
28
+ : undefined;
29
+ const known = code === undefined ? undefined : SPAWN_FAILURES[code];
30
+ if (known) return known(binaryPath);
31
+ return error instanceof Error ? error.message : String(error);
32
+ }
33
+
34
+ /**
35
+ * How a child ended, in the terms that separate a shutdown from a kill.
36
+ * A signalled process reports code `null`, which reads as a clean stop everywhere `code || 0` is written.
37
+ */
38
+ export function describeExit(code, signal) {
39
+ if (signal) {
40
+ if (SHUTDOWN_SIGNALS.has(signal)) {
41
+ return {
42
+ killed: false,
43
+ detail: `terminated by ${signal}`,
44
+ exitCode: 128 + (constants.signals[signal] ?? 0),
45
+ };
46
+ }
47
+ return {
48
+ killed: true,
49
+ detail: `killed by ${signal}, which is a crash or an OOM kill rather than a shutdown`,
50
+ exitCode: 128 + (constants.signals[signal] ?? 0),
51
+ };
52
+ }
53
+ if (code === 0)
54
+ return { killed: false, detail: "exited cleanly", exitCode: 0 };
55
+ return {
56
+ killed: false,
57
+ detail: `exited with code ${code}`,
58
+ exitCode: code ?? 1,
59
+ };
60
+ }
@@ -0,0 +1,61 @@
1
+ // The one binary resolver: the installed platform package first, then a dev checkout's build output.
2
+
3
+ import { existsSync } from "node:fs";
4
+ import { basename, join } from "node:path";
5
+
6
+ // Constant, never derived: a deployed component's nearest package.json can carry any name, and a wrong base
7
+ // resolves a platform package that does not exist.
8
+ export const PACKAGE_NAME = "@deliciousmonster/datadog-agent-binary";
9
+
10
+ const EXE = process.platform === "win32" ? ".exe" : "";
11
+
12
+ // The package root, one level up: a dev checkout's build output sits beside runtime/, never inside it.
13
+ const PACKAGE_ROOT = join(import.meta.dirname, "..");
14
+
15
+ /** This package's platform label for the running host; throws where no platform package exists. */
16
+ function platformName() {
17
+ const os = { linux: "linux", darwin: "macos", win32: "windows" }[
18
+ process.platform
19
+ ];
20
+ const arch = { x64: "x86_64", arm64: "arm64" }[process.arch];
21
+ if (!os || !arch) {
22
+ throw new Error(
23
+ `unsupported platform: ${process.platform}-${process.arch}`
24
+ );
25
+ }
26
+ return `${os}-${arch}`;
27
+ }
28
+
29
+ /** The platform package's accessor first (the npm install path), then a dev checkout's build output. */
30
+ export async function resolveBinary(agent) {
31
+ const file = `${agent.shipsAs}${EXE}`;
32
+ const platformPackage = `${PACKAGE_NAME}-${platformName()}`;
33
+ // Set only on a name mismatch, so the error below can tell "the package answered with the wrong
34
+ // binary" apart from "the package isn't installed" instead of collapsing both into one guess.
35
+ let staleMatch;
36
+ try {
37
+ const pkg = await import(platformPackage);
38
+ const getBinaryPath = pkg.getBinaryPath ?? pkg.default?.getBinaryPath;
39
+ const resolved = getBinaryPath?.(agent.shipsAs);
40
+ // Checked by name: a package published before the trace-agent shipped answers every request with
41
+ // the core agent, and that path exists, so trusting it starts two core agents and no receiver.
42
+ if (resolved && basename(resolved) === file && existsSync(resolved)) {
43
+ return resolved;
44
+ }
45
+ if (resolved) staleMatch = resolved;
46
+ } catch {
47
+ // The optional dependency is not installed here; the dev-checkout path below still applies.
48
+ }
49
+ const local = join(PACKAGE_ROOT, "build", platformName(), "bin", file);
50
+ if (existsSync(local)) return local;
51
+ if (staleMatch) {
52
+ throw new Error(
53
+ `no ${agent.title} binary: ${platformPackage} is installed but predates ${file} support (it ` +
54
+ `resolved ${staleMatch} instead) and no local build exists at ${local}. Update ${platformPackage} ` +
55
+ `to a version that ships ${file}, or build locally with npm run build-agent.`
56
+ );
57
+ }
58
+ throw new Error(
59
+ `no ${agent.title} binary: neither ${platformPackage} nor a local build at ${local} resolved ${file}`
60
+ );
61
+ }
@@ -0,0 +1,195 @@
1
+ // The datadog.yaml both agents read, and the core-check configs without which the core agent runs, reports
2
+ // healthy and collects nothing. Everything here is written under Harper's root, never the component directory.
3
+
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { basename, dirname, isAbsolute, join } from "node:path";
15
+ import { threadId } from "node:worker_threads";
16
+
17
+ // Rejects `rootPath: null`, which Harper's own defaultConfig.yaml ships, and anything relative: a relative
18
+ // root puts the runtime tree, the PID locks and the reaper's replacement-pid file under each worker's cwd.
19
+ const absoluteRoot = (value) => (value && isAbsolute(value) ? value : null);
20
+
21
+ // Read here rather than taken from an environment variable this package invents: Harper reads the same chain
22
+ // for itself and exposes no root path to a component. Absolute or null, never throws.
23
+ function readHarperRootPath() {
24
+ try {
25
+ const boot = readFileSync(
26
+ join(homedir(), ".harperdb", "hdb_boot_properties.file"),
27
+ "utf-8"
28
+ );
29
+ // Java-style properties, and Harper indents every line after the first, so the whitespace class matters.
30
+ const settingsPath = boot.match(
31
+ /^[ \t]*settings_path[ \t]*=[ \t]*(.+?)[ \t]*$/m
32
+ )?.[1];
33
+ if (!settingsPath) return null;
34
+ // rootPath is top level in harper-config.yaml: the one key readable off a single line without a parser.
35
+ const rootPath = readFileSync(settingsPath, "utf-8")
36
+ .match(/^rootPath[ \t]*:[ \t]*(.+?)[ \t]*(?:#.*)?$/m)?.[1]
37
+ ?.replace(/^(['"])(.*)\1$/, "$2");
38
+ return absoluteRoot(rootPath);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /** Harper's root path, or null. ROOTPATH is the harper-pro image's own spelling and wins where it is usable. */
45
+ function harperRoot(log) {
46
+ const spelled = process.env.ROOTPATH;
47
+ if (spelled && !absoluteRoot(spelled)) {
48
+ log.warn(
49
+ `Datadog supervisor: ROOTPATH="${spelled}" is not an absolute path, so it is ignored. A relative ` +
50
+ `one resolves against each worker's own cwd, and two workers that disagree take different PID ` +
51
+ `locks and each start their own pair of agents.`
52
+ );
53
+ }
54
+ return absoluteRoot(spelled) ?? readHarperRootPath();
55
+ }
56
+
57
+ /** YAML-safe scalar; double quotes also survive Windows drive letters. */
58
+ const yamlString = (value) => JSON.stringify(String(value));
59
+
60
+ /** The datadog.yaml both agents read: every path off the unwritable Datadog defaults, and the three ports this component probes written as the values it resolved, so a change to an agent default cannot move a port out from under a probe. */
61
+ function renderDatadogYaml(paths, ports) {
62
+ return [
63
+ "# GENERATED by resources.js on every Harper worker start. Edits are overwritten.",
64
+ "# api_key and site are absent by design: they ride in DD_API_KEY / DD_SITE, never on disk.",
65
+ `confd_path: ${yamlString(paths.confd)}`,
66
+ `run_path: ${yamlString(paths.run)}`,
67
+ `auth_token_file_path: ${yamlString(paths.authToken)}`,
68
+ `ipc_cert_file_path: ${yamlString(paths.ipcCert)}`,
69
+ "# The agents log under the runtime tree; a stdio pipe would tie them to one worker thread.",
70
+ "log_to_console: false",
71
+ `log_file: ${yamlString(paths.coreLog)}`,
72
+ // Both agents read these same top-level keys; apm_config has no rotation settings of its own.
73
+ // 5 MiB by 2 rolls bounds each log file at 15 MiB, against a default that bounds nothing here.
74
+ 'log_file_max_size: "5Mb"',
75
+ "log_file_max_rolls: 2",
76
+ "# Loopback only. Nothing here should be reachable from outside the container.",
77
+ 'bind_host: "127.0.0.1"',
78
+ "# Pinned because the core-agent verify reads expvar off this port. Measured on 7.82.1: the environment",
79
+ "# outranks this file, so a set DD_EXPVAR_PORT wins and this only pins the port against a default that moves.",
80
+ `expvar_port: ${ports.expvar}`,
81
+ "apm_config:",
82
+ " enabled: true",
83
+ ` receiver_port: ${ports.receiver}`,
84
+ " # On, this binds 0.0.0.0 and accepts spans from anything that reaches the container.",
85
+ " apm_non_local_traffic: false",
86
+ ` log_file: ${yamlString(paths.traceLog)}`,
87
+ " # The trace-agent's own expvar, separate from the core agent's. Without it nothing on this node",
88
+ " # can say whether a span that reached the receiver ever left for Datadog.",
89
+ " debug:",
90
+ ` port: ${ports.debug}`,
91
+ "",
92
+ ].join("\n");
93
+ }
94
+
95
+ /** The shipped core checks that apply here; an optional `platforms` file beside a check gates it, absent means everywhere. */
96
+ function collectCoreChecks(packageConfd) {
97
+ const checks = [];
98
+ const entries = readdirSync(packageConfd, { withFileTypes: true }).sort(
99
+ (a, b) => (a.name < b.name ? -1 : 1)
100
+ );
101
+ for (const entry of entries) {
102
+ if (!entry.isDirectory() || !entry.name.endsWith(".d")) continue;
103
+ const source = join(packageConfd, entry.name, "conf.yaml.default");
104
+ if (!existsSync(source)) continue;
105
+ const gate = join(packageConfd, entry.name, "platforms");
106
+ if (existsSync(gate)) {
107
+ const platforms = readFileSync(gate, "utf-8")
108
+ .replace(/#.*$/gm, "")
109
+ .split(/\s+/)
110
+ .filter(Boolean);
111
+ if (!platforms.includes(process.platform)) continue;
112
+ }
113
+ checks.push({
114
+ dir: entry.name,
115
+ name: entry.name.slice(0, -".d".length),
116
+ body: readFileSync(source, "utf-8"),
117
+ });
118
+ }
119
+ return checks;
120
+ }
121
+
122
+ /** This start owns exactly the conf.yaml.default files. One nobody claims was left by an older version on a persistent volume; an operator's own conf.yaml is never touched. */
123
+ function removeStaleDefaults(confd, owned) {
124
+ for (const entry of readdirSync(confd, { withFileTypes: true })) {
125
+ if (entry.isDirectory() && !owned.has(entry.name)) {
126
+ rmSync(join(confd, entry.name, "conf.yaml.default"), { force: true });
127
+ }
128
+ }
129
+ }
130
+
131
+ // The runtime tree lives under Harper's root, never the component directory, which `harper deploy` replaces
132
+ // under a live agent. Named by the component's own directory, not just "datadog": sharing one pidDir means sharing one lock.
133
+ export function prepareRuntime(componentDir, { ports, log }) {
134
+ const root = harperRoot(log);
135
+ const runtimeDir = root
136
+ ? join(root, "datadog", basename(componentDir))
137
+ : join(homedir(), ".harper-datadog", basename(componentDir));
138
+ const paths = {
139
+ runtimeDir,
140
+ configFile: join(runtimeDir, "datadog.yaml"),
141
+ confd: join(runtimeDir, "conf.d"),
142
+ run: join(runtimeDir, "run"),
143
+ authToken: join(runtimeDir, "run", "auth_token"),
144
+ ipcCert: join(runtimeDir, "run", "ipc_cert.pem"),
145
+ coreLog: join(runtimeDir, "logs", "agent.log"),
146
+ traceLog: join(runtimeDir, "logs", "trace-agent.log"),
147
+ // Not Harper's own pids/: the guard's reaper stops every guard-written lock it finds in the directory
148
+ // it watches, and a shared one would hold locks this component never wrote.
149
+ pidDir: join(runtimeDir, "pids"),
150
+ reaperLog: join(runtimeDir, "logs", "reaper.log"),
151
+ };
152
+ mkdirSync(paths.run, { recursive: true });
153
+ mkdirSync(dirname(paths.coreLog), { recursive: true });
154
+ mkdirSync(paths.confd, { recursive: true });
155
+ mkdirSync(paths.pidDir, { recursive: true });
156
+
157
+ const configFiles = { [paths.configFile]: renderDatadogYaml(paths, ports) };
158
+ let checks = [];
159
+ try {
160
+ checks = collectCoreChecks(join(componentDir, "conf.d"));
161
+ for (const check of checks) {
162
+ configFiles[join(paths.confd, check.dir, "conf.yaml.default")] =
163
+ check.body;
164
+ }
165
+ removeStaleDefaults(paths.confd, new Set(checks.map((check) => check.dir)));
166
+ } catch (error) {
167
+ log.warn(
168
+ `Datadog supervisor: no core check configuration was collected (${error.message}), so the agent ` +
169
+ `will report healthy and collect no host metrics. Traces are unaffected.`
170
+ );
171
+ }
172
+
173
+ return {
174
+ root,
175
+ paths,
176
+ configFiles,
177
+ coreChecks: checks.map((check) => check.name),
178
+ };
179
+ }
180
+
181
+ /** Temp-and-rename, because every worker thread writes these and a rereading agent must see old or new, never torn. */
182
+ export function writeConfigFiles(configFiles, log) {
183
+ for (const [target, contents] of Object.entries(configFiles)) {
184
+ try {
185
+ mkdirSync(dirname(target), { recursive: true });
186
+ const temp = `${target}.${process.pid}.${threadId}.tmp`;
187
+ writeFileSync(temp, contents, "utf-8");
188
+ renameSync(temp, target);
189
+ } catch (error) {
190
+ log.error(
191
+ `Datadog supervisor: could not write ${target}: ${error.message}`
192
+ );
193
+ }
194
+ }
195
+ }