|
| 1 | +'use strict' |
| 2 | + |
| 3 | +const { fetchAgentInfo } = require('../agent/info') |
| 4 | +const log = require('../log') |
| 5 | + |
| 6 | +const TRAILING_SLASHES = /\/+$/ |
| 7 | + |
| 8 | +/** |
| 9 | + * Receiver discovery contract |
| 10 | + * |
| 11 | + * The tracer sends `GET /info` to its configured local Agent URL. An |
| 12 | + * Agent-compatible trace receiver produces the response. The tracer does not. |
| 13 | + * |
| 14 | + * The full Agent and serverless-init embed the same trace receiver. A future |
| 15 | + * in-process receiver can expose the same contract. Callers can therefore |
| 16 | + * select capabilities without detecting the receiver implementation. |
| 17 | + * |
| 18 | + * For EVP discovery, `endpoints` advertises registered proxy paths. |
| 19 | + * `evp_proxy_allowed_headers` advertises headers that the proxy forwards to |
| 20 | + * intake. It does not include routing headers that the proxy consumes, such as |
| 21 | + * `X-Datadog-EVP-Subdomain`. |
| 22 | + * |
| 23 | + * An advertised route is not a health check. The receiver can register an EVP |
| 24 | + * route while configuration disables its handler. The request then returns |
| 25 | + * `405`. The `/info` version also identifies the embedded Agent code, not a |
| 26 | + * serverless-init image or deployment type. |
| 27 | + * |
| 28 | + * This module only discovers a candidate route. A missing or unresponsive |
| 29 | + * `/info` endpoint returns an error through the shared request timeout and |
| 30 | + * retry policy. A valid response without a compatible path returns no route. |
| 31 | + * Discovery sends no events, so the caller can safely select direct intake |
| 32 | + * after either result. The caller also owns later delivery failures. It can |
| 33 | + * switch future batches after an ambiguous timeout or reset, but it must not |
| 34 | + * replay the current batch because the first receiver might have accepted it. |
| 35 | + * |
| 36 | + * Reference implementations: |
| 37 | + * |
| 38 | + * Agent `/info` and EVP proxy: |
| 39 | + * https://github.com/DataDog/datadog-agent/tree/main/pkg/trace/api |
| 40 | + * |
| 41 | + * serverless-init entry point and embedded trace receiver: |
| 42 | + * https://github.com/DataDog/datadog-agent/blob/main/cmd/serverless-init/main.go |
| 43 | + * https://github.com/DataDog/datadog-agent/blob/main/pkg/serverless/trace/trace.go |
| 44 | + */ |
| 45 | + |
| 46 | +/** |
| 47 | + * Selects the first advertised EVP proxy path that the caller supports. |
| 48 | + * |
| 49 | + * @param {object} agentInfo - Agent `/info` response |
| 50 | + * @param {object} options - Selection options |
| 51 | + * @param {string[]} options.supportedPaths - Supported paths in preference order |
| 52 | + * @param {string[]} [options.requiredHeaders] - Headers that the proxy must forward to intake. Do not include |
| 53 | + * routing headers that the Agent consumes, such as X-Datadog-EVP-Subdomain. |
| 54 | + * @returns {string|undefined} Selected normalized path |
| 55 | + */ |
| 56 | +function selectEVPProxyPath (agentInfo, { supportedPaths, requiredHeaders = [] } = {}) { |
| 57 | + if (!Array.isArray(agentInfo?.endpoints) || |
| 58 | + !Array.isArray(supportedPaths) || |
| 59 | + !Array.isArray(requiredHeaders) || |
| 60 | + requiredHeaders.some(header => typeof header !== 'string')) { |
| 61 | + return |
| 62 | + } |
| 63 | + |
| 64 | + const allowedHeaders = agentInfo.evp_proxy_allowed_headers |
| 65 | + if (allowedHeaders !== undefined) { |
| 66 | + if (!Array.isArray(allowedHeaders)) return |
| 67 | + |
| 68 | + const normalizedHeaders = new Set() |
| 69 | + for (const header of allowedHeaders) { |
| 70 | + if (typeof header === 'string') { |
| 71 | + normalizedHeaders.add(header.toLowerCase()) |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + if (requiredHeaders.some(header => !normalizedHeaders.has(header.toLowerCase()))) { |
| 76 | + return |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + const advertisedPaths = new Set() |
| 81 | + for (const endpoint of agentInfo.endpoints) { |
| 82 | + if (typeof endpoint === 'string') { |
| 83 | + advertisedPaths.add(endpoint.replace(TRAILING_SLASHES, '')) |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + for (const supportedPath of supportedPaths) { |
| 88 | + if (typeof supportedPath !== 'string') continue |
| 89 | + |
| 90 | + const normalizedPath = supportedPath.replace(TRAILING_SLASHES, '') |
| 91 | + if (advertisedPaths.has(normalizedPath)) { |
| 92 | + return normalizedPath |
| 93 | + } |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +/** |
| 98 | + * Discovers an EVP proxy route through the configured Agent URL. |
| 99 | + * |
| 100 | + * This function performs discovery only when the caller invokes it. It stores |
| 101 | + * no state. The Agent information client owns its existing response cache. |
| 102 | + * |
| 103 | + * @param {URL} url - Configured Agent URL |
| 104 | + * @param {object} options - Selection options |
| 105 | + * @param {string[]} options.supportedPaths - Supported paths in preference order |
| 106 | + * @param {string[]} [options.requiredHeaders] - Headers that the proxy must forward to intake. Do not include |
| 107 | + * routing headers that the Agent consumes, such as X-Datadog-EVP-Subdomain. |
| 108 | + * @param {(error: Error|null, route?: {url: URL, basePath: string}) => void} callback - Result callback |
| 109 | + * @returns {void} |
| 110 | + */ |
| 111 | +function discoverEVPProxy (url, options, callback) { |
| 112 | + fetchAgentInfo(url, (error, agentInfo) => { |
| 113 | + if (error) { |
| 114 | + callback(error) |
| 115 | + return |
| 116 | + } |
| 117 | + |
| 118 | + const basePath = selectEVPProxyPath(agentInfo, options) |
| 119 | + if (basePath === undefined) { |
| 120 | + callback(null) |
| 121 | + return |
| 122 | + } |
| 123 | + |
| 124 | + log.debug('EVP proxy route %s discovered through the configured local receiver', basePath) |
| 125 | + callback(null, { url, basePath }) |
| 126 | + }) |
| 127 | +} |
| 128 | + |
| 129 | +module.exports = { |
| 130 | + discoverEVPProxy, |
| 131 | + selectEVPProxyPath, |
| 132 | +} |
0 commit comments