@uknows/setup 0.1.0 → 0.2.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/README.md +99 -32
- package/lib/automate.mjs +1299 -0
- package/lib/check.mjs +149 -33
- package/lib/ci.mjs +21 -0
- package/lib/cli.mjs +114 -29
- package/lib/cloudid.mjs +456 -0
- package/lib/code.mjs +147 -0
- package/lib/context.mjs +126 -0
- package/lib/contract-docs.mjs +1780 -198
- package/lib/contract.mjs +93 -10
- package/lib/detect.mjs +96 -44
- package/lib/dockerfile.mjs +83 -0
- package/lib/endpoint.mjs +36 -5
- package/lib/engine.mjs +1272 -126
- package/lib/existing.mjs +448 -54
- package/lib/git.mjs +53 -0
- package/lib/hooks/UknowsOpenTelemetry.cs +118 -0
- package/lib/hooks/pins.mjs +28 -0
- package/lib/hooks/uknows-otel.cjs +82 -0
- package/lib/hooks/uknows_otel.py +179 -0
- package/lib/hosts.mjs +521 -16
- package/lib/locks.mjs +353 -0
- package/lib/manifest.mjs +27 -7
- package/lib/members.mjs +269 -0
- package/lib/plan.mjs +130 -0
- package/lib/settings.mjs +216 -28
- package/lib/signin.mjs +259 -0
- package/lib/stacks/azure.mjs +345 -0
- package/lib/stacks/channel.mjs +125 -0
- package/lib/stacks/dotnet.mjs +336 -29
- package/lib/stacks/functions-hooks.mjs +228 -0
- package/lib/stacks/functions.mjs +148 -0
- package/lib/stacks/guided.mjs +153 -30
- package/lib/stacks/index.mjs +10 -2
- package/lib/stacks/java.mjs +14 -13
- package/lib/stacks/node.mjs +142 -19
- package/lib/stacks/otel.mjs +56 -4
- package/lib/stacks/python-bootstrap.mjs +27 -7
- package/lib/stacks/python-lock.mjs +105 -0
- package/lib/stacks/python-sets.mjs +143 -0
- package/lib/stacks/python.mjs +261 -63
- package/lib/toml.mjs +64 -0
- package/lib/types.mjs +52 -5
- package/lib/ui.mjs +139 -20
- package/lib/updates.mjs +398 -0
- package/lib/versions.mjs +115 -0
- package/lib/workspace.mjs +4 -1
- package/package.json +3 -8
package/lib/contract-docs.mjs
CHANGED
|
@@ -12,21 +12,27 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The site
|
|
16
|
-
*
|
|
15
|
+
* The site, app and ingest addresses are build settings. A test build sets
|
|
16
|
+
* VITE_UKNOWS_SITE_URL (for example https://test.example.com),
|
|
17
|
+
* VITE_UKNOWS_APP_URL (for example https://app-test.example.com) and
|
|
17
18
|
* VITE_UKNOWS_INGEST_HOST (for example ingest-test.example.com). Without them,
|
|
18
19
|
* the production names below apply. Vite reads import.meta.env. The Node tools
|
|
19
20
|
* that build llms.txt and the setup CLI contract read process.env.
|
|
21
|
+
*
|
|
22
|
+
* The site has the docs, llms.txt and the key check. The app is where people
|
|
23
|
+
* sign in and see their data. The ingest address gets the data.
|
|
20
24
|
*/
|
|
21
|
-
export const DEFAULT_SITE_URL = 'https://uknows.
|
|
22
|
-
export const
|
|
25
|
+
export const DEFAULT_SITE_URL = 'https://uknows.my'
|
|
26
|
+
export const DEFAULT_APP_URL = 'https://app.uknows.my'
|
|
27
|
+
export const DEFAULT_INGEST_HOST = 'ingest.uknows.my'
|
|
28
|
+
export const DEFAULT_DOCS_URL = `${DEFAULT_SITE_URL}/docs`
|
|
23
29
|
|
|
24
30
|
// Written as import.meta.env exactly, so Vite and Vitest replace it. Node has no import.meta.env.
|
|
25
31
|
export const BUILD_ENV = {}
|
|
26
32
|
|
|
27
33
|
/** A build setting without spaces around it or a trailing slash, or the default. A bad value stops the build. */
|
|
28
|
-
export function buildSetting(name, fallback, valid) {
|
|
29
|
-
const raw =
|
|
34
|
+
export function buildSetting(name, fallback, valid, env = BUILD_ENV) {
|
|
35
|
+
const raw = env[name]
|
|
30
36
|
const value = typeof raw === 'string' ? raw.trim().replace(/\/+$/, '') : ''
|
|
31
37
|
if (!value) return fallback
|
|
32
38
|
if (!valid.test(value)) throw new Error(`${name} is not a valid address: ${value}`)
|
|
@@ -34,25 +40,85 @@ export function buildSetting(name, fallback, valid) {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/** https with a host name, or http on this computer for a local test. No path. */
|
|
37
|
-
export const
|
|
43
|
+
export const ORIGIN = /^(?:https:\/\/[a-z0-9]([a-z0-9.-]*[a-z0-9])?|http:\/\/(?:localhost|127\.0\.0\.1))(?::\d{1,5})?$/i
|
|
44
|
+
export const SITE_URL = buildSetting('VITE_UKNOWS_SITE_URL', DEFAULT_SITE_URL, ORIGIN)
|
|
45
|
+
/**
|
|
46
|
+
* Where people sign in and see their data: the app. A test build sets
|
|
47
|
+
* VITE_UKNOWS_APP_URL (for example https://app-test.example.com), the same rule
|
|
48
|
+
* as the site address. Without it: the production app with the production
|
|
49
|
+
* site, or the site address itself with any other site (a copy on 1 address).
|
|
50
|
+
*/
|
|
51
|
+
export const APP_URL = buildSetting('VITE_UKNOWS_APP_URL', SITE_URL === DEFAULT_SITE_URL ? DEFAULT_APP_URL : SITE_URL, ORIGIN)
|
|
38
52
|
/** A host name only, with an optional port. The apps always send over https. */
|
|
39
|
-
export const
|
|
53
|
+
export const HOST = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(?::\d{1,5})?$/i
|
|
54
|
+
export const INGEST_HOST = buildSetting('VITE_UKNOWS_INGEST_HOST', DEFAULT_INGEST_HOST, HOST)
|
|
55
|
+
/** Sign-in and the Station. */
|
|
56
|
+
export const LIVE_URL = `${APP_URL}/live`
|
|
57
|
+
/** The Connect page: open it with ?app=<the app name> to watch that app send its first data. */
|
|
58
|
+
export const CONNECT_URL = `${APP_URL}/live/connect`
|
|
59
|
+
/**
|
|
60
|
+
* The owner's yes to the scan and revoked-key P1 alerts: the same values the
|
|
61
|
+
* server reads (UKNOWS_ALERT_SCAN_P1 and UKNOWS_ALERT_REVOKED_KEY_P1). The
|
|
62
|
+
* release passes both. "on" turns a switch on; unset, empty or "off" leaves it
|
|
63
|
+
* off. Anything else stops the build. These comments go into the setup package
|
|
64
|
+
* through its contract sync, so they name no internal code or file.
|
|
65
|
+
*/
|
|
66
|
+
export function buildSwitch(name) {
|
|
67
|
+
const raw = BUILD_ENV[name]
|
|
68
|
+
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
|
69
|
+
if (value === 'on') return true
|
|
70
|
+
if (value === '' || value === 'off') return false
|
|
71
|
+
throw new Error(`${name} must be on or off`)
|
|
72
|
+
}
|
|
73
|
+
/** A scan is a P1 alert. Off: probing is a count for the last hour. */
|
|
74
|
+
export const SCAN_ALERT = buildSwitch('VITE_UKNOWS_ALERT_SCAN_P1')
|
|
75
|
+
/** Uploads with a revoked or retired key are a P1 alert. Off: they are a count, a fact. */
|
|
76
|
+
export const REVOKED_KEY_ALERT = buildSwitch('VITE_UKNOWS_ALERT_REVOKED_KEY_P1')
|
|
40
77
|
export const DOCS_URL = `${SITE_URL}/docs`
|
|
41
|
-
export const LLMS_URL =
|
|
78
|
+
export const LLMS_URL = DOCS_URL.replace(/\/docs$/, '/llms.txt')
|
|
42
79
|
export const INGEST_ENDPOINT = `https://${INGEST_HOST}/otlp`
|
|
43
80
|
/** The route that tells whether a key works. The site serves it, not the ingest host. */
|
|
44
81
|
export const PING_URL = `${SITE_URL}/api/v1/ingest/ping`
|
|
45
82
|
export const EXAMPLE_SERVICE = 'orders-api'
|
|
83
|
+
/**
|
|
84
|
+
* The example names in the key scripts and the GitHub Actions steps. The setup tool replaces each one with the
|
|
85
|
+
* name that it finds in the deploy files of the repo, and says plainly when a name is still an example.
|
|
86
|
+
*/
|
|
87
|
+
export const EXAMPLE_NAMES = {
|
|
88
|
+
resourceGroup: 'orders-prod',
|
|
89
|
+
apps: `${EXAMPLE_SERVICE} payments`,
|
|
90
|
+
repo: 'my-org/orders-api',
|
|
91
|
+
org: 'my-org',
|
|
92
|
+
environment: 'production',
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* "See your data" and the last line of the setup tool say this. Applications lists an app a few minutes after its
|
|
96
|
+
* first data (Settings › Applications shows it go from Awaiting first data to Sending). The setup tool passes the app
|
|
97
|
+
* address of its run (UKNOWS_APP_URL, else the one of this build), so a run against a test copy never names the
|
|
98
|
+
* production app.
|
|
99
|
+
*/
|
|
100
|
+
export function openLive(appUrl) {
|
|
101
|
+
return `Sign in at ${appUrl.replace(/^https?:\/\//, '')} and go to Applications. Your app shows there a few minutes after it sends its first data.`
|
|
102
|
+
}
|
|
103
|
+
export const OPEN_LIVE = openLive(APP_URL)
|
|
104
|
+
/** The titles of the 4 steps. Texts name a step by its title, never by its number. */
|
|
105
|
+
export const STEP_TITLES = { key: 'Get your key', install: 'Install', secret: 'Set your key once', data: 'See your data' }
|
|
46
106
|
|
|
47
107
|
/** The 5 plain settings. Commit them in the repo of each app. */
|
|
48
108
|
export const SETTINGS = [
|
|
49
109
|
{ name: 'OTEL_SERVICE_NAME', value: EXAMPLE_SERVICE, summary: 'The name of this app in UKNOWS. Use a different name for each app.' },
|
|
50
110
|
{ name: 'OTEL_EXPORTER_OTLP_ENDPOINT', value: INGEST_ENDPOINT, summary: 'The UKNOWS address that gets your data.' },
|
|
51
111
|
{ name: 'OTEL_EXPORTER_OTLP_PROTOCOL', value: 'http/protobuf', summary: 'Send data in the OTLP format over HTTPS.' },
|
|
52
|
-
{ name: 'OTEL_METRICS_EXPORTER', value: 'none', summary: 'Send traces
|
|
53
|
-
{ name: 'OTEL_LOGS_EXPORTER', value: '
|
|
112
|
+
{ name: 'OTEL_METRICS_EXPORTER', value: 'none', summary: 'Send no metrics. UKNOWS takes traces and logs.', standardOnly: true },
|
|
113
|
+
{ name: 'OTEL_LOGS_EXPORTER', value: 'otlp', summary: 'Send the logs of the app too. A log line written during a request carries the trace id of that request.', standardOnly: true },
|
|
54
114
|
]
|
|
55
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The 3 settings for an app that already uses OpenTelemetry or Azure Monitor.
|
|
118
|
+
* They leave its metrics and logs where they go now.
|
|
119
|
+
*/
|
|
120
|
+
export const SHARED_SETTINGS = SETTINGS.filter((setting) => !setting.standardOnly)
|
|
121
|
+
|
|
56
122
|
/** The 1 secret. Set it once for each environment on the platform, never in the repo. */
|
|
57
123
|
export const SECRET = {
|
|
58
124
|
name: 'OTEL_EXPORTER_OTLP_HEADERS',
|
|
@@ -65,13 +131,15 @@ export const SECRET = {
|
|
|
65
131
|
export const SETTINGS_ENV = SETTINGS.map((setting) => `${setting.name}=${setting.value}`).join('\n')
|
|
66
132
|
export const SECRET_ENV = `${SECRET.name}=${SECRET.value}`
|
|
67
133
|
|
|
68
|
-
export function dockerEnv(extra = []) {
|
|
69
|
-
const pairs = [...
|
|
134
|
+
export function dockerEnv(extra = [], settings = SETTINGS) {
|
|
135
|
+
const pairs = [...settings.map((setting) => `${setting.name}=${setting.value}`), ...extra]
|
|
70
136
|
return `ENV ${pairs.join(' \\\n ')}`
|
|
71
137
|
}
|
|
72
138
|
|
|
73
139
|
/** The ENV lines for the final stage of a Dockerfile. */
|
|
74
140
|
export const SETTINGS_DOCKERFILE = dockerEnv()
|
|
141
|
+
/** The ENV lines for an app that already uses OpenTelemetry or Azure Monitor. */
|
|
142
|
+
export const SHARED_SETTINGS_DOCKERFILE = dockerEnv([], SHARED_SETTINGS)
|
|
75
143
|
|
|
76
144
|
/**
|
|
77
145
|
* The 5 settings as an env file, for apps that do not run from a Dockerfile.
|
|
@@ -88,38 +156,537 @@ export const SETTINGS_FILE = {
|
|
|
88
156
|
/**
|
|
89
157
|
* The setup tool, npx @uknows/setup (packages/uknows-setup). A build with
|
|
90
158
|
* test addresses puts them in front of the command, so the test copy of the
|
|
91
|
-
* page gives a command that writes and checks the test addresses
|
|
92
|
-
* gives the plain command.
|
|
159
|
+
* page gives a command that writes and checks the test addresses, and signs in
|
|
160
|
+
* at the app of that copy. Production gives the plain command.
|
|
161
|
+
*/
|
|
162
|
+
/** The version of the setup tool that these pages describe, the newest on npm. */
|
|
163
|
+
export const SETUP_VERSION = '0.2.0'
|
|
164
|
+
/**
|
|
165
|
+
* Setup 0.2.0 is the first version that knows the production addresses. An older one writes and checks addresses that
|
|
166
|
+
* UKNOWS no longer uses, so with it every setup command gives the production addresses in front too.
|
|
167
|
+
*/
|
|
168
|
+
export const PRODUCTION_NAMES_SINCE = '0.2.0'
|
|
169
|
+
/** major, minor and patch of a version, as numbers */
|
|
170
|
+
export function versionParts(version) {
|
|
171
|
+
return version.split('.').map((part) => Number.parseInt(part, 10) || 0)
|
|
172
|
+
}
|
|
173
|
+
/** True when version a is version b or later (major.minor.patch). */
|
|
174
|
+
export function versionAtLeast(a, b) {
|
|
175
|
+
const [x, y] = [versionParts(a), versionParts(b)]
|
|
176
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0)
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
/** The site and ingest addresses in front of a setup command: none for the production names, from 0.2.0 on. */
|
|
180
|
+
export function frontAddresses(site, ingestHost, known) {
|
|
181
|
+
return [
|
|
182
|
+
...(site === DEFAULT_SITE_URL && known ? [] : [`UKNOWS_SITE_URL=${site}`]),
|
|
183
|
+
...(ingestHost === DEFAULT_INGEST_HOST && known ? [] : [`UKNOWS_INGEST_ENDPOINT=https://${ingestHost}/otlp`]),
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The key check needs only the site and the ingest address. Setup also signs in at the app. With a test address, it
|
|
188
|
+
* signs in only when UKNOWS_APP_URL names the app of that copy, so the command names the app then too. A version
|
|
189
|
+
* before 0.2.0 has no sign-in.
|
|
190
|
+
*/
|
|
191
|
+
export function commandAddresses(site, app, ingestHost, known) {
|
|
192
|
+
const front = frontAddresses(site, ingestHost, known)
|
|
193
|
+
return [...front, ...(known && (front.length || app !== DEFAULT_APP_URL) ? [`UKNOWS_APP_URL=${app}`] : [])]
|
|
194
|
+
}
|
|
195
|
+
export const NAMES_KNOWN = versionAtLeast(SETUP_VERSION, PRODUCTION_NAMES_SINCE)
|
|
196
|
+
export const SETUP_ADDRESSES = frontAddresses(SITE_URL, INGEST_HOST, NAMES_KNOWN)
|
|
197
|
+
export const SETUP_COMMAND_ADDRESSES = commandAddresses(SITE_URL, APP_URL, INGEST_HOST, NAMES_KNOWN)
|
|
198
|
+
const setupCommand = (args = '') => [...SETUP_COMMAND_ADDRESSES, `npx @uknows/setup${args}`].join(' ')
|
|
199
|
+
/**
|
|
200
|
+
* The setup command of a build with these build settings, for a version of the setup tool: the command that the pages
|
|
201
|
+
* of that build give. The plain command with the production names from 0.2.0 on, and the addresses in front for a
|
|
202
|
+
* test copy or an older version. The static pages of the site take it at build time, so they give the same command.
|
|
203
|
+
*/
|
|
204
|
+
export function setupCommandFor(env, version = SETUP_VERSION) {
|
|
205
|
+
const site = buildSetting('VITE_UKNOWS_SITE_URL', DEFAULT_SITE_URL, ORIGIN, env)
|
|
206
|
+
const app = buildSetting('VITE_UKNOWS_APP_URL', site === DEFAULT_SITE_URL ? DEFAULT_APP_URL : site, ORIGIN, env)
|
|
207
|
+
const ingestHost = buildSetting('VITE_UKNOWS_INGEST_HOST', DEFAULT_INGEST_HOST, HOST, env)
|
|
208
|
+
return [...commandAddresses(site, app, ingestHost, versionAtLeast(version, PRODUCTION_NAMES_SINCE)), 'npx @uknows/setup'].join(' ')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The prompt line, exactly. bash only: zsh reads -p as a coprocess. */
|
|
212
|
+
export const KEY_PROMPT = "read -rsp 'Paste your UKNOWS key: ' UKNOWS_KEY; echo"
|
|
213
|
+
/** Stops the script, not the terminal, when no key was pasted. */
|
|
214
|
+
export const KEY_EMPTY_GUARD = '[ -n "$UKNOWS_KEY" ] || { echo "No key entered. Nothing changed."; exit 1; }'
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The key check of the setup tool as 1 line, so it is safe to paste at once: read takes the key from the next line
|
|
218
|
+
* you type, never from a pasted line. The key goes to the check only, as a setting of that 1 command, and the shell
|
|
219
|
+
* forgets it at the end. Nothing stays exported. vars: the test addresses of this run, if any.
|
|
93
220
|
*/
|
|
94
|
-
export
|
|
95
|
-
...
|
|
96
|
-
|
|
221
|
+
export function keyCheckLine(vars = SETUP_ADDRESSES) {
|
|
222
|
+
return `${KEY_PROMPT}; ${[...vars, `${SECRET.name}="${SECRET.header}=$UKNOWS_KEY"`, 'npx @uknows/setup check'].join(' ')}; unset UKNOWS_KEY`
|
|
223
|
+
}
|
|
224
|
+
export const KEY_CHECK_LINE = keyCheckLine()
|
|
225
|
+
|
|
226
|
+
/** Said wherever the docs say that no code from your project runs. */
|
|
227
|
+
export const RESOLVER_NOTE = "uv lock, poetry lock and dotnet restore can run your own project's build code to read its metadata."
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* What the setup tool says when it runs the resolver of a project (uv lock,
|
|
231
|
+
* poetry lock, dotnet restore), when it cannot, and when it picks older
|
|
232
|
+
* OpenTelemetry versions for Python. The tool fills in each {name}. The page
|
|
233
|
+
* does not show these.
|
|
234
|
+
*/
|
|
235
|
+
export const RESOLVE = {
|
|
236
|
+
/** Plan sentence for a lock refresh. */
|
|
237
|
+
update: 'Update {lock} with {command}.',
|
|
238
|
+
/** RESOLVER_NOTE for the lock commands of 1 run only. The tool fills in {commands}, such as "dotnet restore". */
|
|
239
|
+
buildCode: "{commands} can run your own project's build code to read its metadata.",
|
|
240
|
+
/** What a lock refresh writes, said in the plan before it runs. */
|
|
241
|
+
writes: '{command} writes only {files} in your repo.',
|
|
242
|
+
restoreWrites: 'dotnet restore updates {files}. It also downloads the packages to the NuGet folder on this computer, and it writes the obj/ folders of the projects. remove gives back {files}, not the obj/ folders.',
|
|
243
|
+
again: 'Then run npx @uknows/setup again.',
|
|
244
|
+
tool: {
|
|
245
|
+
title: 'Install {tool}{version}, then run setup again',
|
|
246
|
+
why: '{lock} needs {tool} to add {packages}.',
|
|
247
|
+
missing: 'This tool did not find {tool} on this computer, so it changed nothing.',
|
|
248
|
+
version: '{file} needs {tool} {need}. This computer has {tool} {have}, so this tool changed nothing.',
|
|
249
|
+
image: '{file} line {line} uses {tool} {need} in the image. The {tool} on this computer must be the same version, so the image reads the lock it writes. This computer has {tool} {have}, so this tool changed nothing.',
|
|
250
|
+
install: 'Install {tool}{version} on this computer.',
|
|
251
|
+
},
|
|
252
|
+
outside: {
|
|
253
|
+
title: 'Run setup from the repo root',
|
|
254
|
+
lines: [
|
|
255
|
+
'{lock} is above this folder. {command} must run in {dir}, so this tool changed nothing here.',
|
|
256
|
+
'Run npx @uknows/setup in the repo root without --path, and pick this service.',
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
failed: {
|
|
260
|
+
title: '{tool} found no versions that fit',
|
|
261
|
+
lines: [
|
|
262
|
+
'{command} found no versions of the packages that fit the limits of your app. This tool put every file back. Nothing changed.',
|
|
263
|
+
'It names {package}. Change or remove the limit on it, then run npx @uknows/setup again.',
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
python: {
|
|
267
|
+
/** Why the plan uses an older OpenTelemetry set than the newest. */
|
|
268
|
+
olderPython: 'The image uses Python {python}. OpenTelemetry {newest} needs Python {need} or later, so this tool uses OpenTelemetry {core} and {contrib}.',
|
|
269
|
+
olderLimit: '{file} line {line} has {value}. So this tool uses OpenTelemetry {core} and {contrib}, the newest versions that fit.',
|
|
270
|
+
/** For uv and Poetry: the resolver picks the versions, and the set decides which instrumentation packages exist. */
|
|
271
|
+
floorPython: 'The project supports Python {python}. OpenTelemetry {newest} needs Python {need} or later, so this tool adds only the packages that OpenTelemetry {contrib} has. {tool} picks the versions.',
|
|
272
|
+
floorLimit: '{file} line {line} has {value}. So this tool adds only the packages that OpenTelemetry {contrib} has. {tool} picks the versions.',
|
|
273
|
+
noSet: {
|
|
274
|
+
title: 'Pick the OpenTelemetry versions by hand',
|
|
275
|
+
lines: [
|
|
276
|
+
'{file} line {line} has {value}. No OpenTelemetry versions that this tool knows fit it, so this tool changed nothing.',
|
|
277
|
+
'Add opentelemetry-distro, opentelemetry-exporter-otlp-proto-http and the instrumentation packages at versions that fit. pip install, then pip freeze, shows the versions.',
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
procfile: {
|
|
281
|
+
title: 'Start the app with opentelemetry-instrument',
|
|
282
|
+
lines: [
|
|
283
|
+
'{file} starts the app on its web line. This tool does not change a Procfile, because the hosts it supports do not read it.',
|
|
284
|
+
'If your platform starts the app from {file}, change the web line to the line below.',
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
dotnet: {
|
|
289
|
+
sdk: {
|
|
290
|
+
title: 'Install the .NET {need} SDK, then run setup again',
|
|
291
|
+
lines: [
|
|
292
|
+
'{lock} needs dotnet restore to add the packages.',
|
|
293
|
+
'This computer has no .NET SDK {need} or later, so this tool changed nothing. It found: {have}.',
|
|
294
|
+
'Install the .NET {need} SDK on this computer.',
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
offline: {
|
|
298
|
+
title: 'Add the packages to the source that your build restores from',
|
|
299
|
+
lines: [
|
|
300
|
+
'{file} line {line} restores the packages with --network=none, so new packages cannot arrive in the image. This tool changed nothing.',
|
|
301
|
+
'Add these packages to the package source that your build uses. Then add them to {csproj}, update packages.lock.json and start the app with instrument.sh, as below.',
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
runtimes: {
|
|
305
|
+
title: 'Update packages.lock.json by hand',
|
|
306
|
+
lines: [
|
|
307
|
+
'{file} line {line} restores for {value}. This tool refreshes the lock for 1 known runtime only, so it changed nothing.',
|
|
308
|
+
'Add the packages below. Run dotnet restore --force-evaluate with the runtime options of your build. Commit packages.lock.json. Then start the app with instrument.sh.',
|
|
309
|
+
],
|
|
310
|
+
/** Instead of the first line, when packages.lock.json has packages for more than 1 runtime. */
|
|
311
|
+
lock: '{lock} has packages for {value}. This tool refreshes the lock for 1 runtime only, so it changed nothing.',
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* Another project references the app project, such as a test project, and has its own packages.lock.json. That
|
|
315
|
+
* lock lists the packages of the app too, so a locked restore of the solution fails (NU1004) unless it is updated
|
|
316
|
+
* as well. The tool fills in {lock}, {project}, {csproj} and {command}.
|
|
317
|
+
*/
|
|
318
|
+
dependent: '{lock} also locks the packages of {csproj}, because {project} references it. This tool updates it with {command}.',
|
|
319
|
+
/** The plan sentence for that refresh. */
|
|
320
|
+
dependentUpdate: 'Update {lock} with {command}, because {project} references {csproj}.',
|
|
321
|
+
/** That project is above the --path folder, so this tool cannot write its lock. */
|
|
322
|
+
dependentOutside: {
|
|
323
|
+
title: 'Run setup from the repo root',
|
|
324
|
+
lines: [
|
|
325
|
+
'{project} references {csproj} and has its own {lock}. It is above this folder, so this tool cannot update that lock, and it changed nothing.',
|
|
326
|
+
'Run npx @uknows/setup in the repo root without --path, and pick this service.',
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
/**
|
|
330
|
+
* The publish names a runtime for another C library than the final image: glibc (linux-x64) on Alpine, or musl
|
|
331
|
+
* (linux-musl-x64) on a glibc image. Then the native profiler of OpenTelemetry does not load. Without -r, the publish
|
|
332
|
+
* holds the native files of every platform and instrument.sh picks the right one, so that needs no step. Advice,
|
|
333
|
+
* not a required step: the startup hook still traces the requests. The tool fills in {where}, {rid}, {libc},
|
|
334
|
+
* {image}, {imageLibc} and {want}.
|
|
335
|
+
*/
|
|
336
|
+
libc: {
|
|
337
|
+
title: 'Publish for the C library of the final image',
|
|
338
|
+
lines: [
|
|
339
|
+
'{where} publishes with -r {rid}, for {libc}. The final image {image} uses {imageLibc}.',
|
|
340
|
+
'So the native part of OpenTelemetry does not load. Requests still show in UKNOWS, but libraries that need the native part, such as StackExchange.Redis and Microsoft.Data.Sqlite, get no spans.',
|
|
341
|
+
'Publish with -r {want}. If the project has a packages.lock.json, add {want} to its RuntimeIdentifiers and run dotnet restore --force-evaluate first, so the lock has the packages for it.',
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
/** A note when the tool runs dotnet restore and no .dockerignore keeps obj/ out of the image. */
|
|
345
|
+
dockerignore: 'dotnet restore writes obj/ folders in your repo. A docker build on this computer copies them into the image and the build can fail. Add bin/ and obj/ to .dockerignore.',
|
|
346
|
+
assets: {
|
|
347
|
+
title: 'Run setup again for {package}',
|
|
348
|
+
lines: [
|
|
349
|
+
'{assets} shows that the app uses {library} {version} through {via}. The build also needs {package} {need}. This tool put every file back. Nothing changed.',
|
|
350
|
+
'Run npx @uknows/setup again. It reads that file now and adds {package}.',
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
/**
|
|
355
|
+
* npm on this computer writes the lock that npm ci in the image reads. A newer npm than the one in the image can
|
|
356
|
+
* write a lock that the older npm refuses. The tool fills in {file}, {line}, {image}, {need} and {have}.
|
|
357
|
+
*/
|
|
358
|
+
npm: {
|
|
359
|
+
title: 'Use npm {need} on this computer, then run setup again',
|
|
360
|
+
lines: [
|
|
361
|
+
'{file} line {line} builds with {image}, which comes with npm {need}. This computer has npm {have}. A newer npm can write a package-lock.json that npm ci in the image refuses, so this tool changed nothing.',
|
|
362
|
+
'Install npm {need} on this computer with npm install -g npm@{need}, or use the Node.js version of the image, which comes with npm {need}.',
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The Windows tip for a bash terminal. It is the same everywhere, and the welcome email of the product says the same.
|
|
369
|
+
* Only the steps of a cloud name its own shell too: Azure Cloud Shell in the Azure steps, AWS CloudShell in the AWS steps.
|
|
370
|
+
*/
|
|
371
|
+
export const WINDOWS_TIP = 'On Windows, use WSL.'
|
|
372
|
+
export const MACOS_TIP = 'On macOS, type bash first.'
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* What the setup tool needs on your computer. The tool updates the lock files of your app with the tools of your app,
|
|
376
|
+
* so it needs them too. The page and the --help of the tool show these lines.
|
|
377
|
+
*/
|
|
378
|
+
export const SETUP_NEEDS = [
|
|
379
|
+
'Node.js 18 or later, and git.',
|
|
380
|
+
'For a Node.js app: its package tool, npm, pnpm or yarn. npm must not be newer than the npm of your Node.js image.',
|
|
381
|
+
'For a Python app with uv or Poetry: that tool, at the version that your Dockerfile uses.',
|
|
382
|
+
'For a .NET app with packages.lock.json: the .NET SDK of global.json.',
|
|
383
|
+
`A bash terminal for the key steps. ${MACOS_TIP} ${WINDOWS_TIP}`,
|
|
97
384
|
]
|
|
98
|
-
const setupCommand = (args = '') => [...SETUP_ADDRESSES, `npx @uknows/setup${args}`].join(' ')
|
|
99
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Setup runs once on a computer, never in a pipeline. The /docs page and
|
|
388
|
+
* llms.txt show this section, and the setup tool prints it when it stops in a
|
|
389
|
+
* pipeline (exit 2).
|
|
390
|
+
*/
|
|
391
|
+
export const CI_CD = {
|
|
392
|
+
id: 'ci-cd',
|
|
393
|
+
title: 'CI/CD and pipelines',
|
|
394
|
+
summary: 'Run setup once, on your computer, in your repo. Do not run it in your pipeline.',
|
|
395
|
+
steps: [
|
|
396
|
+
'Run npx @uknows/setup in your repo. Review the diff. Commit the change and open a pull request.',
|
|
397
|
+
'Your pipeline builds and deploys the change as usual. It needs no UKNOWS step to build.',
|
|
398
|
+
'Store your key in the secret store of your pipeline, once for each environment. In GitHub Actions, use a secret named UKNOWS_KEY that holds the key only.',
|
|
399
|
+
'In your deploy job, check that the secret is not empty. Set the key on your app before the deploy that uses it.',
|
|
400
|
+
'If your deploy replaces all environment variables, add the key to that list. This includes --replace-env-vars, the environmentVariables of the deploy action, and Bicep or Terraform templates. Otherwise each deploy removes the key.',
|
|
401
|
+
],
|
|
402
|
+
check: 'Setup stops when it runs in a pipeline. To check that a repo is set up, run the command below in the pipeline. It changes nothing. The repo is set up when the JSON says "status": "nothing-to-change" and "exitCode": 0, and the command fails otherwise. In a monorepo, it checks the services that setup set up. The command uses jq, which GitHub Actions and Azure Pipelines runners have.',
|
|
403
|
+
snippet: { caption: 'Check in your pipeline that the repo is set up', language: 'bash', code: `${setupCommand(' --dry-run --json')} | jq -e '.status == "nothing-to-change" and .exitCode == 0'` },
|
|
404
|
+
/** What the setup tool prints when it stops in a pipeline: what happened, then 1 next step. It fills in {docs}. */
|
|
405
|
+
stop: 'This runs in a pipeline, so the tool changes nothing.',
|
|
406
|
+
next: 'Run npx @uknows/setup once on your computer and commit the change. To check in a pipeline that a repo is set up, see {docs}.',
|
|
407
|
+
allowCi: 'A bot or an AI agent that opens a pull request can add --allow-ci.',
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* What the setup tool prints when a run has nothing to change. A repo counts as set up ("nothing-to-change", exit 0)
|
|
412
|
+
* when .uknows/setup.json lists every service, or when no required step is left. Otherwise the status is
|
|
413
|
+
* "needs-steps" with exit 3: the tool could plan nothing, and the steps are yours. The tool fills in {count} and
|
|
414
|
+
* {manifest}. The page does not show these.
|
|
415
|
+
*/
|
|
416
|
+
export const SETUP_STATUS = {
|
|
417
|
+
done: 'Already set up. Nothing to change.',
|
|
418
|
+
doneSteps: 'Steps that this tool cannot check in your repo. Do them if you have not done them yet:',
|
|
419
|
+
steps: 'Nothing for this tool to change. The steps below are yours.',
|
|
420
|
+
recorded: 'Planning the {count} that {manifest} lists. To plan other services, use --all or --path <folder>.',
|
|
421
|
+
/** Set up earlier, but steps in the repo that this tool can check are not done yet ("steps-left", exit 3). */
|
|
422
|
+
stepsLeft: 'Set up earlier, but {count} in your repo {verb} not done yet. This tool checks {pronoun} each time it runs:',
|
|
423
|
+
/** Nothing to change, but the services are not in the record yet: the run writes only the record. */
|
|
424
|
+
record: 'This run writes only {manifest}, so the pipeline check and remove know this service. It changes no file of your app.',
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The last line of a setup run. It fills in {count} and {open}. "data" only when the repo needs nothing more from you
|
|
429
|
+
* and the key check did not fail. {open} is openLive with the app address of the run, or "testCopy" when the run
|
|
430
|
+
* goes to a test copy of UKNOWS and does not know its app address.
|
|
431
|
+
*/
|
|
432
|
+
export const SETUP_NEXT = {
|
|
433
|
+
data: 'Next: commit, open a pull request and deploy as usual. {open}',
|
|
434
|
+
testCopy: 'Then sign in to your test copy of UKNOWS. To see its address here, set UKNOWS_APP_URL to the address of its app.',
|
|
435
|
+
steps: 'Next: do the {count} above first. Then commit, open a pull request and deploy as usual.',
|
|
436
|
+
rejected: 'Next: fix the key first. An Admin or an Engineer of your workspace can show the key again, or create a new one, in Settings › Keys. Then check it with this 1 line in bash:',
|
|
437
|
+
network: 'Next: check the network as the lines above say. You can commit the change already. Then check the key again with this 1 line in bash:',
|
|
438
|
+
skipped: 'To check your key, run this 1 line in bash and paste your key. Nothing stays set in your terminal:',
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const hostOf = (url ) => url.replace(/^https?:\/\//, '')
|
|
442
|
+
/** The store of the UKNOWS agent images (the agent channel): the host of DEFAULT_AGENT_REGISTRY in the setup tool. */
|
|
443
|
+
export const AGENT_REGISTRY_HOST = 'registry.uknows.my'
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Every address that UKNOWS uses, and who connects to it. OpenTelemetry for Node.js asks the metadata addresses of
|
|
447
|
+
* the clouds when the app starts (169.254.169.254 on port 80, and the Kubernetes API on EKS), unless
|
|
448
|
+
* OTEL_NODE_RESOURCE_DETECTORS leaves those detectors out. Each cloud detector (alibaba, aws, azure, gcp) also asks a
|
|
449
|
+
* metadata address, so the Node.js steps keep only the local ones. Checked on 18 Oct 2026.
|
|
450
|
+
*/
|
|
100
451
|
export const NETWORK = {
|
|
101
452
|
host: INGEST_HOST,
|
|
102
453
|
port: 443,
|
|
103
454
|
text: `Allow outbound HTTPS from your apps to ${INGEST_HOST} on port 443. Your apps send data to UKNOWS. UKNOWS never connects to your apps.`,
|
|
455
|
+
hosts: [
|
|
456
|
+
{ host: INGEST_HOST, who: 'Your apps send their traces and logs here, over HTTPS on port 443.' },
|
|
457
|
+
{ host: hostOf(APP_URL), who: 'Your team signs in here in the browser. The setup tool asks here for its sign-in link, and gets the key here after you approve.' },
|
|
458
|
+
{ host: hostOf(SITE_URL), who: 'The docs are here. The setup tool asks here which workspace and environment a key sends to.' },
|
|
459
|
+
{ host: AGENT_REGISTRY_HOST, who: 'The Docker builds of Java, .NET and Node.js apps copy the UKNOWS agent from here, over HTTPS on port 443, with no sign-in.' },
|
|
460
|
+
{ host: 'registry.npmjs.org, pypi.org, files.pythonhosted.org and api.nuget.org', who: 'npm, uv, Poetry and dotnet restore download packages from their registry when the setup tool updates the lock file of your app, on your computer.' },
|
|
461
|
+
],
|
|
462
|
+
node: 'A Node.js app that you set up with these steps asks no cloud metadata address when it starts. OTEL_NODE_RESOURCE_DETECTORS keeps only the detectors that read the app and its host. Without it, OpenTelemetry for Node.js asks the metadata addresses of the clouds, such as 169.254.169.254 on port 80.',
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** The settings that a stack commits: its own list, or the 5. */
|
|
466
|
+
export function stackSettings(stack) {
|
|
467
|
+
return stack.settings ?? SETTINGS
|
|
104
468
|
}
|
|
105
469
|
|
|
106
470
|
/** Extra steps that the page can show folded, under the title. */
|
|
107
471
|
|
|
108
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* The upstream OpenTelemetry Java agent, pinned. Checksum from the release
|
|
474
|
+
* checksums.txt. The OpenTelemetry Operator image holds the same jar at
|
|
475
|
+
* /javaagent.jar. The digest is the image index, for every platform.
|
|
476
|
+
*/
|
|
109
477
|
export const JAVA_AGENT = {
|
|
110
478
|
version: '2.32.0',
|
|
111
479
|
file: 'opentelemetry-javaagent.jar',
|
|
112
480
|
url: 'https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.32.0/opentelemetry-javaagent.jar',
|
|
113
481
|
sha256: 'f787eb6c7f3d18e69a431e108a15278d25ee37f83d68b678f621e063f3988f82',
|
|
482
|
+
image: 'ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.32.0',
|
|
483
|
+
digest: 'sha256:9dad1c6e3e2ecee48fc164a19bcfde703510bee972c8cb421689fde89b5f89e9',
|
|
484
|
+
/** Where the jar is in the image. */
|
|
485
|
+
imagePath: '/javaagent.jar',
|
|
114
486
|
}
|
|
115
487
|
|
|
488
|
+
/** The Dockerfile line that copies the agent from the pinned image. It needs no BuildKit feature. */
|
|
489
|
+
export const JAVA_COPY_LINE = `COPY --from=${JAVA_AGENT.image}@${JAVA_AGENT.digest} ${JAVA_AGENT.imagePath} /otel/${JAVA_AGENT.file}`
|
|
490
|
+
|
|
491
|
+
/** For networks that block ghcr.io: a build stage that downloads the agent from GitHub and checks it. */
|
|
492
|
+
export const JAVA_AGENT_STAGE = [
|
|
493
|
+
'# A build stage, before your final stage',
|
|
494
|
+
'FROM alpine:3.20 AS otel-agent',
|
|
495
|
+
`RUN wget -q -O /${JAVA_AGENT.file} ${JAVA_AGENT.url} \\`,
|
|
496
|
+
` && echo "${JAVA_AGENT.sha256} /${JAVA_AGENT.file}" | sha256sum -c -`,
|
|
497
|
+
'',
|
|
498
|
+
'# Final stage: use this line instead of the COPY line from ghcr.io',
|
|
499
|
+
`COPY --from=otel-agent /${JAVA_AGENT.file} /otel/${JAVA_AGENT.file}`,
|
|
500
|
+
].join('\n')
|
|
501
|
+
|
|
116
502
|
export const NODE_REGISTER = '@opentelemetry/auto-instrumentations-node/register'
|
|
117
503
|
/** For ES modules: the loader hook gives each request its route. No space after = so 1 value holds both options. */
|
|
118
504
|
export const NODE_ESM_OPTIONS = `--experimental-loader=@opentelemetry/instrumentation/hook.mjs --import=${NODE_REGISTER}`
|
|
119
505
|
|
|
506
|
+
/**
|
|
507
|
+
* A small start file for Node.js. It starts OpenTelemetry like the upstream
|
|
508
|
+
* register. On SIGTERM it sends the last data, then stops the app as Node
|
|
509
|
+
* does, unless the app has its own SIGTERM handler. A missing package only
|
|
510
|
+
* prints a warning. The setup tool writes the same file.
|
|
511
|
+
*
|
|
512
|
+
* As PID 1 in a container (CMD ["node", ...] without an init), Linux ignores
|
|
513
|
+
* a signal that has no handler, so the SIGTERM that the file sends again does
|
|
514
|
+
* not stop node. Then the file exits with code 143, the code of SIGTERM.
|
|
515
|
+
* Checked with docker stop on 18 Oct 2026: 10 seconds and a kill before, under
|
|
516
|
+
* 1 second after.
|
|
517
|
+
*/
|
|
518
|
+
export const NODE_REGISTER_FILE = {
|
|
519
|
+
path: 'uknows-otel.cjs',
|
|
520
|
+
language: 'javascript',
|
|
521
|
+
summary: 'Add this file next to package.json.',
|
|
522
|
+
content: `'use strict'
|
|
523
|
+
// Added for UKNOWS. Starts standard OpenTelemetry for Node.js.
|
|
524
|
+
// OTEL_SDK_DISABLED=true turns it off. On SIGTERM it sends the last data, then
|
|
525
|
+
// stops the app as Node does, unless the app has its own SIGTERM handler.
|
|
526
|
+
if (String(process.env.OTEL_SDK_DISABLED || '').trim().toLowerCase() !== 'true') {
|
|
527
|
+
const before = new Set(process.listeners('SIGTERM'))
|
|
528
|
+
try {
|
|
529
|
+
require(require.resolve('${NODE_REGISTER}', { paths: [__dirname, process.cwd()] }))
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.warn(\`OpenTelemetry did not start: \${error.message}\`)
|
|
532
|
+
}
|
|
533
|
+
for (const listener of process.listeners('SIGTERM')) {
|
|
534
|
+
if (before.has(listener)) continue
|
|
535
|
+
process.removeListener('SIGTERM', listener)
|
|
536
|
+
process.on('SIGTERM', function stopAfterFlush(signal) {
|
|
537
|
+
const alone = process.listenerCount('SIGTERM') === 1
|
|
538
|
+
let stopping = false
|
|
539
|
+
const stop = () => {
|
|
540
|
+
if (!alone || stopping) return
|
|
541
|
+
stopping = true
|
|
542
|
+
process.removeListener('SIGTERM', stopAfterFlush)
|
|
543
|
+
process.kill(process.pid, 'SIGTERM')
|
|
544
|
+
// As PID 1 in a container, Linux ignores that signal. Then exit with the code of SIGTERM.
|
|
545
|
+
setTimeout(() => process.exit(143), 100)
|
|
546
|
+
}
|
|
547
|
+
setTimeout(stop, 5000).unref()
|
|
548
|
+
Promise.resolve(listener(signal)).then(stop, stop)
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
}`,
|
|
552
|
+
}
|
|
553
|
+
/** Where the setup tool and the steps put the start file in an image. */
|
|
554
|
+
export const NODE_REGISTER_PATH = `/app/${NODE_REGISTER_FILE.path}`
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* What the setup tool says about the start file. It fills in {file}, {dockerfile}, {ignore} and {dockerfiles}. When
|
|
558
|
+
* it cannot be sure that the file reaches the running app, it loads the register line instead and says why. The page does not show these.
|
|
559
|
+
*/
|
|
560
|
+
export const NODE_START_FILE = {
|
|
561
|
+
add: 'Add {file}. It starts OpenTelemetry and stops the app cleanly on SIGTERM.',
|
|
562
|
+
copy: 'Copy the start file into the image.',
|
|
563
|
+
keep: '{file} is there already. This tool keeps it as it is.',
|
|
564
|
+
noContext: 'This tool cannot tell the build context of {dockerfile} from its COPY lines.',
|
|
565
|
+
noWorkdir: 'The final stage of {dockerfile} has no WORKDIR with a full path, so this tool does not know where the app is in the image.',
|
|
566
|
+
outside: '{file} is outside the build context of {dockerfile}.',
|
|
567
|
+
ignored: '{ignore} leaves {file} out of the build context.',
|
|
568
|
+
gitignored: '.gitignore leaves {file} out of git, so a fresh checkout and the build in your pipeline do not have it.',
|
|
569
|
+
dockerfiles: 'This tool did not pick a Dockerfile for this service. It saw {dockerfiles}. An image built from them would not have {file}.',
|
|
570
|
+
fallback: 'So this tool loads the OpenTelemetry register line instead of the start file. The app works, but it does not stop on SIGTERM by itself. Docker, Kubernetes and systemd then wait for their time limit and kill it. To stop cleanly, add the start file yourself.',
|
|
571
|
+
}
|
|
572
|
+
|
|
120
573
|
/** .NET writes its own OpenTelemetry log files. The default folder is not writable on macOS or as a user that is not root. */
|
|
121
574
|
export const DOTNET_LOG_DIRECTORY = 'OTEL_DOTNET_AUTO_LOG_DIRECTORY=/tmp/otel-dotnet'
|
|
122
575
|
|
|
576
|
+
/**
|
|
577
|
+
* .NET sends an ILogger line as its message template, such as "Served order {Id}", with the values beside it. UKNOWS
|
|
578
|
+
* keeps the text of a log line and not those values (DATA_FACTS logs), so the text must be the formatted message.
|
|
579
|
+
*/
|
|
580
|
+
export const DOTNET_LOGS_MESSAGE = 'OTEL_DOTNET_AUTO_LOGS_INCLUDE_FORMATTED_MESSAGE=true'
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Node.js: only the resource detectors that read the app and its host (NETWORK.node). env goes last, so a value in
|
|
584
|
+
* OTEL_RESOURCE_ATTRIBUTES wins over a detected one.
|
|
585
|
+
*/
|
|
586
|
+
export const NODE_DETECTORS = 'OTEL_NODE_RESOURCE_DETECTORS=host,os,serviceinstance,container,env'
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Python: the host name and the operating system of the app, as Node.js sends them. Python sends no host name without
|
|
590
|
+
* it, so UKNOWS could not tell which revision or pod of the app the data comes from. Neither detector asks a network
|
|
591
|
+
* address. OTEL_RESOURCE_ATTRIBUTES still wins over a detected value.
|
|
592
|
+
*/
|
|
593
|
+
export const PYTHON_DETECTORS = 'OTEL_EXPERIMENTAL_RESOURCE_DETECTORS=host,os'
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Python: the OpenTelemetry logging package stays off the root logger. On the root logger it changes what the app
|
|
597
|
+
* prints: Python's last-resort output, logging.warning() before any setup and Flask's own handler all stop (install
|
|
598
|
+
* proof, 22 Oct 2026). The UKNOWS log bridge sends the log records instead.
|
|
599
|
+
*/
|
|
600
|
+
export const PYTHON_LOG_HANDLER_OFF = 'OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION=false'
|
|
601
|
+
|
|
602
|
+
/** Where the UKNOWS log bridge for Python goes: a folder next to the dependency file, copied into the image. */
|
|
603
|
+
export const PYTHON_BRIDGE_DIR = 'uknows-otel'
|
|
604
|
+
export const PYTHON_BRIDGE_IMAGE = '/opt/uknows-otel'
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* The UKNOWS log bridge for Python. opentelemetry-instrument finds its entry point on PYTHONPATH and calls install()
|
|
608
|
+
* after it starts OpenTelemetry. The bridge sends each log record that the app's own logging handles, without a
|
|
609
|
+
* handler on the root logger, so the app prints exactly what it printed before.
|
|
610
|
+
*/
|
|
611
|
+
export const PYTHON_LOG_BRIDGE = {
|
|
612
|
+
dir: PYTHON_BRIDGE_DIR,
|
|
613
|
+
image: PYTHON_BRIDGE_IMAGE,
|
|
614
|
+
files: [
|
|
615
|
+
{
|
|
616
|
+
path: 'uknows_logs.py',
|
|
617
|
+
language: 'python',
|
|
618
|
+
summary: 'The bridge.',
|
|
619
|
+
content: `# Added for UKNOWS. Sends the log records of this app to UKNOWS with OpenTelemetry, and changes nothing in what the
|
|
620
|
+
# app prints: no handler goes on the root logger, so Python's last-resort output, logging.basicConfig, Flask's own
|
|
621
|
+
# handler and every check for handlers work as before. opentelemetry-instrument calls install() after it starts
|
|
622
|
+
# OpenTelemetry. OTEL_LOGS_EXPORTER=none turns it off.
|
|
623
|
+
import logging
|
|
624
|
+
import os
|
|
625
|
+
|
|
626
|
+
def install():
|
|
627
|
+
try:
|
|
628
|
+
_install()
|
|
629
|
+
except Exception: # never stop the app, never print in its output
|
|
630
|
+
pass
|
|
631
|
+
|
|
632
|
+
def _install():
|
|
633
|
+
if "otlp" not in [name.strip() for name in os.environ.get("OTEL_LOGS_EXPORTER", "").split(",")]:
|
|
634
|
+
return
|
|
635
|
+
if getattr(logging.Logger.callHandlers, "_uknows", False):
|
|
636
|
+
return
|
|
637
|
+
from opentelemetry._logs import get_logger_provider
|
|
638
|
+
|
|
639
|
+
try:
|
|
640
|
+
from opentelemetry.instrumentation.logging import LoggingInstrumentor
|
|
641
|
+
from opentelemetry.instrumentation.logging.handler import LoggingHandler
|
|
642
|
+
except ImportError:
|
|
643
|
+
return
|
|
644
|
+
# The logging package puts its handler on the root logger, which changes what the app prints. Take it off.
|
|
645
|
+
root = logging.getLogger()
|
|
646
|
+
other = getattr(LoggingInstrumentor, "_logging_handler", None)
|
|
647
|
+
if other is not None and other in root.handlers:
|
|
648
|
+
root.removeHandler(other)
|
|
649
|
+
handler = LoggingHandler(logger_provider=get_logger_provider())
|
|
650
|
+
handler.handleError = lambda record: None
|
|
651
|
+
call_handlers = logging.Logger.callHandlers
|
|
652
|
+
|
|
653
|
+
def callHandlers(self, record):
|
|
654
|
+
call_handlers(self, record)
|
|
655
|
+
if record.name.startswith("opentelemetry"):
|
|
656
|
+
return
|
|
657
|
+
try:
|
|
658
|
+
handler.handle(record)
|
|
659
|
+
except Exception:
|
|
660
|
+
pass
|
|
661
|
+
|
|
662
|
+
callHandlers._uknows = True
|
|
663
|
+
logging.Logger.callHandlers = callHandlers`,
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
path: 'uknows_logs-1.0.dist-info/METADATA',
|
|
667
|
+
language: 'text',
|
|
668
|
+
summary: 'Its name, so Python finds the entry point.',
|
|
669
|
+
content: `Metadata-Version: 2.1
|
|
670
|
+
Name: uknows-logs
|
|
671
|
+
Version: 1.0
|
|
672
|
+
Summary: Added for UKNOWS. Sends the log records of the app without changing what the app prints.`,
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
path: 'uknows_logs-1.0.dist-info/entry_points.txt',
|
|
676
|
+
language: 'ini',
|
|
677
|
+
summary: 'The entry point that opentelemetry-instrument calls.',
|
|
678
|
+
content: `[opentelemetry_post_instrument]
|
|
679
|
+
uknows_logs = uknows_logs:install`,
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** PYTHONPATH with the bridge folder of the image after the value that the image has, if any. */
|
|
685
|
+
export const PYTHON_BRIDGE_PATH = 'PYTHONPATH=${PYTHONPATH:+${PYTHONPATH}:}' + PYTHON_BRIDGE_IMAGE
|
|
686
|
+
|
|
687
|
+
/** The Dockerfile line that copies the bridge folder into the image. */
|
|
688
|
+
export const PYTHON_BRIDGE_COPY = `COPY ${PYTHON_BRIDGE_DIR} ${PYTHON_BRIDGE_IMAGE}`
|
|
689
|
+
|
|
123
690
|
export const GO_INIT = `package main
|
|
124
691
|
|
|
125
692
|
import (
|
|
@@ -132,6 +699,7 @@ import (
|
|
|
132
699
|
"go.opentelemetry.io/otel/attribute"
|
|
133
700
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
134
701
|
"go.opentelemetry.io/otel/propagation"
|
|
702
|
+
"go.opentelemetry.io/otel/sdk/resource"
|
|
135
703
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
136
704
|
"go.opentelemetry.io/otel/trace"
|
|
137
705
|
)
|
|
@@ -146,7 +714,10 @@ func setupOTel(ctx context.Context) (func(context.Context) error, error) {
|
|
|
146
714
|
if err != nil {
|
|
147
715
|
return nil, err
|
|
148
716
|
}
|
|
149
|
-
|
|
717
|
+
// The OTEL_ settings and the host name: UKNOWS tells the instances of the app apart by the host name.
|
|
718
|
+
// A detector that fails leaves only its own part out.
|
|
719
|
+
res, _ := resource.New(ctx, resource.WithFromEnv(), resource.WithTelemetrySDK(), resource.WithHost())
|
|
720
|
+
provider := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter), sdktrace.WithResource(res))
|
|
150
721
|
otel.SetTracerProvider(provider)
|
|
151
722
|
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
|
152
723
|
propagation.TraceContext{}, propagation.Baggage{},
|
|
@@ -177,23 +748,22 @@ func setRoute(r *http.Request, route string) {
|
|
|
177
748
|
}
|
|
178
749
|
}`
|
|
179
750
|
|
|
180
|
-
|
|
751
|
+
/** The part of main.go before the router lines. */
|
|
752
|
+
export const GO_MAIN_HEAD = `// Imports for this code: "context", "errors", "log", "net/http",
|
|
181
753
|
// "os", "os/signal", "syscall" and "time".
|
|
182
754
|
|
|
183
755
|
// 1. Start OpenTelemetry at the start of main.
|
|
184
756
|
shutdown, err := setupOTel(context.Background())
|
|
185
757
|
if err != nil {
|
|
186
758
|
log.Fatal(err)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 2. Add the middleware for your router. Keep only the lines for your router.
|
|
190
|
-
var handler http.Handler
|
|
759
|
+
}`
|
|
191
760
|
|
|
192
|
-
|
|
761
|
+
/** The middleware of each router. Each one sets handler. echoOlder is Echo v4 with Go below 1.25. */
|
|
762
|
+
export const GO_ROUTERS = {
|
|
763
|
+
nethttp: `// net/http: wrap the ServeMux. withRoute is in otel.go.
|
|
193
764
|
// import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
194
|
-
handler = otelhttp.NewHandler(withRoute(mux), "http.server")
|
|
195
|
-
|
|
196
|
-
// chi: add this middleware before your routes. Then wrap the router.
|
|
765
|
+
handler = otelhttp.NewHandler(withRoute(mux), "http.server")`,
|
|
766
|
+
chi: `// chi: add this middleware before your routes. Then wrap the router.
|
|
197
767
|
// import "github.com/go-chi/chi/v5" and otelhttp
|
|
198
768
|
router.Use(func(next http.Handler) http.Handler {
|
|
199
769
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
@@ -201,22 +771,38 @@ router.Use(func(next http.Handler) http.Handler {
|
|
|
201
771
|
setRoute(r, chi.RouteContext(r.Context()).RoutePattern())
|
|
202
772
|
})
|
|
203
773
|
})
|
|
204
|
-
handler = otelhttp.NewHandler(router, "http.server")
|
|
205
|
-
|
|
206
|
-
// Gin: add the middleware. Then use the router as the handler.
|
|
774
|
+
handler = otelhttp.NewHandler(router, "http.server")`,
|
|
775
|
+
gin: `// Gin: add the middleware. Then use the router as the handler.
|
|
207
776
|
// import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
|
208
777
|
router.Use(otelgin.Middleware(os.Getenv("OTEL_SERVICE_NAME")))
|
|
209
|
-
handler = router
|
|
210
|
-
|
|
211
|
-
// Echo v4: add the middleware. Then use e as the handler.
|
|
778
|
+
handler = router`,
|
|
779
|
+
echo: `// Echo v4: add the middleware. Then use e as the handler.
|
|
212
780
|
// import echootel "github.com/labstack/echo-otel/v4"
|
|
213
781
|
e.Use(echootel.NewMiddleware(""))
|
|
214
|
-
handler = e
|
|
782
|
+
handler = e`,
|
|
783
|
+
echoOlder: `// Echo v4: add the middleware. Then use e as the handler.
|
|
784
|
+
// import "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
|
|
785
|
+
e.Use(otelecho.Middleware(os.Getenv("OTEL_SERVICE_NAME")))
|
|
786
|
+
handler = e`,
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* main.go with the middleware of the given routers and the address the server listens on. The page shows every
|
|
791
|
+
* router and :8080. The setup tool shows the router it found and the port of the app.
|
|
792
|
+
*/
|
|
793
|
+
export function goMain(routers = ['nethttp', 'chi', 'gin', 'echo'], address = ':8080') {
|
|
794
|
+
const keep = routers.length === 1 ? '' : ' Keep only the lines for your router.'
|
|
795
|
+
return `${GO_MAIN_HEAD}
|
|
796
|
+
|
|
797
|
+
// 2. Add the middleware for your router.${keep}
|
|
798
|
+
var handler http.Handler
|
|
799
|
+
|
|
800
|
+
${routers.map((router) => GO_ROUTERS[router]).join('\n\n')}
|
|
215
801
|
|
|
216
802
|
// 3. Run the server. On Ctrl+C or SIGTERM, stop the server.
|
|
217
803
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
218
804
|
defer stop()
|
|
219
|
-
srv := &http.Server{Addr: "
|
|
805
|
+
srv := &http.Server{Addr: "${address}", Handler: handler}
|
|
220
806
|
serveErr := make(chan error, 1)
|
|
221
807
|
go func() { serveErr <- srv.ListenAndServe() }()
|
|
222
808
|
select {
|
|
@@ -234,6 +820,109 @@ cancel()
|
|
|
234
820
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
235
821
|
log.Fatal(err)
|
|
236
822
|
}`
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export const GO_MAIN = goMain()
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* OpenTelemetry Go for an app that builds with an older Go. For each Go version: the newest OpenTelemetry Go
|
|
829
|
+
* release whose go.mod allows it, and the contrib release made with it.
|
|
830
|
+
* Checked on proxy.golang.org on 18 Oct 2026, and built with each Go in Docker. otel.go needs Go 1.23 or later,
|
|
831
|
+
* because it reads http.Request.Pattern.
|
|
832
|
+
*/
|
|
833
|
+
export const GO_RELEASES = [
|
|
834
|
+
{ go: '1.25', otel: 'v1.46.0', contrib: 'v0.71.0' },
|
|
835
|
+
{ go: '1.24', otel: 'v1.41.0', contrib: 'v0.66.0' },
|
|
836
|
+
{ go: '1.23', otel: 'v1.38.0', contrib: 'v0.63.0' },
|
|
837
|
+
]
|
|
838
|
+
|
|
839
|
+
/** echo-otel needs Go 1.25. With an older Go, Echo v4 uses the older contrib middleware otelecho. */
|
|
840
|
+
export const GO_ECHO = { module: 'github.com/labstack/echo-otel/v4', go: '1.25', older: 'go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho' }
|
|
841
|
+
export const GO_GORM = 'gorm.io/plugin/opentelemetry'
|
|
842
|
+
export const GO_REDIS = 'github.com/redis/go-redis/extra/redisotel/v9'
|
|
843
|
+
const goOlder = (release ) => Number(release.go.split('.')[1]) < Number(GO_ECHO.go.split('.')[1])
|
|
844
|
+
|
|
845
|
+
/** The go get lines. Without a release, go get takes the newest modules. With one, each module gets its version. */
|
|
846
|
+
export function goInstall(release) {
|
|
847
|
+
const otel = (module ) => (release ? `${module}@${release.otel}` : module)
|
|
848
|
+
const contrib = (module ) => (release ? `${module}@${release.contrib}` : module)
|
|
849
|
+
const echo = !release ? GO_ECHO.module : goOlder(release) ? contrib(GO_ECHO.older) : `${GO_ECHO.module}@v4.0.0`
|
|
850
|
+
return [
|
|
851
|
+
`go get ${otel('go.opentelemetry.io/otel')} \\`,
|
|
852
|
+
` ${otel('go.opentelemetry.io/otel/sdk')} \\`,
|
|
853
|
+
` ${otel('go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp')} \\`,
|
|
854
|
+
` ${contrib('go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp')}`,
|
|
855
|
+
'# Gin only',
|
|
856
|
+
`go get ${contrib('go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin')}`,
|
|
857
|
+
'# Echo v4 only',
|
|
858
|
+
`go get ${echo}`,
|
|
859
|
+
].join('\n')
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** The go-redis version of go.mod, for the redisotel line of the docs. The setup tool prints the version itself. */
|
|
863
|
+
export const GO_REDIS_VERSION = "$(go list -m -f '{{.Version}}' github.com/redis/go-redis/v9)"
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Spans for each database query and each Redis command. The setup tool prints the one that the app uses, at a
|
|
867
|
+
* version that does not raise the gorm or go-redis of go.mod. Checked on proxy.golang.org on 18 Oct 2026, with go get,
|
|
868
|
+
* go mod tidy and go build in golang:1.24-alpine: the plugin raises gorm to the gorm in its module graph.
|
|
869
|
+
*/
|
|
870
|
+
export const GO_LIBRARIES = {
|
|
871
|
+
gorm: {
|
|
872
|
+
module: GO_GORM,
|
|
873
|
+
/** Newest first: each plugin version, the lowest gorm it works with (it raises gorm to that), and its Go. */
|
|
874
|
+
versions: [
|
|
875
|
+
{ version: 'v0.1.16', gorm: 'v1.30.0', go: '1.22' },
|
|
876
|
+
{ version: 'v0.1.14', gorm: 'v1.25.10', go: '1.22' },
|
|
877
|
+
{ version: 'v0.1.12', gorm: 'v1.25.1', go: '1.22' },
|
|
878
|
+
],
|
|
879
|
+
note: `For gorm, run go get ${GO_GORM} at the version for the gorm of your go.mod: v0.1.16 for gorm v1.30.0 or later, v0.1.14 for gorm v1.25.10 or later, and v0.1.12 for gorm v1.25.1 or later. A newer plugin version also raises the gorm version in go.mod. After gorm.Open, call db.Use(tracing.NewPlugin(tracing.WithoutMetrics())). Import ${GO_GORM}/tracing. Each query then gets its own span.`,
|
|
880
|
+
/** The setup tool adds this when it cannot read the gorm version of go.mod. It fills in {gorm}. */
|
|
881
|
+
raises: 'This also raises gorm in go.mod to {gorm}.',
|
|
882
|
+
/** The setup tool says this instead of the go get line when the gorm of go.mod is older than every row. */
|
|
883
|
+
tooOld: 'This app uses gorm {gorm}. The gorm plugin needs gorm v1.25.1 or later, so this tool leaves it out. The other spans work without it. To get a span for each query, raise gorm to v1.25.1 or later first. Then run npx @uknows/setup again.',
|
|
884
|
+
},
|
|
885
|
+
redis: {
|
|
886
|
+
module: GO_REDIS,
|
|
887
|
+
/**
|
|
888
|
+
* The redisotel/v9 versions on proxy.golang.org, oldest first. redisotel v9.X needs go-redis v9.X, and some
|
|
889
|
+
* go-redis releases have no redisotel tag. The setup tool takes the newest one at or below the go-redis of go.mod.
|
|
890
|
+
*/
|
|
891
|
+
versions: ['v9.0.0', 'v9.0.1', 'v9.0.2', 'v9.0.4', 'v9.0.5', 'v9.5.3', 'v9.5.5', 'v9.6.2', 'v9.6.3', 'v9.7.0', 'v9.7.1', 'v9.7.2', 'v9.7.3',
|
|
892
|
+
'v9.8.0', 'v9.9.0', 'v9.10.0', 'v9.11.0', 'v9.12.0', 'v9.12.1', 'v9.13.0', 'v9.14.0', 'v9.14.1', 'v9.15.1', 'v9.16.0', 'v9.17.0', 'v9.17.1',
|
|
893
|
+
'v9.17.2', 'v9.17.3', 'v9.18.0', 'v9.19.0', 'v9.20.0', 'v9.20.1', 'v9.21.0', 'v9.22.0', 'v9.23.0'],
|
|
894
|
+
note: `For go-redis v9, run go get ${GO_REDIS} at the go-redis version of your go.mod, as in go get ${GO_REDIS}@${GO_REDIS_VERSION}. A newer redisotel also raises the go-redis version in go.mod. For example, go-redis v9.18.0 sends an extra command when it connects, and Redis 7 answers it with an error, so each new connection shows an error span. After redis.NewClient, call redisotel.InstrumentTracing(rdb). Each Redis command then gets its own span.`,
|
|
895
|
+
},
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** The Echo v4 code for Go below 1.25. */
|
|
899
|
+
export const GO_ECHO_OLDER = `For Echo v4 with Go below ${GO_ECHO.go}, use otelecho instead of echo-otel. echo-otel needs Go ${GO_ECHO.go}. Import ${GO_ECHO.older}. Then use e.Use(otelecho.Middleware(os.Getenv("OTEL_SERVICE_NAME"))).`
|
|
900
|
+
|
|
901
|
+
/** The step for an app that builds with Go 1.23, 1.24 or 1.25. The setup tool prints the lines for the Go of the app. */
|
|
902
|
+
export const GO_OLDER = {
|
|
903
|
+
title: 'Your app builds with Go 1.23, 1.24 or 1.25?',
|
|
904
|
+
summary: 'The newest OpenTelemetry Go modules need Go 1.26. Use the versions for your Go instead. Then go get does not raise the go line of go.mod, and the build works with the Go of your image.',
|
|
905
|
+
steps: [
|
|
906
|
+
'Use the go get lines for the Go version of your build image.',
|
|
907
|
+
'The code in otel.go and main.go stays the same.',
|
|
908
|
+
GO_ECHO_OLDER,
|
|
909
|
+
'For gorm and go-redis, add the versions that fit your go.mod, as the notes for gorm and go-redis say.',
|
|
910
|
+
],
|
|
911
|
+
echo: GO_ECHO_OLDER,
|
|
912
|
+
/** What the setup tool says in its plan. It fills in {version}. */
|
|
913
|
+
plan: 'This app builds with Go {version}. The newest OpenTelemetry Go modules need Go 1.26, so the go get lines name the versions for Go {version}.',
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const GO_OLDER_OPTION = {
|
|
917
|
+
title: GO_OLDER.title,
|
|
918
|
+
summary: GO_OLDER.summary,
|
|
919
|
+
steps: GO_OLDER.steps,
|
|
920
|
+
snippets: GO_RELEASES.map((release) => ({
|
|
921
|
+
caption: `Go ${release.go}`,
|
|
922
|
+
language: 'bash',
|
|
923
|
+
code: [goInstall(release), '# go-redis v9 only: redisotel at the go-redis version of go.mod', `go get ${GO_REDIS}@${GO_REDIS_VERSION}`].join('\n'),
|
|
924
|
+
})),
|
|
925
|
+
}
|
|
237
926
|
|
|
238
927
|
export const RUBY_INITIALIZER = `# UKNOWS monitoring. The OTEL_ settings configure it.
|
|
239
928
|
unless ENV["OTEL_SDK_DISABLED"] == "true"
|
|
@@ -264,6 +953,229 @@ export const COLLECTOR_CONFIG = [
|
|
|
264
953
|
/** UKNOWS counts the spans it gets. It does not scale them up by a sample rate. */
|
|
265
954
|
export const SAMPLING_NOTE = 'Keep sampling off. UKNOWS counts the requests that it gets. With OTEL_TRACES_SAMPLER=traceidratio and OTEL_TRACES_SAMPLER_ARG=0.1, UKNOWS shows 1 in 10 of your requests. Remove both settings, or set OTEL_TRACES_SAMPLER=parentbased_always_on.'
|
|
266
955
|
|
|
956
|
+
/** Every OTLP exporter in the app reads OTEL_EXPORTER_OTLP_HEADERS, and Phoenix falls back to it. */
|
|
957
|
+
export const OTHER_EXPORTERS_NOTE = 'Every OTLP exporter in your app reads OTEL_EXPORTER_OTLP_HEADERS, so it gets your UKNOWS key. If your app also sends traces to Phoenix, set PHOENIX_COLLECTOR_ENDPOINT and PHOENIX_API_KEY for it. Then Phoenix gets its own address and key.'
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* What the setup tool says when the app uses Phoenix and the repo does not give Phoenix its own address. Phoenix
|
|
961
|
+
* register() falls back to OTEL_EXPORTER_OTLP_ENDPOINT. Seen on 18 Oct 2026 with arize-phoenix-otel 0.17.2: it
|
|
962
|
+
* then sent to the UKNOWS host on port 4317, and each span end waited about 20 seconds. The page does not show this.
|
|
963
|
+
*/
|
|
964
|
+
export const PHOENIX_STEP = {
|
|
965
|
+
title: 'Give Phoenix its own address',
|
|
966
|
+
lines: [
|
|
967
|
+
'Phoenix sends its traces to OTEL_EXPORTER_OTLP_ENDPOINT when PHOENIX_COLLECTOR_ENDPOINT is not set. After this setup, that is the UKNOWS address, which refuses them, and your app can wait for each span.',
|
|
968
|
+
'Set PHOENIX_COLLECTOR_ENDPOINT to the address of your Phoenix where the app runs, and set PHOENIX_API_KEY for it. Or pass the address to register() in your code.',
|
|
969
|
+
],
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Phoenix register() with no headers and no PHOENIX_API_KEY makes its exporter read the OTLP headers settings, which
|
|
974
|
+
* hold the UKNOWS key after this setup. Seen in the bench review of 18 Oct 2026. The page does not show this.
|
|
975
|
+
*/
|
|
976
|
+
export const PHOENIX_KEY_STEP = {
|
|
977
|
+
title: 'Give Phoenix its own key',
|
|
978
|
+
lines: [
|
|
979
|
+
'When the app gives Phoenix no key, Phoenix sends its traces with the headers in OTEL_EXPORTER_OTLP_TRACES_HEADERS or OTEL_EXPORTER_OTLP_HEADERS. After this setup, that is your UKNOWS key, so your Phoenix server would get it.',
|
|
980
|
+
'Set PHOENIX_API_KEY where the app runs, or pass headers to register() in your code.',
|
|
981
|
+
],
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** For an app that already uses OpenTelemetry: send only its traces to UKNOWS. */
|
|
985
|
+
export const TRACES_ONLY_NOTE = `For Node.js, Python and Java, set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=${INGEST_ENDPOINT}/v1/traces and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf instead of OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL. Their OpenTelemetry SDKs can send metrics and logs over OTLP by default, and UKNOWS refuses metrics. Then your metrics and logs keep going where they go now. Set your key as OTEL_EXPORTER_OTLP_TRACES_HEADERS instead of OTEL_EXPORTER_OTLP_HEADERS too, so only the traces exporter sends it. Do the same in other languages when your app also sends metrics or logs over OTLP.`
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* What the setup tool says when it uses the traces-only settings. It fills in {where} and {lang}. The page does
|
|
989
|
+
* not show these.
|
|
990
|
+
*/
|
|
991
|
+
export const TRACES_ONLY_PLAN = {
|
|
992
|
+
code: '{where} also sends metrics or logs over OTLP.',
|
|
993
|
+
sdk: 'The OpenTelemetry SDK for {lang} can send metrics and logs over OTLP by default.',
|
|
994
|
+
set: 'This tool sets OTEL_EXPORTER_OTLP_TRACES_ENDPOINT and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, so only traces go to UKNOWS. Your metrics and logs keep their current address.',
|
|
995
|
+
/** The key name for these apps, and the advice that goes with it. */
|
|
996
|
+
key: 'Your key goes in OTEL_EXPORTER_OTLP_TRACES_HEADERS, not in OTEL_EXPORTER_OTLP_HEADERS, so only the traces exporter sends it. The key steps below use that name.',
|
|
997
|
+
keyTitle: 'Keep your UKNOWS key on the traces exporter',
|
|
998
|
+
keyCheck: 'If your code gives the OTLP exporter of metrics or logs its own headers, check that they never hold the UKNOWS key.',
|
|
999
|
+
/** For a .NET, Go, PHP or Ruby app with no metrics or logs over OTLP in its code. It fills in {endpoint}. */
|
|
1000
|
+
none: 'If your app also sends metrics or logs over OTLP, set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT={endpoint}/v1/traces and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf instead of OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL, and set your key as OTEL_EXPORTER_OTLP_TRACES_HEADERS. UKNOWS refuses metrics.',
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* .NET AddOtlpExporter() reads only the general OTLP settings, not the OTEL_EXPORTER_OTLP_TRACES_ ones (only
|
|
1005
|
+
* UseOtlpExporter() reads those). With the traces-only settings, the traces exporter takes the address in code.
|
|
1006
|
+
*/
|
|
1007
|
+
export const DOTNET_TRACES_ONLY = {
|
|
1008
|
+
note: 'For .NET, AddOtlpExporter() does not read the OTEL_EXPORTER_OTLP_TRACES_ settings. UseOtlpExporter() reads them. If your app uses AddOtlpExporter(), pass the traces address and the key on in the code of the traces exporter.',
|
|
1009
|
+
code: `using OpenTelemetry.Exporter;
|
|
1010
|
+
|
|
1011
|
+
// In WithTracing(...):
|
|
1012
|
+
.AddOtlpExporter(otlp =>
|
|
1013
|
+
{
|
|
1014
|
+
// AddOtlpExporter does not read the OTEL_EXPORTER_OTLP_TRACES_ settings, so pass the address and the key on.
|
|
1015
|
+
if (Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") is { Length: > 0 } endpoint)
|
|
1016
|
+
{
|
|
1017
|
+
otlp.Endpoint = new Uri(endpoint);
|
|
1018
|
+
otlp.Protocol = OtlpExportProtocol.HttpProtobuf;
|
|
1019
|
+
otlp.Headers = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_TRACES_HEADERS") ?? string.Empty;
|
|
1020
|
+
}
|
|
1021
|
+
})`,
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* The block for configure_azure_monitor at module level: 2 blank lines before the call, as PEP 8 wants.
|
|
1026
|
+
* The ${'\n'} line keeps them, because tools/sync-contract.mjs folds 3 new lines into 2. A nested
|
|
1027
|
+
* function, so it never shadows the os or trace names of the app. Inside a function, indent it to the call.
|
|
1028
|
+
*/
|
|
1029
|
+
export const AZURE_PYTHON_BLOCK = `# Also send these traces to UKNOWS.
|
|
1030
|
+
def _send_traces_to_uknows():
|
|
1031
|
+
import os
|
|
1032
|
+
|
|
1033
|
+
if not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
|
1034
|
+
return
|
|
1035
|
+
from opentelemetry import trace
|
|
1036
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
1037
|
+
OTLPSpanExporter,
|
|
1038
|
+
)
|
|
1039
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
1040
|
+
|
|
1041
|
+
provider = trace.get_tracer_provider()
|
|
1042
|
+
if hasattr(provider, "add_span_processor"):
|
|
1043
|
+
exporter = OTLPSpanExporter()
|
|
1044
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
1045
|
+
${'\n'}
|
|
1046
|
+
_send_traces_to_uknows()`
|
|
1047
|
+
|
|
1048
|
+
export const AZURE_NODE_CODE = `// Also send these traces to UKNOWS. Keep your other options.
|
|
1049
|
+
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base')
|
|
1050
|
+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto')
|
|
1051
|
+
|
|
1052
|
+
useAzureMonitor({
|
|
1053
|
+
// ...your options, such as azureMonitorExporterOptions
|
|
1054
|
+
spanProcessors: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ? [new BatchSpanProcessor(new OTLPTraceExporter())] : [],
|
|
1055
|
+
})`
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Apps that use Azure Monitor (App Insights) keep it. UKNOWS adds 1 OTLP
|
|
1059
|
+
* exporter to the same tracer provider, so nothing is traced twice. The setup
|
|
1060
|
+
* tool inserts this code and these settings word for word.
|
|
1061
|
+
*/
|
|
1062
|
+
export const AZURE_MONITOR = {
|
|
1063
|
+
/** Setup only, after the .NET note of the otel stack: the pages name UKNOWS only. */
|
|
1064
|
+
otelNote: 'If your app uses Azure Monitor, keep it. Setup adds UKNOWS beside it.',
|
|
1065
|
+
title: 'Your app uses Azure Monitor? Keep it and add UKNOWS',
|
|
1066
|
+
summary: 'Your App Insights setup stays as it is. UKNOWS adds 1 exporter to the same traces, so nothing is traced twice. Add 1 package and a few lines of code for your language, and 3 settings.',
|
|
1067
|
+
steps: [
|
|
1068
|
+
'Add the OTLP exporter package for your language. Use the OpenTelemetry version that your app has now.',
|
|
1069
|
+
'Add the code for your language after the line that starts Azure Monitor.',
|
|
1070
|
+
'Add the 3 settings. Set OTEL_SERVICE_NAME only if your app does not set it now.',
|
|
1071
|
+
'Keep APPLICATIONINSIGHTS_CONNECTION_STRING. Do not add opentelemetry-instrument, NODE_OPTIONS or instrument.sh.',
|
|
1072
|
+
],
|
|
1073
|
+
python: {
|
|
1074
|
+
/** The call that starts Azure Monitor. The block goes after its statement. */
|
|
1075
|
+
call: 'configure_azure_monitor(',
|
|
1076
|
+
package: 'opentelemetry-exporter-otlp-proto-http',
|
|
1077
|
+
code: AZURE_PYTHON_BLOCK,
|
|
1078
|
+
/** The web frameworks whose requests azure-monitor-opentelemetry traces. */
|
|
1079
|
+
traced: ['django', 'fastapi', 'flask'],
|
|
1080
|
+
/**
|
|
1081
|
+
* Web frameworks that it does not trace, the requirement names that show them, and the OpenTelemetry
|
|
1082
|
+
* instrumentation to add. FastMCP and the mcp SDK serve HTTP with Starlette.
|
|
1083
|
+
*/
|
|
1084
|
+
servers: [
|
|
1085
|
+
{ label: 'Starlette', names: ['starlette', 'fastmcp', 'mcp'], package: 'opentelemetry-instrumentation-starlette', module: 'opentelemetry.instrumentation.starlette', instrumentor: 'StarletteInstrumentor' },
|
|
1086
|
+
{ label: 'aiohttp', names: ['aiohttp'], package: 'opentelemetry-instrumentation-aiohttp-server', module: 'opentelemetry.instrumentation.aiohttp_server', instrumentor: 'AioHttpServerInstrumentor' },
|
|
1087
|
+
{ label: 'Tornado', names: ['tornado'], package: 'opentelemetry-instrumentation-tornado', module: 'opentelemetry.instrumentation.tornado', instrumentor: 'TornadoInstrumentor' },
|
|
1088
|
+
{ label: 'Falcon', names: ['falcon'], package: 'opentelemetry-instrumentation-falcon', module: 'opentelemetry.instrumentation.falcon', instrumentor: 'FalconInstrumentor' },
|
|
1089
|
+
{ label: 'Pyramid', names: ['pyramid'], package: 'opentelemetry-instrumentation-pyramid', module: 'opentelemetry.instrumentation.pyramid', instrumentor: 'PyramidInstrumentor' },
|
|
1090
|
+
],
|
|
1091
|
+
/** The required step for those apps. The setup tool fills in each {name}. */
|
|
1092
|
+
untraced: {
|
|
1093
|
+
title: 'Trace the requests of your {label} app',
|
|
1094
|
+
found: '{file} line {line} lists {name}.',
|
|
1095
|
+
builtOn: '{name} serves HTTP with {label}.',
|
|
1096
|
+
why: 'Azure Monitor traces the requests of Django, FastAPI and Flask apps only. Without the step below, App Insights and UKNOWS get no spans for the requests of this app.',
|
|
1097
|
+
add: 'Add {package} to {manifest}. Use the version of your other opentelemetry-instrumentation packages{version}.',
|
|
1098
|
+
call: 'Put the code below at the top of the file that starts the app, before its other imports. instrument() must run before the app imports {name}, or it does not trace the requests.',
|
|
1099
|
+
caption: 'Python: at the top of the file that starts the app',
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
dotnet: {
|
|
1103
|
+
call: 'UseAzureMonitor(',
|
|
1104
|
+
package: 'OpenTelemetry.Exporter.OpenTelemetryProtocol',
|
|
1105
|
+
using: 'using OpenTelemetry.Trace;',
|
|
1106
|
+
/** The 1 line goes after this line. */
|
|
1107
|
+
after: 'var builder = WebApplication.CreateBuilder(args);',
|
|
1108
|
+
/**
|
|
1109
|
+
* The overload without the service provider. It runs when the services are added, so AddOtlpExporter can add
|
|
1110
|
+
* its own services. With (sp, tracing) the app stops at start: services cannot be added after the provider is built.
|
|
1111
|
+
* Only with OTEL_EXPORTER_OTLP_ENDPOINT, as the Python and Node.js code: without it, AddOtlpExporter would send to
|
|
1112
|
+
* localhost, for example in tests. Removing that setting turns UKNOWS off and keeps App Insights.
|
|
1113
|
+
*/
|
|
1114
|
+
line: 'if (System.Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") is { Length: > 0 }) builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());',
|
|
1115
|
+
},
|
|
1116
|
+
node: {
|
|
1117
|
+
call: 'useAzureMonitor(',
|
|
1118
|
+
packages: ['@opentelemetry/exporter-trace-otlp-proto', '@opentelemetry/sdk-trace-base'],
|
|
1119
|
+
code: AZURE_NODE_CODE,
|
|
1120
|
+
/** For apps that start Azure Monitor with the applicationinsights 3.x package. */
|
|
1121
|
+
shim: 'The applicationinsights 3.x package runs on @azure/monitor-opentelemetry. To add the UKNOWS processor, start Azure Monitor with useAzureMonitor from @azure/monitor-opentelemetry, as in the code.',
|
|
1122
|
+
},
|
|
1123
|
+
/** The 3 settings. OTEL_SERVICE_NAME only when the app does not set it. */
|
|
1124
|
+
settings: SHARED_SETTINGS,
|
|
1125
|
+
roleName: 'OTEL_SERVICE_NAME also sets the Cloud Role Name in App Insights. Alerts and the application map use that name. Set it only when your app does not set it now.',
|
|
1126
|
+
sampling: 'Azure Monitor samples traces, by default 5 traces a second. UKNOWS gets the same traces, so it shows fewer requests for a busy app. A sampling_ratio or traces_per_second in your code, or OTEL_TRACES_SAMPLER, sets other sampling. To send every trace, set OTEL_TRACES_SAMPLER=parentbased_always_on. Then App Insights gets every trace too, which can cost more.',
|
|
1127
|
+
notes: [
|
|
1128
|
+
'Your metrics and logs keep going to App Insights. These settings do not turn them off.',
|
|
1129
|
+
'To turn UKNOWS off and keep App Insights, remove OTEL_EXPORTER_OTLP_ENDPOINT and restart the app. The code above sends to UKNOWS only when that setting is there. Do not use OTEL_SDK_DISABLED=true: App Insights uses the same OpenTelemetry, so it stops too.',
|
|
1130
|
+
'For Python, Azure Monitor traces the requests of Django, FastAPI and Flask apps only. For Starlette, FastMCP, aiohttp, Tornado, Falcon or Pyramid, add the OpenTelemetry instrumentation of that framework. Call its instrument() at the top of the file that starts the app, before the app imports the framework.',
|
|
1131
|
+
'For Node.js, the code shows the spanProcessors option. If your code already passes spanProcessors, add the UKNOWS processor to that list.',
|
|
1132
|
+
'For .NET, the line needs the OpenTelemetry.Trace namespace. Put the using line with the other using lines.',
|
|
1133
|
+
],
|
|
1134
|
+
/**
|
|
1135
|
+
* What the setup tool says in its plan for these apps. It fills in {name}, {file}, {line} and {value}.
|
|
1136
|
+
* The page does not show these.
|
|
1137
|
+
*/
|
|
1138
|
+
plan: {
|
|
1139
|
+
/** 1 sentence on this mode, in the plan of each Azure Monitor app. */
|
|
1140
|
+
mode: 'This app uses Azure Monitor (App Insights). This tool keeps it and adds 1 exporter beside it, so the same traces also go to UKNOWS. App Insights keeps getting the traces, metrics and logs that it gets now.',
|
|
1141
|
+
roleName: {
|
|
1142
|
+
title: 'App Insights Cloud Role Name',
|
|
1143
|
+
set: [
|
|
1144
|
+
'This tool sets OTEL_SERVICE_NAME={name}. Azure Monitor uses that name as the Cloud Role Name of this app, so App Insights shows the app as {name}.',
|
|
1145
|
+
'Alerts, workbooks and the application map that use the current role name stop matching when the name changes. To see the current name, run the query below in Logs in your App Insights resource.',
|
|
1146
|
+
'To keep the current name, run npx @uknows/setup again with --service-name and that name. It takes lowercase letters, digits and -.',
|
|
1147
|
+
],
|
|
1148
|
+
query: 'requests | summarize count() by cloud_RoleName',
|
|
1149
|
+
kept: '{where} sets the service name already. This tool keeps it, so the Cloud Role Name in App Insights stays the same.',
|
|
1150
|
+
},
|
|
1151
|
+
/** For the Keep line of AZURE_MONITOR.steps, per stack. The tool fills in {start}. */
|
|
1152
|
+
keep: 'Keep APPLICATIONINSIGHTS_CONNECTION_STRING. Do not add {start}.',
|
|
1153
|
+
start: { python: 'opentelemetry-instrument', dotnet: 'instrument.sh', node: 'NODE_OPTIONS' },
|
|
1154
|
+
samplingDefault: 'This app uses the default sampling of Azure Monitor: 5 traces a second.',
|
|
1155
|
+
samplingCode: '{file} line {line} sets the sampling of Azure Monitor: {value}.',
|
|
1156
|
+
samplingEnv: '{file} sets OTEL_TRACES_SAMPLER to {value}.',
|
|
1157
|
+
/** The rest of the sampling advice, per stack. The tool fills in {options}. */
|
|
1158
|
+
sampling: [
|
|
1159
|
+
'UKNOWS gets the same sampled traces, so it shows fewer requests than a busy app gets.',
|
|
1160
|
+
'{options} in your code, or OTEL_TRACES_SAMPLER, sets other sampling.',
|
|
1161
|
+
'To send every trace, set OTEL_TRACES_SAMPLER=parentbased_always_on. Then App Insights gets every trace too, which can cost more.',
|
|
1162
|
+
],
|
|
1163
|
+
samplingOptions: { python: 'A sampling_ratio or traces_per_second', dotnet: 'A SamplingRatio or TracesPerSecond option', node: 'A samplingRatio or tracesPerSecond option' },
|
|
1164
|
+
/**
|
|
1165
|
+
* How the exporter version follows the SDK. A uv or Poetry project gets a lower limit, so its resolver keeps the
|
|
1166
|
+
* exporter at the SDK version, also after a later Azure Monitor upgrade. A requirements file gets the exact pin
|
|
1167
|
+
* of its other lines. The tool fills in {file}, {package}, {version} and {tool}.
|
|
1168
|
+
*/
|
|
1169
|
+
floor: '{file} gets {package}>={version}, a lower limit, so {tool} keeps it at the opentelemetry-sdk version of the app, also when you upgrade Azure Monitor later.',
|
|
1170
|
+
pin: '{file} gets {package}=={version}, the opentelemetry-sdk version of the app. When you upgrade Azure Monitor later, raise {package} to the new opentelemetry-sdk version too. They have the same version.',
|
|
1171
|
+
/** The plan sentence for the Python code. The tool fills in {count}, {call}, {file} and {line}. */
|
|
1172
|
+
pythonCode: 'Also send the Azure Monitor traces to UKNOWS: add {count} lines of code after the {call}...) call in {file} line {line}. The diff below shows them.',
|
|
1173
|
+
browser: '{file} uses {value}. It runs in the browser, so the setup tool does not change it.',
|
|
1174
|
+
connectionString: '{file} sets APPLICATIONINSIGHTS_CONNECTION_STRING, but the app has no Azure Monitor package. The setup tool adds standard OpenTelemetry.',
|
|
1175
|
+
autoAttach: '{file} sets AZURE_MONITOR_AUTO_ATTACH. It attaches Azure Monitor only on Azure App Service, so the setup tool adds standard OpenTelemetry.',
|
|
1176
|
+
},
|
|
1177
|
+
}
|
|
1178
|
+
|
|
267
1179
|
export const STACKS = [
|
|
268
1180
|
{
|
|
269
1181
|
id: 'node',
|
|
@@ -276,35 +1188,66 @@ export const STACKS = [
|
|
|
276
1188
|
dockerfile: [
|
|
277
1189
|
'# Final stage, after your app files and node_modules',
|
|
278
1190
|
`ENV NODE_OPTIONS="--require ${NODE_REGISTER}"`,
|
|
279
|
-
|
|
1191
|
+
dockerEnv([NODE_DETECTORS]),
|
|
280
1192
|
'CMD ["node", "server.js"]',
|
|
281
1193
|
].join('\n'),
|
|
282
1194
|
start: `node --require ${NODE_REGISTER} server.js`,
|
|
283
1195
|
// --require= has no space. With a space, a shell that loads otel.env runs the package path as a command.
|
|
284
1196
|
platformEnv: [`NODE_OPTIONS=--require=${NODE_REGISTER}`],
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
steps: [
|
|
289
|
-
'Add 1 more package: @opentelemetry/instrumentation.',
|
|
290
|
-
'Load OpenTelemetry with the 2 options below, in your Dockerfile, your start line or the NODE_OPTIONS setting on your platform.',
|
|
291
|
-
'Node prints an ExperimentalWarning about --experimental-loader when the app starts. You can ignore it.',
|
|
292
|
-
],
|
|
293
|
-
snippets: [
|
|
294
|
-
{ caption: 'Terminal: 1 more package', language: 'bash', code: 'npm install --save @opentelemetry/instrumentation' },
|
|
295
|
-
{ caption: 'Dockerfile for ES modules', language: 'dockerfile', code: `ENV NODE_OPTIONS="${NODE_ESM_OPTIONS}"` },
|
|
296
|
-
{ caption: 'Start line for ES modules', language: 'bash', code: `node ${NODE_ESM_OPTIONS} server.mjs` },
|
|
297
|
-
{ caption: 'NODE_OPTIONS on your platform', language: 'text', code: NODE_ESM_OPTIONS },
|
|
298
|
-
],
|
|
1197
|
+
extraSettings: {
|
|
1198
|
+
lines: [NODE_DETECTORS],
|
|
1199
|
+
why: 'OpenTelemetry then asks no cloud metadata address when the app starts.',
|
|
299
1200
|
},
|
|
1201
|
+
options: [
|
|
1202
|
+
{
|
|
1203
|
+
title: 'Your app uses ES modules?',
|
|
1204
|
+
summary: 'Use these lines when package.json has "type": "module", or when your app starts from an .mjs file. With the --require line alone, UKNOWS gets each request without its route.',
|
|
1205
|
+
steps: [
|
|
1206
|
+
'Add 1 more package: @opentelemetry/instrumentation.',
|
|
1207
|
+
'Load OpenTelemetry with the 2 options below, in your Dockerfile, your start line or the NODE_OPTIONS setting on your platform.',
|
|
1208
|
+
'Node prints an ExperimentalWarning about --experimental-loader when the app starts. You can ignore it.',
|
|
1209
|
+
],
|
|
1210
|
+
snippets: [
|
|
1211
|
+
{ caption: 'Terminal: 1 more package', language: 'bash', code: 'npm install --save @opentelemetry/instrumentation' },
|
|
1212
|
+
{ caption: 'Dockerfile for ES modules', language: 'dockerfile', code: `ENV NODE_OPTIONS="${NODE_ESM_OPTIONS}"` },
|
|
1213
|
+
{ caption: 'Start line for ES modules', language: 'bash', code: `node ${NODE_ESM_OPTIONS} server.mjs` },
|
|
1214
|
+
{ caption: 'NODE_OPTIONS on your platform', language: 'text', code: NODE_ESM_OPTIONS },
|
|
1215
|
+
],
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
title: 'Stops cleanly on SIGTERM: add a start file',
|
|
1219
|
+
summary: 'OpenTelemetry listens for SIGTERM to send the last data. Because of that, Node no longer stops on SIGTERM by itself, and Docker, Kubernetes and systemd wait for their time limit and then kill the app. This start file sends the last data, then stops the app as Node does. An app with its own SIGTERM handler keeps it. The setup tool adds the same file.',
|
|
1220
|
+
steps: [
|
|
1221
|
+
`Add the file ${NODE_REGISTER_FILE.path} next to package.json.`,
|
|
1222
|
+
'In the final stage of your Dockerfile, copy the file into the folder of your app. Load it with --require instead of the register line.',
|
|
1223
|
+
`Without Docker, start the app with node --require ./${NODE_REGISTER_FILE.path} server.js.`,
|
|
1224
|
+
`For ES modules, keep the loader option. Use --import=${NODE_REGISTER_PATH} instead of --import=${NODE_REGISTER}.`,
|
|
1225
|
+
'If a package is missing, the app still starts. It prints the warning "OpenTelemetry did not start".',
|
|
1226
|
+
],
|
|
1227
|
+
snippets: [
|
|
1228
|
+
{ caption: NODE_REGISTER_FILE.path, language: NODE_REGISTER_FILE.language, code: NODE_REGISTER_FILE.content },
|
|
1229
|
+
{
|
|
1230
|
+
caption: 'Dockerfile, final stage',
|
|
1231
|
+
language: 'dockerfile',
|
|
1232
|
+
code: ['WORKDIR /app', `COPY ${NODE_REGISTER_FILE.path} ./`, `ENV NODE_OPTIONS="--require ${NODE_REGISTER_PATH}"`].join('\n'),
|
|
1233
|
+
},
|
|
1234
|
+
{ caption: 'Start line', language: 'bash', code: `node --require ./${NODE_REGISTER_FILE.path} server.js` },
|
|
1235
|
+
{ caption: 'NODE_OPTIONS on your platform', language: 'text', code: `--require=${NODE_REGISTER_PATH}` },
|
|
1236
|
+
],
|
|
1237
|
+
},
|
|
1238
|
+
],
|
|
300
1239
|
notes: [
|
|
301
1240
|
'Replace server.js with the file that starts your server.',
|
|
302
1241
|
'Install the packages as normal dependencies. Your production image must include them.',
|
|
303
1242
|
'If your image already sets NODE_OPTIONS, add the --require option to that value.',
|
|
304
1243
|
'If your app uses ES modules, use the steps in "Your app uses ES modules?". The --require line alone loses the route of each request.',
|
|
305
1244
|
'If your repo uses yarn or pnpm, add the same packages with that tool.',
|
|
306
|
-
|
|
307
|
-
|
|
1245
|
+
// OpenTelemetry for Node.js sends the log records of pino and bunyan (instrumentation-pino, instrumentation-bunyan:
|
|
1246
|
+
// "log sending"). It does not read console.log, and winston needs @opentelemetry/winston-transport, which the UKNOWS
|
|
1247
|
+
// agent image does not have (checked on registry.uknows.my/uknows/agent-node:stable 1.1.1, 25 Oct 2026).
|
|
1248
|
+
'UKNOWS gets the logs that your app writes with pino or bunyan, with the trace id of each request. Lines that your app writes with console.log stay in its own output only.',
|
|
1249
|
+
'With the register line, Node no longer stops on SIGTERM by itself. To stop cleanly, use the start file in "Stops cleanly on SIGTERM".',
|
|
1250
|
+
`Keep ${NODE_DETECTORS} with the 5 settings. Without it, OpenTelemetry asks the metadata addresses of the clouds when the app starts, such as 169.254.169.254 on port 80, and logs a MetadataLookupWarning outside Google Cloud.`,
|
|
308
1251
|
],
|
|
309
1252
|
},
|
|
310
1253
|
{
|
|
@@ -324,19 +1267,44 @@ export const STACKS = [
|
|
|
324
1267
|
dockerfile: [
|
|
325
1268
|
'# requirements.txt now lists the OpenTelemetry packages',
|
|
326
1269
|
'RUN pip install --no-cache-dir -r requirements.txt',
|
|
327
|
-
|
|
1270
|
+
PYTHON_BRIDGE_COPY,
|
|
1271
|
+
dockerEnv([PYTHON_LOG_HANDLER_OFF, PYTHON_DETECTORS, PYTHON_BRIDGE_PATH]),
|
|
328
1272
|
'CMD ["opentelemetry-instrument", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]',
|
|
329
1273
|
].join('\n'),
|
|
330
1274
|
start: 'opentelemetry-instrument uvicorn main:app --host 0.0.0.0 --port 8000',
|
|
331
1275
|
startRequired: 'Python still needs the start line. The settings alone do not start monitoring. Start the app with opentelemetry-instrument, also under systemd, Docker and SAE.',
|
|
1276
|
+
extraSettings: {
|
|
1277
|
+
lines: [PYTHON_LOG_HANDLER_OFF, PYTHON_DETECTORS],
|
|
1278
|
+
why: 'The OpenTelemetry logging package then stays off the root logger, so the app prints what it printed before, and the app sends its host name.',
|
|
1279
|
+
},
|
|
1280
|
+
options: [
|
|
1281
|
+
{
|
|
1282
|
+
title: 'Send the logs: add the UKNOWS log bridge',
|
|
1283
|
+
summary: `The OpenTelemetry logging package sends logs from the root logger, and that changes what your app prints: warnings of a logger with no handler, logging.warning() before any setup and the error lines of Flask stop showing. The bridge sends the same log records and changes nothing in the app output. opentelemetry-instrument loads it from PYTHONPATH. The setup tool adds the same files.`,
|
|
1284
|
+
steps: [
|
|
1285
|
+
`Add the folder ${PYTHON_BRIDGE_DIR} next to requirements.txt or pyproject.toml, with the 3 files below.`,
|
|
1286
|
+
`In the final stage of your Dockerfile, copy the folder to ${PYTHON_BRIDGE_IMAGE} and add it to PYTHONPATH, with ${PYTHON_LOG_HANDLER_OFF}.`,
|
|
1287
|
+
`Without Docker, set PYTHONPATH to the full path of the folder, and keep ${PYTHON_LOG_HANDLER_OFF}.`,
|
|
1288
|
+
'Without the bridge, the app sends no logs. Its output stays as it is.',
|
|
1289
|
+
],
|
|
1290
|
+
snippets: [
|
|
1291
|
+
...PYTHON_LOG_BRIDGE.files.map((file) => ({ caption: `${PYTHON_BRIDGE_DIR}/${file.path}`, language: file.language, code: file.content })),
|
|
1292
|
+
{ caption: 'Dockerfile, final stage', language: 'dockerfile', code: [PYTHON_BRIDGE_COPY, dockerEnv([PYTHON_LOG_HANDLER_OFF, PYTHON_BRIDGE_PATH], [])].join('\n') },
|
|
1293
|
+
],
|
|
1294
|
+
},
|
|
1295
|
+
],
|
|
332
1296
|
notes: [
|
|
333
1297
|
'Use Python 3.10 or later. The OpenTelemetry packages need it.',
|
|
334
1298
|
'Run the install commands in the virtual environment of the app. Install the app packages first, because opentelemetry-bootstrap reads them.',
|
|
1299
|
+
`The install line adds opentelemetry-instrumentation-logging. On its own it puts an OpenTelemetry handler on the root logger, whatever OTEL_LOGS_EXPORTER says, and then the warnings and errors of loggers that have no handler do not show in the app output. ${PYTHON_LOG_HANDLER_OFF} keeps it off the root logger. The UKNOWS log bridge in "Send the logs" sends the logs instead.`,
|
|
1300
|
+
`Keep ${PYTHON_DETECTORS} with the 5 settings. Python then sends the host name of the app, as Node.js does.`,
|
|
335
1301
|
'Put opentelemetry-instrument in front of the command that starts your app now.',
|
|
336
1302
|
'For Django, set DJANGO_SETTINGS_MODULE before the app starts.',
|
|
337
1303
|
'opentelemetry-instrument works with 1 worker. For Gunicorn or uvicorn with more than 1 worker, follow the pre-fork server steps in the OpenTelemetry Python docs.',
|
|
338
1304
|
'Remove the --reload option in production.',
|
|
339
|
-
'If your repo uses
|
|
1305
|
+
'If your repo uses uv or Poetry, add the packages to the project that your image installs, then run uv lock or poetry lock. In a uv workspace, that is the member that uv sync --package names. Commit the lock file.',
|
|
1306
|
+
'If your app needs protobuf below version 5, use opentelemetry-distro==0.48b0 and opentelemetry-exporter-otlp-proto-http==1.27.0. They are the last releases that work with protobuf 4. Later releases need protobuf 5 or later.',
|
|
1307
|
+
'A Procfile starts the app only on a platform that reads it. If yours does, put opentelemetry-instrument before the command on the web line yourself.',
|
|
340
1308
|
],
|
|
341
1309
|
},
|
|
342
1310
|
{
|
|
@@ -357,22 +1325,25 @@ export const STACKS = [
|
|
|
357
1325
|
'# Final stage',
|
|
358
1326
|
'WORKDIR /app',
|
|
359
1327
|
'COPY --from=build /app/publish .',
|
|
360
|
-
dockerEnv([DOTNET_LOG_DIRECTORY]),
|
|
1328
|
+
dockerEnv([DOTNET_LOG_DIRECTORY, DOTNET_LOGS_MESSAGE]),
|
|
361
1329
|
'ENTRYPOINT ["sh", "./instrument.sh", "dotnet", "OrdersApi.dll"]',
|
|
362
1330
|
].join('\n'),
|
|
363
1331
|
start: 'sh ./publish/instrument.sh dotnet ./publish/OrdersApi.dll',
|
|
364
1332
|
startRequired: '.NET still needs the start line. The settings alone do not start monitoring. Start the app with instrument.sh, also under systemd, Docker and SAE.',
|
|
365
1333
|
extraSettings: {
|
|
366
|
-
lines: [DOTNET_LOG_DIRECTORY],
|
|
367
|
-
why: 'OpenTelemetry writes its log files to this folder
|
|
1334
|
+
lines: [DOTNET_LOG_DIRECTORY, DOTNET_LOGS_MESSAGE],
|
|
1335
|
+
why: 'OpenTelemetry writes its log files to this folder, so the app must be able to write to it. The formatted message puts the values of each log line in its text.',
|
|
368
1336
|
},
|
|
369
1337
|
notes: [
|
|
370
1338
|
'Replace OrdersApi.dll with the dll of your app.',
|
|
371
1339
|
'Use .NET 8 or later.',
|
|
372
|
-
'
|
|
1340
|
+
'A runtime identifier in the build is optional. With one, such as -r linux-x64, linux-arm64 or linux-musl-x64 for Alpine images, the publish folder holds the native files of that platform only. Without one, it holds them for every platform, and instrument.sh picks the one it runs on.',
|
|
373
1341
|
'The package puts instrument.sh in the publish folder. The script starts your app with monitoring on.',
|
|
374
1342
|
`Keep ${DOTNET_LOG_DIRECTORY} with the 5 settings. Without a folder that the app can write, the app stops when it starts. The default folder, /var/log/opentelemetry, is not writable on a Mac or in an image that runs as a user that is not root.`,
|
|
375
|
-
|
|
1343
|
+
`Keep ${DOTNET_LOGS_MESSAGE} too. Without it, a log line reaches UKNOWS as its message template, such as "Served order {Id}", without the values.`,
|
|
1344
|
+
'If your app uses StackExchange.Redis, also when a package such as NRedisStack brings it in, the build stops and names 1 more package and its version. Add that package. For OpenTelemetry.AutoInstrumentation 1.17.0, run dotnet add package OpenTelemetry.Instrumentation.StackExchangeRedis --version 1.19.0-beta.1.',
|
|
1345
|
+
'If your repo has packages.lock.json, run dotnet restore --force-evaluate after you add the packages. Use the same -r runtime identifier as your build, for example -r linux-x64. Run it also for each project that references the app and has its own packages.lock.json, such as a test project, with the -r of that lock. Then commit every packages.lock.json that changed.',
|
|
1346
|
+
'If your Dockerfile restores the packages with --network=none, add the new packages to the package source that it uses first.',
|
|
376
1347
|
'To turn monitoring off for .NET, set OTEL_DOTNET_AUTO_TRACES_ENABLED=false.',
|
|
377
1348
|
],
|
|
378
1349
|
},
|
|
@@ -388,22 +1359,34 @@ export const STACKS = [
|
|
|
388
1359
|
].join('\n'),
|
|
389
1360
|
files: [],
|
|
390
1361
|
dockerfile: [
|
|
391
|
-
'# Final stage',
|
|
392
|
-
|
|
1362
|
+
'# Final stage, after your last RUN line',
|
|
1363
|
+
JAVA_COPY_LINE,
|
|
393
1364
|
`ENV JAVA_TOOL_OPTIONS="-javaagent:/otel/${JAVA_AGENT.file}"`,
|
|
394
1365
|
SETTINGS_DOCKERFILE,
|
|
395
1366
|
'CMD ["java", "-jar", "app.jar"]',
|
|
396
1367
|
].join('\n'),
|
|
397
1368
|
start: `java -javaagent:./${JAVA_AGENT.file} -jar app.jar`,
|
|
398
1369
|
platformEnv: [`JAVA_TOOL_OPTIONS=-javaagent:/path/to/${JAVA_AGENT.file}`],
|
|
1370
|
+
options: [
|
|
1371
|
+
{
|
|
1372
|
+
title: 'Your network blocks ghcr.io?',
|
|
1373
|
+
summary: 'Download the agent from GitHub in a build stage instead. The stage checks the SHA-256 checksum of the agent.',
|
|
1374
|
+
steps: [
|
|
1375
|
+
'Add the build stage below before the final stage of your Dockerfile.',
|
|
1376
|
+
'In the final stage, use its COPY line instead of the COPY line from ghcr.io.',
|
|
1377
|
+
'Keep the ENV JAVA_TOOL_OPTIONS line.',
|
|
1378
|
+
],
|
|
1379
|
+
snippets: [{ caption: 'Dockerfile', language: 'dockerfile', code: JAVA_AGENT_STAGE }],
|
|
1380
|
+
},
|
|
1381
|
+
],
|
|
399
1382
|
notes: [
|
|
400
1383
|
`The agent is version ${JAVA_AGENT.version}. Check its SHA-256 checksum before you use it.`,
|
|
401
1384
|
'In the JAVA_TOOL_OPTIONS line, replace /path/to with the folder of the agent.',
|
|
402
1385
|
'On macOS, use shasum -a 256 -c - instead of sha256sum -c -.',
|
|
403
1386
|
'Replace app.jar with your built app. Put -javaagent before -jar. Keep your other JVM options.',
|
|
404
1387
|
'If your image already sets JAVA_TOOL_OPTIONS, add the -javaagent option to that value.',
|
|
405
|
-
|
|
406
|
-
'
|
|
1388
|
+
`The COPY line takes the same agent from the OpenTelemetry image ${JAVA_AGENT.image}. The digest pins the exact image. Docker keeps it in its cache, so a build does not download the agent each time.`,
|
|
1389
|
+
'Put the COPY and ENV lines after the last RUN line of the final stage, so build steps do not load the agent.',
|
|
407
1390
|
'Use Java 8 or later. Use a JVM build. Native images need the OpenTelemetry starter for your framework.',
|
|
408
1391
|
'To turn the agent off completely, set OTEL_JAVAAGENT_ENABLED=false.',
|
|
409
1392
|
],
|
|
@@ -414,16 +1397,7 @@ export const STACKS = [
|
|
|
414
1397
|
summary: 'Add the OpenTelemetry Go SDK and the middleware for your router. Add a short init file.',
|
|
415
1398
|
manifests: ['go.mod'],
|
|
416
1399
|
frameworks: ['net/http', 'Gin', 'Echo', 'chi'],
|
|
417
|
-
install:
|
|
418
|
-
'go get go.opentelemetry.io/otel \\',
|
|
419
|
-
' go.opentelemetry.io/otel/sdk \\',
|
|
420
|
-
' go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \\',
|
|
421
|
-
' go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp',
|
|
422
|
-
'# Gin only',
|
|
423
|
-
'go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin',
|
|
424
|
-
'# Echo v4 only',
|
|
425
|
-
'go get github.com/labstack/echo-otel/v4',
|
|
426
|
-
].join('\n'),
|
|
1400
|
+
install: goInstall(),
|
|
427
1401
|
files: [
|
|
428
1402
|
{ path: 'otel.go', language: 'go', summary: 'Add this file to your main package.', content: GO_INIT },
|
|
429
1403
|
{ path: 'main.go', language: 'go', summary: 'Change main like this. Keep only the middleware for your router.', content: GO_MAIN },
|
|
@@ -434,8 +1408,9 @@ export const STACKS = [
|
|
|
434
1408
|
SETTINGS_DOCKERFILE,
|
|
435
1409
|
].join('\n'),
|
|
436
1410
|
start: `go build -o ${EXAMPLE_SERVICE} . && ./${EXAMPLE_SERVICE}`,
|
|
1411
|
+
options: [GO_OLDER_OPTION],
|
|
437
1412
|
notes: [
|
|
438
|
-
|
|
1413
|
+
`Use Go 1.26 or later. The current OpenTelemetry Go modules need it. With an older Go, the build stops with a Go version error. For Go 1.23, 1.24 or 1.25, use the versions in "${GO_OLDER.title}".`,
|
|
439
1414
|
'Call setupOTel at the start of main. The main.go code stops the server on Ctrl+C or SIGTERM. Then it calls the shutdown function, which sends the last data.',
|
|
440
1415
|
'If your app already stops the server on SIGTERM, keep your code. Call the shutdown function after the server stops and before the app exits.',
|
|
441
1416
|
'Use the middleware for your router. For net/http, wrap the ServeMux with withRoute, then with otelhttp.NewHandler. withRoute adds the route of each request. Gin and Echo add the route on their own.',
|
|
@@ -443,6 +1418,8 @@ export const STACKS = [
|
|
|
443
1418
|
'For another router, call setRoute with the route of the request after the router picks it.',
|
|
444
1419
|
'For outgoing HTTP calls, use otelhttp.NewTransport in your HTTP client. Pass the request context to each call.',
|
|
445
1420
|
'For Echo v5, use github.com/labstack/echo-otel/v5.',
|
|
1421
|
+
GO_LIBRARIES.gorm.note,
|
|
1422
|
+
GO_LIBRARIES.redis.note,
|
|
446
1423
|
'Run go mod tidy. Then run your tests and build.',
|
|
447
1424
|
'These steps add code at startup only. Your business logic stays the same.',
|
|
448
1425
|
],
|
|
@@ -506,32 +1483,37 @@ export const STACKS = [
|
|
|
506
1483
|
{
|
|
507
1484
|
id: 'otel',
|
|
508
1485
|
label: 'I already use OpenTelemetry',
|
|
509
|
-
summary: 'Keep your OpenTelemetry setup. Point
|
|
1486
|
+
summary: 'Keep your OpenTelemetry setup. Point its traces at UKNOWS with 3 of the settings.',
|
|
510
1487
|
manifests: ['package.json', 'requirements.txt', 'pyproject.toml', '*.csproj', 'pom.xml', 'build.gradle', 'go.mod', 'composer.json', 'Gemfile'],
|
|
511
1488
|
frameworks: ['Any app that already lists OpenTelemetry packages: @opentelemetry/*, opentelemetry-*, OpenTelemetry.*, io.opentelemetry, go.opentelemetry.io or open-telemetry/*'],
|
|
512
1489
|
install: '# No new packages. Keep the OpenTelemetry packages that you use now.',
|
|
513
1490
|
files: [],
|
|
514
|
-
dockerfile: ['# Final stage. Your SDK reads these settings at startup.',
|
|
1491
|
+
dockerfile: ['# Final stage. Your SDK reads these settings at startup.', SHARED_SETTINGS_DOCKERFILE].join('\n'),
|
|
515
1492
|
start: '# Keep your start command. Your SDK reads the OTEL_ settings when the app starts.',
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
'
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1493
|
+
settings: SHARED_SETTINGS,
|
|
1494
|
+
warning: 'These settings send your traces to UKNOWS instead of the backend that gets them now. Your metrics and logs keep going where they go now. To keep your current backend for traces too, add UKNOWS to an OpenTelemetry Collector instead. See "Keep your current backend: add UKNOWS to your Collector".',
|
|
1495
|
+
options: [
|
|
1496
|
+
{
|
|
1497
|
+
title: 'Keep your current backend: add UKNOWS to your Collector',
|
|
1498
|
+
summary: 'Your apps keep their settings and send to your OpenTelemetry Collector. The Collector sends traces to your current backend and to UKNOWS.',
|
|
1499
|
+
steps: [
|
|
1500
|
+
'Add the otlphttp/uknows exporter below to your Collector config.',
|
|
1501
|
+
'Add otlphttp/uknows to the exporters of your traces pipeline. Keep the exporters that you have.',
|
|
1502
|
+
`Set UKNOWS_KEY as a secret on the Collector, as "${STEP_TITLES.secret}" says. Use the key only, without x-uknows-key=.`,
|
|
1503
|
+
'Restart the Collector.',
|
|
1504
|
+
'Newer Collectors name this exporter otlp_http. If your Collector says that otlphttp is deprecated, rename otlphttp/uknows to otlp_http/uknows in both places.',
|
|
1505
|
+
],
|
|
1506
|
+
snippets: [{ caption: 'Collector config', language: 'yaml', code: COLLECTOR_CONFIG }],
|
|
1507
|
+
},
|
|
1508
|
+
],
|
|
529
1509
|
notes: [
|
|
530
1510
|
'Remove exporter addresses and headers from your code. Then the OTEL_ settings apply.',
|
|
531
1511
|
'Set OTEL_TRACES_EXPORTER to otlp, or remove it.',
|
|
532
1512
|
'UKNOWS takes OTLP over HTTP. If your code creates a gRPC exporter, change it to the HTTP exporter.',
|
|
533
1513
|
'For Python, use the opentelemetry-exporter-otlp-proto-http package. For Go, use the otlptracehttp exporter.',
|
|
534
|
-
'If you send data through an OpenTelemetry Collector, add UKNOWS to the Collector. The config is in "Keep your current backend".',
|
|
1514
|
+
'If you send data through an OpenTelemetry Collector, add UKNOWS to the Collector. The config is in "Keep your current backend: add UKNOWS to your Collector".',
|
|
1515
|
+
TRACES_ONLY_NOTE,
|
|
1516
|
+
DOTNET_TRACES_ONLY.note,
|
|
535
1517
|
SAMPLING_NOTE,
|
|
536
1518
|
],
|
|
537
1519
|
},
|
|
@@ -543,7 +1525,7 @@ export const STACKS = [
|
|
|
543
1525
|
* systemd and docker run --env-file skip the # lines.
|
|
544
1526
|
*/
|
|
545
1527
|
export function stackSettingsEnv(stack) {
|
|
546
|
-
const lines = [
|
|
1528
|
+
const lines = [stackSettings(stack).map((setting) => `${setting.name}=${setting.value}`).join('\n')]
|
|
547
1529
|
if (stack.extraSettings) lines.push(`# For ${stack.label}: ${stack.extraSettings.why}`, ...stack.extraSettings.lines)
|
|
548
1530
|
if (stack.platformEnv?.length) lines.push(`# For ${stack.label}: this loads OpenTelemetry when the app starts.`, ...stack.platformEnv)
|
|
549
1531
|
return lines.join('\n')
|
|
@@ -551,13 +1533,66 @@ export function stackSettingsEnv(stack) {
|
|
|
551
1533
|
|
|
552
1534
|
/** A stricter way to store the key, shown folded under the host. */
|
|
553
1535
|
|
|
554
|
-
/**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1536
|
+
/**
|
|
1537
|
+
* A check for 1 kind of deploy file. The page lists every check of a host.
|
|
1538
|
+
* The setup tool prints a check when it finds that kind of file in the repo,
|
|
1539
|
+
* with the file and line.
|
|
1540
|
+
*/
|
|
1541
|
+
|
|
1542
|
+
/** How the setup tool and the page name the checks of a host. The tool fills in {where} for the files that it finds. */
|
|
1543
|
+
export const DEPLOY_CHECKS = {
|
|
1544
|
+
title: 'Check your deploy files',
|
|
1545
|
+
found: 'Found in {where}.',
|
|
1546
|
+
}
|
|
1547
|
+
|
|
558
1548
|
/** The path of the key file on a VM or Docker host. Only root can read it. */
|
|
559
1549
|
export const KEY_FILE = '/etc/uknows.env'
|
|
560
1550
|
|
|
1551
|
+
/** A word for a bash script: as it is when it is plain, else in single quotes. */
|
|
1552
|
+
const shellWord = (text ) => (/^[A-Za-z0-9._-]+$/.test(text) ? text : `'${text.replace(/'/g, `'\\''`)}'`)
|
|
1553
|
+
|
|
1554
|
+
/** event.name of the deploy marker of a VM: the coverage watchdog of UKNOWS looks for log records with it. */
|
|
1555
|
+
export const DEPLOY_EVENT = 'uknows.deploy'
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* The deploy marker of a VM: run on the host as the last step of each deploy, it sends 1 log record to UKNOWS. A deploy
|
|
1559
|
+
* inside a machine never reaches the activity log of the cloud, so this record is how UKNOWS knows that a deploy
|
|
1560
|
+
* happened, and can say when one stops the data of the app. The record has event.name uknows.deploy, the service name,
|
|
1561
|
+
* and the Azure resource id of the machine from the Azure instance metadata (exact, never a guess), and the body
|
|
1562
|
+
* "uknows deploy", with "commit <7 to 12 hex characters>" when COMMIT holds one. The key comes from KEY_FILE and is never
|
|
1563
|
+
* an argument of a program: curl reads the header from a pipe. The deploy never fails because of the marker: each problem
|
|
1564
|
+
* prints 1 line and the script ends with 0. RESOURCE_ID, when set, takes the place of the instance metadata; it must be
|
|
1565
|
+
* an Azure resource id of a virtual machine or a scale set instance.
|
|
1566
|
+
*/
|
|
1567
|
+
export function vmDeployMarker(service = EXAMPLE_SERVICE) {
|
|
1568
|
+
return [
|
|
1569
|
+
'# Run in bash on the host, as root or with sudo, as the last step of each deploy.',
|
|
1570
|
+
`SERVICE=${shellWord(service)} # the service name of the app (OTEL_SERVICE_NAME)`,
|
|
1571
|
+
'COMMIT=${COMMIT:-} # optional: the commit that this deploy runs, 7 to 12 hex characters',
|
|
1572
|
+
'(',
|
|
1573
|
+
` KEY_FILE=\${UKNOWS_KEY_FILE:-${KEY_FILE}}`,
|
|
1574
|
+
` ENDPOINT=\${OTEL_EXPORTER_OTLP_ENDPOINT:-${INGEST_ENDPOINT}}`,
|
|
1575
|
+
` KEY="$(sed -n 's/^${SECRET.name}=${SECRET.header}=//p' "$KEY_FILE" 2>/dev/null | head -n 1)"`,
|
|
1576
|
+
' [ -n "$KEY" ] || { echo "No key in $KEY_FILE, so no deploy marker. Do the key step first."; exit 0; }',
|
|
1577
|
+
" ID=\"${RESOURCE_ID:-$(curl -fsS -m 5 -H Metadata:true 'http://169.254.169.254/metadata/instance/compute/resourceId?api-version=2021-02-01&format=text' 2>/dev/null)}\"",
|
|
1578
|
+
' case "$ID" in',
|
|
1579
|
+
' /subscriptions/*/resourceGroups/*/providers/Microsoft.Compute/*) ;;',
|
|
1580
|
+
' *) echo "This is not an Azure VM and RESOURCE_ID is not set, so no deploy marker."; exit 0 ;;',
|
|
1581
|
+
' esac',
|
|
1582
|
+
' case "$SERVICE$ID" in *[!A-Za-z0-9/._\\(\\)-]*) echo "SERVICE or the resource id has other characters, so no deploy marker."; exit 0 ;; esac',
|
|
1583
|
+
' BODY="uknows deploy"',
|
|
1584
|
+
' case "$COMMIT" in ?????????????*|*[!0-9a-f]*|"") ;; ???????*) BODY="uknows deploy commit $COMMIT" ;; esac',
|
|
1585
|
+
' NOW="$(date +%s)000000000"',
|
|
1586
|
+
` attr() { printf '{"key":"%s","value":{"stringValue":"%s"}}' "$1" "$2"; }`,
|
|
1587
|
+
` printf '{"resourceLogs":[{"resource":{"attributes":[%s,%s,%s,%s]},"scopeLogs":[{"scope":{"name":"${DEPLOY_EVENT}"},"logRecords":[{"timeUnixNano":"%s","observedTimeUnixNano":"%s","severityNumber":9,"severityText":"INFO","body":{"stringValue":"%s"},"attributes":[%s]}]}]}]}' \\`,
|
|
1588
|
+
' "$(attr service.name "$SERVICE")" "$(attr cloud.provider azure)" "$(attr cloud.platform azure.vm)" "$(attr cloud.resource_id "$ID")" \\',
|
|
1589
|
+
` "$NOW" "$NOW" "$BODY" "$(attr event.name ${DEPLOY_EVENT})" |`,
|
|
1590
|
+
` curl -fsS -m 10 -o /dev/null -X POST "$ENDPOINT/v1/logs" -H 'content-type: application/json' -H @<(printf '${SECRET.header}: %s\\n' "$KEY") --data-binary @- \\`,
|
|
1591
|
+
' && echo "Sent the deploy marker to UKNOWS." || echo "UKNOWS did not get the deploy marker. Your deploy is done."',
|
|
1592
|
+
')',
|
|
1593
|
+
].join('\n')
|
|
1594
|
+
}
|
|
1595
|
+
|
|
561
1596
|
/**
|
|
562
1597
|
* A bash script that asks for the key without showing it. The ( ) block is a
|
|
563
1598
|
* subshell: exit stops the block, not the terminal, and the key is gone when
|
|
@@ -573,21 +1608,52 @@ export function keyScript(setup, body, where = 'Run in bash') {
|
|
|
573
1608
|
].join('\n')
|
|
574
1609
|
}
|
|
575
1610
|
|
|
576
|
-
|
|
1611
|
+
/** The first step of the key steps of a host. Kubernetes and the other hosts that are not 1 cloud use the plain Windows tip. */
|
|
1612
|
+
export const BASH_STEP = `Open a bash terminal. ${MACOS_TIP} ${WINDOWS_TIP}`
|
|
1613
|
+
export const AZURE_BASH_STEP = `Open a bash terminal. ${MACOS_TIP} On Windows, use Azure Cloud Shell or WSL.`
|
|
1614
|
+
export const AWS_BASH_STEP = `Open a bash terminal. ${MACOS_TIP} On Windows, use AWS CloudShell or WSL.`
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* The names that a key script uses. The page shows the examples (EXAMPLE_NAMES). The setup tool fills in the names
|
|
1618
|
+
* that it finds in the deploy files of the repo. from: where each found name comes from, such as "deploy.yml line 12".
|
|
1619
|
+
* Each name keeps a comment that says where it comes from, or that it is an example.
|
|
1620
|
+
*/
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* The first lines of each Azure script: the subscription, the resource group and the apps. Every az command names
|
|
1624
|
+
* the subscription, so a script never acts on another subscription that happens to be the default of the CLI.
|
|
1625
|
+
* The script stops when SUBSCRIPTION is empty.
|
|
1626
|
+
*/
|
|
1627
|
+
export function azureNames(names = {}, one = false) {
|
|
1628
|
+
const from = names.from ?? {}
|
|
1629
|
+
// A comment for each name: where it comes from, what it is, or that it is an example.
|
|
1630
|
+
const say = (key , given , what , example ) =>
|
|
1631
|
+
(from[key] ? `from ${from[key]}` : given ? what : example)
|
|
1632
|
+
return [
|
|
1633
|
+
`SUBSCRIPTION="${names.subscription ?? ''}" # ${say('subscription', Boolean(names.subscription), 'the subscription of your apps', 'the id of the subscription of your apps. az account list -o table lists them.')}`,
|
|
1634
|
+
'[ -n "$SUBSCRIPTION" ] || { echo "Set SUBSCRIPTION first. Nothing changed."; exit 1; }',
|
|
1635
|
+
`RESOURCE_GROUP=${names.resourceGroup ?? EXAMPLE_NAMES.resourceGroup} # ${say('resourceGroup', Boolean(names.resourceGroup), 'the resource group of your apps', 'an example. Use the resource group of your apps.')}`,
|
|
1636
|
+
...(one
|
|
1637
|
+
? [`APP=${names.app ?? EXAMPLE_SERVICE} # ${say('app', Boolean(names.app), 'the name of your app on the platform', 'an example. Use the name of your app on the platform.')}`,
|
|
1638
|
+
...(names.slot ? [names.slotUnknown
|
|
1639
|
+
? `SLOT=your-slot # an example. Use the slot that ${from.slot ?? 'your deploy'} deploys to.`
|
|
1640
|
+
: `SLOT=${names.slot} # ${say('slot', true, 'the deployment slot', '')}`] : [])]
|
|
1641
|
+
: [`APPS="${names.apps ?? EXAMPLE_NAMES.apps}" # ${say('apps', Boolean(names.apps), 'only these apps get the key', 'examples. Only these apps get the key.')}${from.apps ? '. Only these apps get the key.' : ''}`]),
|
|
1642
|
+
]
|
|
1643
|
+
}
|
|
577
1644
|
|
|
578
1645
|
/**
|
|
579
1646
|
* The lines before the key prompt of a script that changes apps one by one.
|
|
580
1647
|
* With DRY_RUN=1 the script only checks that each app exists and says what it
|
|
581
1648
|
* would change. It never asks for the key.
|
|
582
1649
|
*/
|
|
583
|
-
export function dryRun(
|
|
1650
|
+
export function dryRun(show, change, names = {}) {
|
|
584
1651
|
return [
|
|
585
|
-
|
|
586
|
-
`APPS="${apps}" # only these apps get the key`,
|
|
1652
|
+
...azureNames(names),
|
|
587
1653
|
'DRY_RUN=1 # 1: list the changes only. 0: make them.',
|
|
588
1654
|
'if [ "$DRY_RUN" = 1 ]; then',
|
|
589
1655
|
' for APP in $APPS; do',
|
|
590
|
-
` ${show} -g "$RESOURCE_GROUP" -n "$APP" -o none &&`,
|
|
1656
|
+
` ${show} --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none &&`,
|
|
591
1657
|
` echo "Would set the key on $APP. ${change}"`,
|
|
592
1658
|
' done',
|
|
593
1659
|
' exit 0',
|
|
@@ -595,34 +1661,355 @@ export function dryRun(apps, show, change) {
|
|
|
595
1661
|
]
|
|
596
1662
|
}
|
|
597
1663
|
|
|
1664
|
+
/** The key script for Azure Container Apps. */
|
|
1665
|
+
export function acaKeyScript(names = {}) {
|
|
1666
|
+
return keyScript(dryRun('az containerapp show', 'It would restart the app.', names), [
|
|
1667
|
+
'for APP in $APPS; do',
|
|
1668
|
+
' az containerapp secret set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
1669
|
+
' --secrets "uknows-otlp-headers=x-uknows-key=$UKNOWS_KEY" &&',
|
|
1670
|
+
' az containerapp update --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
1671
|
+
' --set-env-vars "OTEL_EXPORTER_OTLP_HEADERS=secretref:uknows-otlp-headers" &&',
|
|
1672
|
+
' echo "Set the key on $APP."',
|
|
1673
|
+
'done',
|
|
1674
|
+
])
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** The Key Vault script for Azure Container Apps. It never sees the key. */
|
|
1678
|
+
export function acaVaultScript(names = {}) {
|
|
1679
|
+
return [
|
|
1680
|
+
'# Run in bash. This script never sees the key.',
|
|
1681
|
+
'(',
|
|
1682
|
+
...dryRun('az containerapp show', 'It would restart the app.', names).map((line) => ` ${line}`),
|
|
1683
|
+
' SECRET_URI=https://my-vault.vault.azure.net/secrets/uknows-otlp-headers',
|
|
1684
|
+
' for APP in $APPS; do',
|
|
1685
|
+
' az containerapp secret set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
1686
|
+
' --secrets "uknows-otlp-headers=keyvaultref:$SECRET_URI,identityref:system" &&',
|
|
1687
|
+
' az containerapp update --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
1688
|
+
' --set-env-vars "OTEL_EXPORTER_OTLP_HEADERS=secretref:uknows-otlp-headers" &&',
|
|
1689
|
+
' echo "Set the key on $APP."',
|
|
1690
|
+
' done',
|
|
1691
|
+
')',
|
|
1692
|
+
].join('\n')
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/** The key script for Azure App Service web apps. */
|
|
1696
|
+
export function appServiceKeyScript(names = {}) {
|
|
1697
|
+
return keyScript(dryRun('az webapp show', 'It would restart the app.', names), [
|
|
1698
|
+
'for APP in $APPS; do',
|
|
1699
|
+
' az webapp config appsettings set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
1700
|
+
' --settings "OTEL_EXPORTER_OTLP_HEADERS=x-uknows-key=$UKNOWS_KEY" &&',
|
|
1701
|
+
' echo "Set the key on $APP."',
|
|
1702
|
+
'done',
|
|
1703
|
+
])
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/** The key script for Azure Functions: the key as an app setting of each function app. */
|
|
1707
|
+
export function functionsKeyScript(names = {}) {
|
|
1708
|
+
return keyScript(dryRun('az functionapp show', 'It would restart the app.', names), [
|
|
1709
|
+
'for APP in $APPS; do',
|
|
1710
|
+
' az functionapp config appsettings set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
1711
|
+
' --settings "OTEL_EXPORTER_OTLP_HEADERS=x-uknows-key=$UKNOWS_KEY" &&',
|
|
1712
|
+
' echo "Set the key on $APP."',
|
|
1713
|
+
'done',
|
|
1714
|
+
])
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/** The 5 settings as app settings of each function app (the docs tab). It has no key. */
|
|
1718
|
+
export function functionsSettingsScript(names = {}) {
|
|
1719
|
+
return [
|
|
1720
|
+
'# Run in bash. This script has no key.',
|
|
1721
|
+
...azureNames(names),
|
|
1722
|
+
'for APP in $APPS; do',
|
|
1723
|
+
' az functionapp config appsettings set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none --settings \\',
|
|
1724
|
+
...SETTINGS.map((setting) => ` "${setting.name}=${setting.name === 'OTEL_SERVICE_NAME' ? '$APP' : setting.value}" \\`),
|
|
1725
|
+
' && echo "Set the 5 settings on $APP."',
|
|
1726
|
+
'done',
|
|
1727
|
+
].join('\n')
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Azure Functions, for the setup tool. The docs page does not show these yet: it says that serverless functions are not
|
|
1732
|
+
* supported (SERVERLESS) until the install proof (test A3) passes. The Functions host itself sends a server span for
|
|
1733
|
+
* each call of a function, and the logs, over OTLP, when host.json has "telemetryMode": "OpenTelemetry" and the app
|
|
1734
|
+
* has the OTLP app settings. Measured on the bench on 22 Oct 2026: every request of a Node.js, a Python and a .NET
|
|
1735
|
+
* isolated function app had its server span, with no package and no code in the app. The setup tool fills in {file},
|
|
1736
|
+
* {value} and {count}.
|
|
1737
|
+
*/
|
|
1738
|
+
export const FUNCTIONS = {
|
|
1739
|
+
label: 'Azure Functions',
|
|
1740
|
+
telemetryMode: 'OpenTelemetry',
|
|
1741
|
+
summary: 'Turn on OpenTelemetry in the Functions host: "telemetryMode": "OpenTelemetry" in {file}.',
|
|
1742
|
+
other: {
|
|
1743
|
+
title: 'Turn on OpenTelemetry in host.json',
|
|
1744
|
+
lines: [
|
|
1745
|
+
'{file} sets "telemetryMode" to {value}. This tool does not change it.',
|
|
1746
|
+
'Set it to "OpenTelemetry". Then the Functions host sends the traces and logs of your functions to UKNOWS.',
|
|
1747
|
+
],
|
|
1748
|
+
},
|
|
1749
|
+
unreadable: {
|
|
1750
|
+
title: 'Turn on OpenTelemetry in host.json',
|
|
1751
|
+
lines: [
|
|
1752
|
+
'This tool cannot read {file} as JSON, so it did not change it.',
|
|
1753
|
+
'Add "telemetryMode": "OpenTelemetry" next to "version" in it. Then the Functions host sends the traces and logs of your functions to UKNOWS.',
|
|
1754
|
+
],
|
|
1755
|
+
},
|
|
1756
|
+
notes: [
|
|
1757
|
+
'The Functions host sends a trace for each call of a function, and the logs, once host.json and the app settings are in. Your code needs no package.',
|
|
1758
|
+
'With OpenTelemetry on, the log stream of the Azure portal does not show the logs of this app, and "Recent function invocations" only shows data that goes to Application Insights. If the app has APPLICATIONINSIGHTS_CONNECTION_STRING, the host keeps sending to Application Insights too.',
|
|
1759
|
+
],
|
|
1760
|
+
/** The UKNOWS hooks cannot apply to this app (hooks.unsupported), so host.json stays as it is (measured 22 Oct 2026). */
|
|
1761
|
+
standDown: {
|
|
1762
|
+
title: 'OpenTelemetry stays off for this function app',
|
|
1763
|
+
reason: 'This tool cannot add the UKNOWS hooks to this app: {reason}.',
|
|
1764
|
+
signs: '{signs} show that this app sends to Application Insights.',
|
|
1765
|
+
unknown: 'This tool could not look at the app settings of your function app in Azure, so it cannot tell if the app sends to Application Insights.',
|
|
1766
|
+
lines: [
|
|
1767
|
+
'With OpenTelemetry on in the Functions host, Application Insights still gets the requests and the logs. But it names each request by its route, such as GET api/orders/{id}, instead of the name of the function, and leaves out InvocationId. The function list under Monitor in the Azure portal, and your own queries and alerts on the function name, then change. The log stream of the Azure portal stops showing the logs of the app.',
|
|
1768
|
+
'To turn it on in the Functions host anyway, run npx @uknows/setup --functions-opentelemetry.',
|
|
1769
|
+
],
|
|
1770
|
+
},
|
|
1771
|
+
/** A dry run in a terminal: what a real run does first. */
|
|
1772
|
+
predicted: 'A real run first checks the app settings of your function app in Azure. If the app sends to Application Insights, it adds the UKNOWS hooks instead of this line.',
|
|
1773
|
+
/** The pull request text, when host.json gets the line: a paragraph of its own. */
|
|
1774
|
+
pr: 'Turns on OpenTelemetry in the Azure Functions host (host.json). The host then sends the traces and logs of each call of a function to UKNOWS. The log stream of the Azure portal stops showing the logs of the app; UKNOWS shows them.',
|
|
1775
|
+
/**
|
|
1776
|
+
* The UKNOWS worker hooks: the default for a function app that sends to Application Insights, or where setup cannot
|
|
1777
|
+
* tell. The host keeps its Application Insights mode (measured on the bench, 22 and 23 Oct 2026: Application Insights
|
|
1778
|
+
* the same, a server span for each call, the same trace id and InvocationId).
|
|
1779
|
+
*/
|
|
1780
|
+
hooks: {
|
|
1781
|
+
summary: {
|
|
1782
|
+
node: 'Add the UKNOWS hooks: {file}, loaded with your functions ("main": "{main}" in package.json). Application Insights stays exactly as it is.',
|
|
1783
|
+
file: 'Add the UKNOWS hooks: {file}. Application Insights stays exactly as it is.',
|
|
1784
|
+
python: 'Add the UKNOWS hooks: {file}, loaded by 2 lines in {app}. Application Insights stays exactly as it is.',
|
|
1785
|
+
dotnet: 'Add the UKNOWS hooks: {file}, loaded by 2 lines in {program}. Application Insights stays exactly as it is.',
|
|
1786
|
+
},
|
|
1787
|
+
notes: [
|
|
1788
|
+
'This function app sends to Application Insights, or this tool could not tell. So instead of the line in host.json, this tool adds small UKNOWS hooks to the code of the app. Application Insights keeps the same requests, names and fields. UKNOWS gets a span for each call of a function, with the trace id of Application Insights, and the logs.',
|
|
1789
|
+
],
|
|
1790
|
+
packagesTitle: 'Add the OpenTelemetry packages of the UKNOWS hooks',
|
|
1791
|
+
packagesByHand: 'This tool cannot add these packages here. Add them at exactly these versions: UKNOWS tested the hooks with them.',
|
|
1792
|
+
pythonTitle: 'Load the UKNOWS hooks in function_app.py',
|
|
1793
|
+
pythonByHand: 'This tool did not find 1 FunctionApp and the azure.functions import in {file}. Add the import at the top, and the instrument line after the last function, with the name of your FunctionApp.',
|
|
1794
|
+
dotnetTitle: 'Add the UKNOWS middleware in Program.cs',
|
|
1795
|
+
dotnetByHand: 'This tool cannot place the 2 lines in {file}. Add them where the worker is set up, after the builder is made.',
|
|
1796
|
+
untested: {
|
|
1797
|
+
title: 'Check UKNOWS after the deploy: a version that UKNOWS did not test',
|
|
1798
|
+
other: 'This app uses {name} {found}. UKNOWS tested the hooks with {name} {tested}.',
|
|
1799
|
+
unknown: 'This tool cannot read the version of {name} that this app uses. UKNOWS tested the hooks with {name} {tested}.',
|
|
1800
|
+
on: 'Tested on {on}.',
|
|
1801
|
+
lines: ['After the deploy, check that each call of the app shows in UKNOWS. If it does not, run npx @uknows/setup remove and tell us.'],
|
|
1802
|
+
},
|
|
1803
|
+
unsupported: {
|
|
1804
|
+
otel: 'the app sends OpenTelemetry or Azure Monitor data from its own code already',
|
|
1805
|
+
language: 'the hooks are for Node.js, Python and .NET function apps, not {lang}',
|
|
1806
|
+
packageJson: 'this tool cannot read package.json',
|
|
1807
|
+
nodeV3: 'the hooks need version 4 of @azure/functions, the v4 programming model',
|
|
1808
|
+
nodeMain: 'package.json has no "main" that this tool can change',
|
|
1809
|
+
pythonV1: 'the hooks need function_app.py, the v2 programming model of Python',
|
|
1810
|
+
pythonRequirements: 'the app has no requirements.txt',
|
|
1811
|
+
dotnetInProcess: 'the hooks need the isolated worker (Microsoft.Azure.Functions.Worker); the in-process model has no middleware',
|
|
1812
|
+
},
|
|
1813
|
+
pr: 'Adds small UKNOWS hooks to the function app ({files}). The Functions host keeps its Application Insights mode, so Application Insights keeps the same requests, names and fields. UKNOWS gets a span for each call and the logs from the worker, with the same trace id.',
|
|
1814
|
+
prPython: 'Python: the function app needs the app setting PYTHON_ENABLE_WORKER_EXTENSIONS=1 too, with the other settings. The Python worker runs the hooks only with it.',
|
|
1815
|
+
},
|
|
1816
|
+
settings: {
|
|
1817
|
+
title: 'Set the app settings of your function app',
|
|
1818
|
+
lines: [
|
|
1819
|
+
'A function app reads its settings from its app settings, not from a file in the repo. Add the {count} settings below to each function app that runs this code.',
|
|
1820
|
+
'They stay when you deploy new code. Set them once for each app, for example staging and production.',
|
|
1821
|
+
],
|
|
1822
|
+
},
|
|
1823
|
+
key: {
|
|
1824
|
+
title: 'Set your key once: Azure Functions',
|
|
1825
|
+
lines: [
|
|
1826
|
+
AZURE_BASH_STEP,
|
|
1827
|
+
'Sign in with az login.',
|
|
1828
|
+
'Set SUBSCRIPTION, RESOURCE_GROUP and APPS to your function apps at the top of the script. az account list -o table lists your subscriptions. Run the script with DRY_RUN=1 first. It checks each app and says what it would change.',
|
|
1829
|
+
'Set DRY_RUN=0 and run it again. Paste your key when it asks. The key does not show on the screen.',
|
|
1830
|
+
'Azure restarts each function app with the key. Run it at a quiet time.',
|
|
1831
|
+
'Run the script on your own computer or in Azure Cloud Shell. Do not run it on a shared server. Other users of a shared server can see the commands that run, and the az command gets the key.',
|
|
1832
|
+
],
|
|
1833
|
+
},
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** The Key Vault script for Azure App Service. It never sees the key. */
|
|
1837
|
+
export function appServiceVaultScript(names = {}) {
|
|
1838
|
+
return [
|
|
1839
|
+
'# Run in bash. This script never sees the key.',
|
|
1840
|
+
'(',
|
|
1841
|
+
...dryRun('az webapp show', 'It would restart the app.', names).map((line) => ` ${line}`),
|
|
1842
|
+
' SECRET_URI=https://my-vault.vault.azure.net/secrets/uknows-otlp-headers',
|
|
1843
|
+
' for APP in $APPS; do',
|
|
1844
|
+
' az webapp config appsettings set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
1845
|
+
' --settings "OTEL_EXPORTER_OTLP_HEADERS=@Microsoft.KeyVault(SecretUri=$SECRET_URI)" &&',
|
|
1846
|
+
' echo "Set the key on $APP."',
|
|
1847
|
+
' done',
|
|
1848
|
+
')',
|
|
1849
|
+
].join('\n')
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* App Service with a deployment slot that is swapped into production. A swap moves the code and the plain app
|
|
1854
|
+
* settings between the slots, and a slot setting stays with its slot. So the key goes on each slot as a slot setting.
|
|
1855
|
+
* The setup tool prints this when a deploy file names a slot. It fills in {app}, {slot} and {where}.
|
|
1856
|
+
*/
|
|
1857
|
+
export const APP_SERVICE_SLOTS = {
|
|
1858
|
+
title: 'Set the key on both slots of {app}',
|
|
1859
|
+
lines: [
|
|
1860
|
+
'{where} deploys {app} to the slot {slot}, and a swap moves the slot into production.',
|
|
1861
|
+
'A swap moves the code and the plain app settings between the slots. A slot setting stays with its slot. So set the key as a slot setting on both slots: the production slot with the key of your Production environment, and the {slot} slot with the key of the environment that it serves.',
|
|
1862
|
+
'Run both scripts below. Each one asks for its key. Each one restarts that slot, so run them at a quiet time.',
|
|
1863
|
+
],
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/** The script that sets the key as a slot setting on 1 slot of a web app: production when names.slot is empty. */
|
|
1867
|
+
export function appServiceSlotScript(names = {}) {
|
|
1868
|
+
const slot = names.slot ? ' --slot "$SLOT"' : ''
|
|
1869
|
+
return keyScript(azureNames(names, true), [
|
|
1870
|
+
`az webapp config appsettings set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP"${slot} -o none \\`,
|
|
1871
|
+
' --slot-settings "OTEL_EXPORTER_OTLP_HEADERS=x-uknows-key=$UKNOWS_KEY" &&',
|
|
1872
|
+
names.slot ? 'echo "Set the key on the $SLOT slot of $APP."' : 'echo "Set the key on the production slot of $APP."',
|
|
1873
|
+
], names.slotUnknown ? 'Run in bash for the deployment slot' : names.slot ? `Run in bash for the ${names.slot} slot` : 'Run in bash for the production slot')
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* App Service that runs the code of the app, not an image (a zip or Oryx deploy). App Service does not use the
|
|
1878
|
+
* Dockerfile of the repo then, so the start line and the settings must be set on App Service. The setup tool prints
|
|
1879
|
+
* this as a required step. It fills in {where}, {dockerfile}, {count}, {startWhere}, {startup} and {prefix}.
|
|
1880
|
+
*/
|
|
1881
|
+
export const APP_SERVICE_CODE = {
|
|
1882
|
+
title: 'Set the start command and the settings on App Service',
|
|
1883
|
+
lines: [
|
|
1884
|
+
'{where} deploys the code of this app to App Service, not an image. App Service builds the code itself and does not use {dockerfile}, so the changes in {dockerfile} do not reach the app there.',
|
|
1885
|
+
'Add the {count} settings below as app settings of the web app. Put them in the step of your deploy that sets the other app settings, so each deploy keeps them.',
|
|
1886
|
+
],
|
|
1887
|
+
start: '{startWhere} sets the startup command to {startup}. Change it to the startup command below, so App Service starts the app with {prefix}.',
|
|
1888
|
+
startNone: 'Set the startup command below on the web app, so App Service starts the app with {prefix}. Replace the command after {prefix} with the command that starts your app.',
|
|
1889
|
+
noDockerfile: 'your Dockerfile',
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/** The GitHub environment secret commands, 1 for each environment, and the repository secret when a job has none. */
|
|
1893
|
+
export function githubSecretCommands(environments = [EXAMPLE_NAMES.environment], repo = EXAMPLE_NAMES.repo) {
|
|
1894
|
+
return environments.map((environment) => `gh secret set UKNOWS_KEY --env ${environment} --repo ${repo}`).join('\n')
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
/** The workflow step that sets the key on a container app, before the deploy step. */
|
|
1898
|
+
export function githubKeyStep(names = {}) {
|
|
1899
|
+
const environment = names.environment === undefined ? EXAMPLE_NAMES.environment : names.environment
|
|
1900
|
+
return [
|
|
1901
|
+
`# Add this step before your deploy step${environment ? `, in the job with environment: ${environment}` : ''}.`,
|
|
1902
|
+
'# An earlier step signs in to Azure.',
|
|
1903
|
+
'- name: Set the UKNOWS key',
|
|
1904
|
+
' env:',
|
|
1905
|
+
' SUBSCRIPTION: ${{ vars.AZURE_SUBSCRIPTION_ID }} # the subscription id of the app',
|
|
1906
|
+
` RESOURCE_GROUP: ${names.resourceGroup ?? EXAMPLE_NAMES.resourceGroup}`,
|
|
1907
|
+
` APP: ${names.app ?? EXAMPLE_SERVICE}`,
|
|
1908
|
+
' UKNOWS_KEY: ${{ secrets.UKNOWS_KEY }}',
|
|
1909
|
+
' run: |',
|
|
1910
|
+
' [ -n "$UKNOWS_KEY" ] || { echo "::error::Set the UKNOWS_KEY secret for this environment."; exit 1; }',
|
|
1911
|
+
' az containerapp secret set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none --secrets "uknows-otlp-headers=x-uknows-key=$UKNOWS_KEY"',
|
|
1912
|
+
`# In your deploy step, add ${ACA_SECRET_REF} to its environment variables.`,
|
|
1913
|
+
`# If the command has --set-env-vars already, add the pair to that list: az uses only the last --set-env-vars.`,
|
|
1914
|
+
].join('\n')
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* What the setup tool says about the deploy jobs of each workflow. It fills in {where}, {job} and {environment}.
|
|
1919
|
+
* The page does not show these.
|
|
1920
|
+
*/
|
|
1921
|
+
export const GITHUB_JOBS = {
|
|
1922
|
+
found: '{where}: the deploy job {job} has environment: {environment}. Set the key for that environment.',
|
|
1923
|
+
none: '{where}: the deploy job {job} has no environment: line, so it cannot read an environment secret.',
|
|
1924
|
+
}
|
|
1925
|
+
|
|
598
1926
|
export const QUIET_TIME = 'Run it at a quiet time.'
|
|
599
1927
|
|
|
1928
|
+
/** The env entry that loads the key from the uknows-otlp-headers secret of a container app. */
|
|
1929
|
+
export const ACA_SECRET_REF = 'OTEL_EXPORTER_OTLP_HEADERS=secretref:uknows-otlp-headers'
|
|
1930
|
+
|
|
1931
|
+
export const ACA_YAML = [
|
|
1932
|
+
'# Your Container Apps YAML file. Keep your other settings.',
|
|
1933
|
+
'properties:',
|
|
1934
|
+
' configuration:',
|
|
1935
|
+
' secrets:',
|
|
1936
|
+
' - name: uknows-otlp-headers',
|
|
1937
|
+
' keyVaultUrl: https://my-vault.vault.azure.net/secrets/uknows-otlp-headers',
|
|
1938
|
+
' identity: system',
|
|
1939
|
+
' template:',
|
|
1940
|
+
' containers:',
|
|
1941
|
+
` - name: ${EXAMPLE_SERVICE}`,
|
|
1942
|
+
' env:',
|
|
1943
|
+
' - name: OTEL_EXPORTER_OTLP_HEADERS',
|
|
1944
|
+
' secretRef: uknows-otlp-headers',
|
|
1945
|
+
].join('\n')
|
|
1946
|
+
|
|
1947
|
+
export const ACA_BICEP = [
|
|
1948
|
+
'// In your Microsoft.App/containerApps resource. Keep your other settings.',
|
|
1949
|
+
'// The app reads the secret from Key Vault with its managed identity.',
|
|
1950
|
+
'configuration: {',
|
|
1951
|
+
' secrets: [',
|
|
1952
|
+
' {',
|
|
1953
|
+
" name: 'uknows-otlp-headers'",
|
|
1954
|
+
" keyVaultUrl: 'https://my-vault.vault.azure.net/secrets/uknows-otlp-headers'",
|
|
1955
|
+
" identity: 'system'",
|
|
1956
|
+
' }',
|
|
1957
|
+
' ]',
|
|
1958
|
+
'}',
|
|
1959
|
+
'template: {',
|
|
1960
|
+
' containers: [',
|
|
1961
|
+
' {',
|
|
1962
|
+
` name: '${EXAMPLE_SERVICE}'`,
|
|
1963
|
+
' env: [',
|
|
1964
|
+
" { name: 'OTEL_EXPORTER_OTLP_HEADERS', secretRef: 'uknows-otlp-headers' }",
|
|
1965
|
+
' ]',
|
|
1966
|
+
' }',
|
|
1967
|
+
' ]',
|
|
1968
|
+
'}',
|
|
1969
|
+
].join('\n')
|
|
1970
|
+
|
|
1971
|
+
export const ACA_TERRAFORM = [
|
|
1972
|
+
'# Pass the value with TF_VAR_uknows_otlp_headers. Keep it out of the repo.',
|
|
1973
|
+
'variable "uknows_otlp_headers" {',
|
|
1974
|
+
' type = string',
|
|
1975
|
+
' sensitive = true',
|
|
1976
|
+
'}',
|
|
1977
|
+
'',
|
|
1978
|
+
'# In your azurerm_container_app resource. Keep your other settings.',
|
|
1979
|
+
'secret {',
|
|
1980
|
+
' name = "uknows-otlp-headers"',
|
|
1981
|
+
' value = var.uknows_otlp_headers',
|
|
1982
|
+
'}',
|
|
1983
|
+
'',
|
|
1984
|
+
'template {',
|
|
1985
|
+
' container {',
|
|
1986
|
+
` name = "${EXAMPLE_SERVICE}"`,
|
|
1987
|
+
' env {',
|
|
1988
|
+
' name = "OTEL_EXPORTER_OTLP_HEADERS"',
|
|
1989
|
+
' secret_name = "uknows-otlp-headers"',
|
|
1990
|
+
' }',
|
|
1991
|
+
' }',
|
|
1992
|
+
'}',
|
|
1993
|
+
].join('\n')
|
|
1994
|
+
|
|
600
1995
|
export const HOSTS = [
|
|
601
1996
|
{
|
|
602
1997
|
id: 'azure-container-apps',
|
|
603
1998
|
label: 'Azure Container Apps',
|
|
604
1999
|
summary: 'Set the key as a secret on the container apps that you name.',
|
|
605
2000
|
steps: [
|
|
606
|
-
|
|
2001
|
+
AZURE_BASH_STEP,
|
|
607
2002
|
'Sign in with az login.',
|
|
608
|
-
'Set RESOURCE_GROUP
|
|
2003
|
+
'Set SUBSCRIPTION, RESOURCE_GROUP and APPS at the top of the script. APPS is the list of apps that send data to UKNOWS. az account list -o table lists your subscriptions, and az containerapp list --subscription <subscription id> -g <resource group> --query "[].name" -o tsv lists the apps.',
|
|
609
2004
|
'Run the script with DRY_RUN=1 first. It checks each app and says what it would change. It changes nothing.',
|
|
610
2005
|
'Set DRY_RUN=0 and run it again. Paste your key when it asks. The key does not show on the screen.',
|
|
611
|
-
`Each app
|
|
2006
|
+
`Each app restarts with the key. ${QUIET_TIME}`,
|
|
612
2007
|
],
|
|
613
2008
|
snippets: [
|
|
614
2009
|
{
|
|
615
2010
|
caption: 'Set the key on your container apps',
|
|
616
2011
|
language: 'bash',
|
|
617
|
-
code:
|
|
618
|
-
'for APP in $APPS; do',
|
|
619
|
-
' az containerapp secret set -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
620
|
-
' --secrets "uknows-otlp-headers=x-uknows-key=$UKNOWS_KEY" &&',
|
|
621
|
-
' az containerapp update -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
622
|
-
' --set-env-vars "OTEL_EXPORTER_OTLP_HEADERS=secretref:uknows-otlp-headers" &&',
|
|
623
|
-
' echo "Set the key on $APP."',
|
|
624
|
-
'done',
|
|
625
|
-
]),
|
|
2012
|
+
code: acaKeyScript(),
|
|
626
2013
|
},
|
|
627
2014
|
],
|
|
628
2015
|
notes: [
|
|
@@ -637,38 +2024,50 @@ export const HOSTS = [
|
|
|
637
2024
|
'In the Azure portal, open your key vault. Create a secret named uknows-otlp-headers. Set its value to x-uknows-key=<key>.',
|
|
638
2025
|
'Turn on the system-assigned managed identity of each container app.',
|
|
639
2026
|
'Give each identity the Key Vault Secrets User role on the key vault.',
|
|
640
|
-
'Set APPS and SECRET_URI. Run the script with DRY_RUN=1 first. Then set DRY_RUN=0 and run it again at a quiet time.',
|
|
641
|
-
],
|
|
642
|
-
snippets: [
|
|
643
|
-
{
|
|
644
|
-
caption: 'Use the Key Vault secret on your container apps',
|
|
645
|
-
language: 'bash',
|
|
646
|
-
code: [
|
|
647
|
-
'# Run in bash. This script never sees the key.',
|
|
648
|
-
'(',
|
|
649
|
-
...dryRun('orders-api payments', 'az containerapp show', 'It would start a new revision.').map((line) => ` ${line}`),
|
|
650
|
-
' SECRET_URI=https://my-vault.vault.azure.net/secrets/uknows-otlp-headers',
|
|
651
|
-
' for APP in $APPS; do',
|
|
652
|
-
' az containerapp secret set -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
653
|
-
' --secrets "uknows-otlp-headers=keyvaultref:$SECRET_URI,identityref:system" &&',
|
|
654
|
-
' az containerapp update -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
655
|
-
' --set-env-vars "OTEL_EXPORTER_OTLP_HEADERS=secretref:uknows-otlp-headers" &&',
|
|
656
|
-
' echo "Set the key on $APP."',
|
|
657
|
-
' done',
|
|
658
|
-
')',
|
|
659
|
-
].join('\n'),
|
|
660
|
-
},
|
|
2027
|
+
'Set SUBSCRIPTION, RESOURCE_GROUP, APPS and SECRET_URI. Run the script with DRY_RUN=1 first. Then set DRY_RUN=0 and run it again at a quiet time.',
|
|
661
2028
|
],
|
|
2029
|
+
snippets: [{ caption: 'Use the Key Vault secret on your container apps', language: 'bash', code: acaVaultScript() }],
|
|
662
2030
|
},
|
|
2031
|
+
checks: [
|
|
2032
|
+
{
|
|
2033
|
+
id: 'replace-env-vars',
|
|
2034
|
+
text: `If your deploy runs az containerapp update with --replace-env-vars, that command replaces every environment variable. The next deploy removes the key reference. Add ${ACA_SECRET_REF} to that list.`,
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
id: 'deploy-action-env',
|
|
2038
|
+
text: `If your workflow uses azure/container-apps-deploy-action with environmentVariables, the action replaces every environment variable too. Add ${ACA_SECRET_REF} to environmentVariables.`,
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
id: 'deploy-action-yaml',
|
|
2042
|
+
text: 'If the deploy action uses yamlConfigPath, put the secret and the env entry in that YAML file. Otherwise each deploy drops them.',
|
|
2043
|
+
snippets: [{ caption: 'Container Apps YAML', language: 'yaml', code: ACA_YAML }],
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
id: 'template',
|
|
2047
|
+
text: 'If a Bicep or Terraform template deploys your container app, the template owns its environment variables and secrets. Add the secret and the env entry to the template, as a Key Vault reference, a @secure() parameter or a secret block. A change made only with az is undone the next time the template is applied.',
|
|
2048
|
+
snippets: [
|
|
2049
|
+
{ caption: 'Bicep', language: 'bicep', code: ACA_BICEP },
|
|
2050
|
+
{ caption: 'Terraform', language: 'hcl', code: ACA_TERRAFORM },
|
|
2051
|
+
],
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
id: 'template-ignored',
|
|
2055
|
+
text: 'If your Terraform template has lifecycle ignore_changes on the env or the secrets of the container app, terraform apply keeps what the app has. Then set the key with az, as the script above does. A change to the template does not reach this app.',
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
id: 'secret-first',
|
|
2059
|
+
text: 'Set the secret on each app before the deploy that references it. A deploy that references a secret that is not there yet fails with ContainerAppSecretRefNotFound.',
|
|
2060
|
+
},
|
|
2061
|
+
],
|
|
663
2062
|
},
|
|
664
2063
|
{
|
|
665
2064
|
id: 'azure-app-service',
|
|
666
2065
|
label: 'Azure App Service',
|
|
667
2066
|
summary: 'Set the key as an app setting on the web apps that you name.',
|
|
668
2067
|
steps: [
|
|
669
|
-
|
|
2068
|
+
AZURE_BASH_STEP,
|
|
670
2069
|
'Sign in with az login.',
|
|
671
|
-
'Set RESOURCE_GROUP and APPS to your web apps. Run the script with DRY_RUN=1 first. It checks each app and says what it would change.',
|
|
2070
|
+
'Set SUBSCRIPTION, RESOURCE_GROUP and APPS to your web apps at the top of the script. az account list -o table lists your subscriptions. Run the script with DRY_RUN=1 first. It checks each app and says what it would change.',
|
|
672
2071
|
'Set DRY_RUN=0 and run it again. Paste your key when it asks. The key does not show on the screen.',
|
|
673
2072
|
`App Service restarts each app with the key. ${QUIET_TIME}`,
|
|
674
2073
|
],
|
|
@@ -676,20 +2075,14 @@ export const HOSTS = [
|
|
|
676
2075
|
{
|
|
677
2076
|
caption: 'Set the key on your web apps',
|
|
678
2077
|
language: 'bash',
|
|
679
|
-
code:
|
|
680
|
-
'for APP in $APPS; do',
|
|
681
|
-
' az webapp config appsettings set -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
682
|
-
' --settings "OTEL_EXPORTER_OTLP_HEADERS=x-uknows-key=$UKNOWS_KEY" &&',
|
|
683
|
-
' echo "Set the key on $APP."',
|
|
684
|
-
'done',
|
|
685
|
-
]),
|
|
2078
|
+
code: appServiceKeyScript(),
|
|
686
2079
|
},
|
|
687
2080
|
],
|
|
688
2081
|
notes: [
|
|
689
2082
|
'Run the script on your own computer or in Azure Cloud Shell. Do not run it on a shared server. Other users of a shared server can see the commands that run, and the az command gets the key.',
|
|
690
2083
|
'If your app does not start from your Dockerfile, add the 5 settings as app settings too.',
|
|
691
2084
|
'For Python and .NET, put the start line of your stack in the startup command of the app.',
|
|
692
|
-
'
|
|
2085
|
+
'If you swap a deployment slot into production, set the key on both slots as a slot setting, with --slot-settings instead of --settings. A swap moves the plain app settings with the code, and a slot setting stays with its slot. Add --slot with the name of the slot for that slot, and no --slot for production.',
|
|
693
2086
|
],
|
|
694
2087
|
stricter: {
|
|
695
2088
|
title: 'Stricter option: keep the key in Azure Key Vault',
|
|
@@ -698,19 +2091,69 @@ export const HOSTS = [
|
|
|
698
2091
|
'In the Azure portal, open your key vault. Create a secret named uknows-otlp-headers. Set its value to x-uknows-key=<key>.',
|
|
699
2092
|
'Turn on the system-assigned managed identity of each web app.',
|
|
700
2093
|
'Give each identity the Key Vault Secrets User role on the key vault.',
|
|
701
|
-
'Set APPS and SECRET_URI. Run the script with DRY_RUN=1 first. Then set DRY_RUN=0 and run it again at a quiet time.',
|
|
2094
|
+
'Set SUBSCRIPTION, RESOURCE_GROUP, APPS and SECRET_URI. Run the script with DRY_RUN=1 first. Then set DRY_RUN=0 and run it again at a quiet time.',
|
|
2095
|
+
],
|
|
2096
|
+
snippets: [{ caption: 'Use the Key Vault secret on your web apps', language: 'bash', code: appServiceVaultScript() }],
|
|
2097
|
+
},
|
|
2098
|
+
checks: [
|
|
2099
|
+
{
|
|
2100
|
+
id: 'slot',
|
|
2101
|
+
text: 'If you deploy to a slot, with slot-name in azure/webapps-deploy or with --slot, set the key on each slot as a slot setting, with --slot-settings. Then each slot keeps its own key when you swap.',
|
|
2102
|
+
},
|
|
2103
|
+
],
|
|
2104
|
+
},
|
|
2105
|
+
{
|
|
2106
|
+
id: 'azure-functions',
|
|
2107
|
+
label: 'Azure Functions',
|
|
2108
|
+
summary: 'The setup tool sets up a function app for you, for Node.js, Python and .NET. By hand: add 1 line to host.json, then set the 5 settings and your key as app settings of each function app.',
|
|
2109
|
+
steps: [
|
|
2110
|
+
'Run the setup tool in the repo of your function app. If the app does not send to the monitoring built into Azure, the tool adds 1 line to host.json. If it does, or the tool cannot tell, the tool adds small UKNOWS hooks to the code of the app instead, so that monitoring keeps showing what it shows now. Then it prints the app settings and the key steps for your function apps.',
|
|
2111
|
+
'By hand, for an app that does not send to the monitoring built into Azure: in host.json, add "telemetryMode": "OpenTelemetry" next to "version", as in the example. Commit it and deploy as usual. The Functions host then sends a trace for each call of a function, and the logs. Your code needs no package.',
|
|
2112
|
+
AZURE_BASH_STEP,
|
|
2113
|
+
'Sign in with az login.',
|
|
2114
|
+
'Set SUBSCRIPTION, RESOURCE_GROUP and APPS to your function apps at the top of both scripts. az account list -o table lists your subscriptions. Run the key script with DRY_RUN=1 first. It checks each app and says what it would change.',
|
|
2115
|
+
'Set DRY_RUN=0 and run it again. Paste your key when it asks. The key does not show on the screen.',
|
|
2116
|
+
'Run the settings script. A function app reads its settings from its app settings, not from a file in your repo, and keeps them when you deploy new code. Each app gets its own name in UKNOWS: the name of the function app.',
|
|
2117
|
+
`Azure restarts each function app when its settings change. ${QUIET_TIME}`,
|
|
2118
|
+
],
|
|
2119
|
+
snippets: [
|
|
2120
|
+
{
|
|
2121
|
+
caption: 'host.json: the line to add',
|
|
2122
|
+
language: 'json',
|
|
2123
|
+
code: ['{', ' "version": "2.0",', ' "telemetryMode": "OpenTelemetry"', '}'].join('\n'),
|
|
2124
|
+
},
|
|
2125
|
+
{ caption: 'Set the key on your function apps', language: 'bash', code: functionsKeyScript() },
|
|
2126
|
+
{ caption: 'Add the 5 settings to your function apps', language: 'bash', code: functionsSettingsScript() },
|
|
2127
|
+
],
|
|
2128
|
+
notes: [
|
|
2129
|
+
'Run the scripts on your own computer or in Azure Cloud Shell. Do not run them on a shared server. Other users of a shared server can see the commands that run, and the az command gets the key.',
|
|
2130
|
+
'Tested with Node.js, Python and .NET (isolated worker) function apps on Linux.',
|
|
2131
|
+
'If the function app sends its data to the monitoring built into Azure (it has the app setting APPLICATIONINSIGHTS_CONNECTION_STRING), the line in host.json changes what that monitoring shows. It still gets the requests, but names each one by its route, such as GET api/orders/{id}, instead of the name of the function, and leaves out InvocationId. The function list under Monitor in the Azure portal, and your own queries and alerts on the function name, then change. So for such an app the setup tool adds the UKNOWS hooks instead. The hooks keep that monitoring exactly as it is, and send a span for each call of a function and the logs to UKNOWS, with the same trace id.',
|
|
2132
|
+
'The hooks need the v4 programming model of Node.js, the v2 model of Python or the isolated worker of .NET. For an app on another model that sends to the monitoring built into Azure, the tool changes nothing and says why. To turn on the line in host.json anyway, run npx @uknows/setup --functions-opentelemetry.',
|
|
2133
|
+
'A Python function app with the hooks also needs the app setting PYTHON_ENABLE_WORKER_EXTENSIONS=1. The setup tool lists it with the other app settings.',
|
|
2134
|
+
'With the line in host.json, the log stream of the Azure portal no longer shows the logs of the app.',
|
|
2135
|
+
'For a deployment slot, add --slot with the name of the slot to each az command.',
|
|
2136
|
+
],
|
|
2137
|
+
stricter: {
|
|
2138
|
+
title: 'Stricter option: keep the key in Azure Key Vault',
|
|
2139
|
+
summary: 'The key stays in Key Vault. Each function app reads it with its managed identity. The key never passes through a terminal.',
|
|
2140
|
+
steps: [
|
|
2141
|
+
'In the Azure portal, open your key vault. Create a secret named uknows-otlp-headers. Set its value to x-uknows-key=<key>.',
|
|
2142
|
+
'Turn on the system-assigned managed identity of each function app.',
|
|
2143
|
+
'Give each identity the Key Vault Secrets User role on the key vault.',
|
|
2144
|
+
'Set SUBSCRIPTION, RESOURCE_GROUP, APPS and SECRET_URI. Run the script with DRY_RUN=1 first. Then set DRY_RUN=0 and run it again at a quiet time.',
|
|
702
2145
|
],
|
|
703
2146
|
snippets: [
|
|
704
2147
|
{
|
|
705
|
-
caption: 'Use the Key Vault secret on your
|
|
2148
|
+
caption: 'Use the Key Vault secret on your function apps',
|
|
706
2149
|
language: 'bash',
|
|
707
2150
|
code: [
|
|
708
2151
|
'# Run in bash. This script never sees the key.',
|
|
709
2152
|
'(',
|
|
710
|
-
...dryRun('
|
|
2153
|
+
...dryRun('az functionapp show', 'It would restart the app.').map((line) => ` ${line}`),
|
|
711
2154
|
' SECRET_URI=https://my-vault.vault.azure.net/secrets/uknows-otlp-headers',
|
|
712
2155
|
' for APP in $APPS; do',
|
|
713
|
-
' az
|
|
2156
|
+
' az functionapp config appsettings set --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
714
2157
|
' --settings "OTEL_EXPORTER_OTLP_HEADERS=@Microsoft.KeyVault(SecretUri=$SECRET_URI)" &&',
|
|
715
2158
|
' echo "Set the key on $APP."',
|
|
716
2159
|
' done',
|
|
@@ -725,7 +2168,7 @@ export const HOSTS = [
|
|
|
725
2168
|
label: 'AWS ECS and Fargate',
|
|
726
2169
|
summary: 'Store the key in AWS Secrets Manager. Each task definition reads it as a secret.',
|
|
727
2170
|
steps: [
|
|
728
|
-
|
|
2171
|
+
AWS_BASH_STEP,
|
|
729
2172
|
'Sign in to the AWS CLI, for example with aws sso login.',
|
|
730
2173
|
'Run the script once for each environment. Paste your key when it asks. The script prints the ARN of the secret.',
|
|
731
2174
|
'In the task definition of each app, add the secret to the container. Use the ARN that the script printed.',
|
|
@@ -762,6 +2205,12 @@ export const HOSTS = [
|
|
|
762
2205
|
'You can use AWS Systems Manager Parameter Store instead. Store a SecureString parameter and put its ARN in valueFrom.',
|
|
763
2206
|
'For EKS, use the Kubernetes steps. For EC2, use the VM steps.',
|
|
764
2207
|
],
|
|
2208
|
+
checks: [
|
|
2209
|
+
{
|
|
2210
|
+
id: 'task-definition',
|
|
2211
|
+
text: 'If your repo has the task definition JSON, add the secrets entry to that file and commit it. The ARN is not a secret.',
|
|
2212
|
+
},
|
|
2213
|
+
],
|
|
765
2214
|
},
|
|
766
2215
|
{
|
|
767
2216
|
id: 'kubernetes',
|
|
@@ -798,6 +2247,7 @@ export const HOSTS = [
|
|
|
798
2247
|
' envFrom:',
|
|
799
2248
|
' - secretRef:',
|
|
800
2249
|
' name: uknows',
|
|
2250
|
+
' optional: true',
|
|
801
2251
|
].join('\n'),
|
|
802
2252
|
},
|
|
803
2253
|
{
|
|
@@ -823,6 +2273,7 @@ export const HOSTS = [
|
|
|
823
2273
|
'envFrom:',
|
|
824
2274
|
' - secretRef:',
|
|
825
2275
|
' name: uknows',
|
|
2276
|
+
' optional: true',
|
|
826
2277
|
].join('\n'),
|
|
827
2278
|
},
|
|
828
2279
|
],
|
|
@@ -831,6 +2282,12 @@ export const HOSTS = [
|
|
|
831
2282
|
'The key goes to kubectl through a pipe, not as an argument. Other users of the computer cannot see it.',
|
|
832
2283
|
'To change the key, run the script again. Then run kubectl rollout restart for each deployment.',
|
|
833
2284
|
],
|
|
2285
|
+
checks: [
|
|
2286
|
+
{
|
|
2287
|
+
id: 'optional',
|
|
2288
|
+
text: 'The envFrom lines mark the secret optional: true, so a missing secret never stops pods from starting. Create the secret first. Pods that start without it send no data to UKNOWS.',
|
|
2289
|
+
},
|
|
2290
|
+
],
|
|
834
2291
|
},
|
|
835
2292
|
{
|
|
836
2293
|
id: 'alibaba-sae',
|
|
@@ -866,6 +2323,7 @@ export const HOSTS = [
|
|
|
866
2323
|
`For Docker, add --env-file ${KEY_FILE} to docker run.`,
|
|
867
2324
|
`For systemd, add EnvironmentFile=${KEY_FILE} to the service.`,
|
|
868
2325
|
'Restart the app.',
|
|
2326
|
+
'At the end of each deploy, run the deploy marker below on the host. It tells UKNOWS that a deploy happened, so UKNOWS can warn you when a deploy stops the data of your app. On an Azure VM it finds the resource id of the machine by itself.',
|
|
869
2327
|
],
|
|
870
2328
|
snippets: [
|
|
871
2329
|
{
|
|
@@ -894,6 +2352,7 @@ export const HOSTS = [
|
|
|
894
2352
|
`EnvironmentFile=${KEY_FILE}`,
|
|
895
2353
|
].join('\n'),
|
|
896
2354
|
},
|
|
2355
|
+
{ caption: 'Mark each deploy', language: 'bash', code: vmDeployMarker() },
|
|
897
2356
|
],
|
|
898
2357
|
notes: [
|
|
899
2358
|
'Docker reads the file as the user that runs docker. Run docker with sudo or as root.',
|
|
@@ -910,57 +2369,63 @@ export const HOSTS = [
|
|
|
910
2369
|
summary: 'Store the key as a GitHub environment secret, once for each environment. Your deploy workflow sets it on your app.',
|
|
911
2370
|
steps: [
|
|
912
2371
|
'Install the GitHub CLI. Sign in with gh auth login.',
|
|
913
|
-
'Run the command once for each environment. Paste
|
|
914
|
-
'
|
|
2372
|
+
'Run the command once for each GitHub environment that your deploy jobs use. Paste the key of that environment when it asks. Paste the key only, without x-uknows-key=. The key does not show on the screen.',
|
|
2373
|
+
'Add the step below to each deploy job, before its deploy step. It reads the secret and sets the key on your app. The example is for Azure Container Apps.',
|
|
915
2374
|
],
|
|
916
2375
|
snippets: [
|
|
917
|
-
{
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
code: 'gh secret set OTEL_EXPORTER_OTLP_HEADERS --env production --repo my-org/orders-api',
|
|
921
|
-
},
|
|
922
|
-
{
|
|
923
|
-
caption: 'Set the key in your deploy workflow',
|
|
924
|
-
language: 'yaml',
|
|
925
|
-
code: [
|
|
926
|
-
'# Add this step before your deploy step. The job has environment: production.',
|
|
927
|
-
'# An earlier step signs in to Azure.',
|
|
928
|
-
'- name: Set the UKNOWS key',
|
|
929
|
-
' env:',
|
|
930
|
-
' RESOURCE_GROUP: orders-prod',
|
|
931
|
-
' OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}',
|
|
932
|
-
` run: az containerapp secret set -g "$RESOURCE_GROUP" -n ${EXAMPLE_SERVICE} --secrets "uknows-otlp-headers=$OTEL_EXPORTER_OTLP_HEADERS"`,
|
|
933
|
-
].join('\n'),
|
|
934
|
-
},
|
|
935
|
-
{
|
|
936
|
-
caption: 'Or: 1 organization secret for many repos',
|
|
937
|
-
language: 'bash',
|
|
938
|
-
code: 'gh secret set OTEL_EXPORTER_OTLP_HEADERS --org my-org --visibility private',
|
|
939
|
-
},
|
|
2376
|
+
{ caption: 'Set the environment secret', language: 'bash', code: githubSecretCommands() },
|
|
2377
|
+
{ caption: 'Set the key in your deploy workflow', language: 'yaml', code: githubKeyStep() },
|
|
2378
|
+
{ caption: 'Or: 1 organization secret for many repos', language: 'bash', code: `gh secret set UKNOWS_KEY --org ${EXAMPLE_NAMES.org} --visibility private` },
|
|
940
2379
|
],
|
|
941
2380
|
notes: [
|
|
942
|
-
|
|
943
|
-
|
|
2381
|
+
`Replace ${EXAMPLE_NAMES.repo} with your repo, and ${EXAMPLE_NAMES.environment} with the environment: name of your deploy job. Never rename an environment that your workflows use: its other secrets and variables stay with the old name.`,
|
|
2382
|
+
`A job can read an environment secret only when it has an environment: line. If your deploy job has none, do not add one just for the key. A job that signs in to Azure with OpenID Connect then signs in as environment:<name> instead of ref:refs/heads/<branch>, and azure/login fails until the Azure identity trusts that name. Either add a federated credential for environment:<name> to the identity first, or store the key as a repository secret: gh secret set UKNOWS_KEY --repo ${EXAMPLE_NAMES.repo}.`,
|
|
2383
|
+
`Each environment has its own key. gh secret set UKNOWS_KEY --env ${EXAMPLE_NAMES.environment} sets the key for ${EXAMPLE_NAMES.environment} only. For staging, run it again with --env staging and the staging key.`,
|
|
944
2384
|
'An organization secret holds 1 key. Use it only when all the repos that read it deploy to the same environment.',
|
|
945
2385
|
'--visibility private shares the organization secret with the private repos of the organization. Public repos cannot use it.',
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
2386
|
+
`To share it with some repos only, use --visibility selected --repos ${EXAMPLE_SERVICE},payments instead.`,
|
|
2387
|
+
`In the example, replace ${EXAMPLE_NAMES.resourceGroup} and ${EXAMPLE_SERVICE} with your resource group and app.`,
|
|
2388
|
+
`The app must load the secret. Make your deploy command set ${ACA_SECRET_REF}, as the Azure Container Apps script does. If the command has --set-env-vars already, add the pair to that list: az uses only the last --set-env-vars.`,
|
|
949
2389
|
'On other platforms, pass the secret the same way to the step that sets your app settings.',
|
|
950
2390
|
],
|
|
2391
|
+
checks: [
|
|
2392
|
+
{
|
|
2393
|
+
id: 'guard',
|
|
2394
|
+
text: 'The step above fails the job when the UKNOWS_KEY secret is empty, so the app is never deployed without the key. It sets the key on the app first. Then your deploy step adds the env entry that reads it.',
|
|
2395
|
+
},
|
|
2396
|
+
],
|
|
951
2397
|
},
|
|
952
2398
|
]
|
|
953
2399
|
|
|
954
2400
|
/** Said plainly, on the page and in llms.txt. */
|
|
955
|
-
export const SERVERLESS = '
|
|
2401
|
+
export const SERVERLESS = 'Azure Functions works: the setup tool sets up Node.js, Python and .NET function apps, and its tab has the steps by hand. AWS Lambda, Google Cloud Functions and Alibaba Function Compute are not supported yet. Apps in containers, on Kubernetes and on VMs work.'
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* What the setup tool says when the check cannot reach UKNOWS. The site check can pass and the upload to the ingest
|
|
2405
|
+
* address still fail: they are 2 addresses. The tool fills in {host}, {port} and {seconds}.
|
|
2406
|
+
*/
|
|
2407
|
+
export const CHECK_NETWORK = {
|
|
2408
|
+
keyFine: 'The key is fine, but the test span did not reach UKNOWS.',
|
|
2409
|
+
timeout: 'This computer cannot reach {host} on port {port}: no answer in {seconds} seconds.',
|
|
2410
|
+
refused: '{host} refused the connection on port {port}.',
|
|
2411
|
+
reset: 'The connection to {host} closed before UKNOWS answered.',
|
|
2412
|
+
unreachable: 'This computer cannot reach {host}. Check its network.',
|
|
2413
|
+
dns: 'This computer cannot find {host} in DNS. Check DNS, or the proxy that this computer must use.',
|
|
2414
|
+
other: 'This computer cannot reach {host} ({code}).',
|
|
2415
|
+
ask: 'Ask your network team to allow outbound HTTPS to {host} on port {port}.',
|
|
2416
|
+
where: 'This check tests the network of this computer. Your apps need the same address from where they run. To test that, run the network check in "Check your key" on a host of your app.',
|
|
2417
|
+
proxy: 'This tool does not use HTTPS_PROXY. Run the check on a network that can reach UKNOWS directly.',
|
|
2418
|
+
}
|
|
956
2419
|
|
|
957
2420
|
const KEY_ANSWERS = [
|
|
958
2421
|
{ answer: '"workspace" and "environment"', meaning: 'The key works. It sends data to that workspace and environment.' },
|
|
959
2422
|
{ answer: '"keyState": "retiring"', meaning: 'The key still works until the time in "retiresAt". A new key replaced it. Set the new key.' },
|
|
960
|
-
{ answer: 'ingest_key_invalid', meaning: 'UKNOWS does not know this key. Copy the whole key again, with no spaces or quotes.' },
|
|
2423
|
+
{ answer: 'ingest_key_invalid', meaning: 'UKNOWS does not know this key. Copy the whole key again, with no spaces or quotes. An Admin or an Engineer of your workspace can show it again in Settings › Keys.' },
|
|
961
2424
|
{ answer: 'ingest_key_revoked', meaning: 'The key no longer works. An Admin revoked it, a new key replaced it, or its environment was turned off. Get a new key from an Admin of your workspace.' },
|
|
962
2425
|
{ answer: 'workspace_paused', meaning: 'Your workspace is paused. Talk to us to start it again.' },
|
|
963
2426
|
{ answer: 'rate_limited', meaning: 'Too many checks from your address. Wait 1 minute.' },
|
|
2427
|
+
{ answer: `No answer from ${INGEST_HOST}`, meaning: `The key can still be fine. This computer cannot reach ${INGEST_HOST} on port 443. Ask your network team to allow outbound HTTPS to ${INGEST_HOST}, then check again.` },
|
|
2428
|
+
{ answer: `Cannot find ${INGEST_HOST}`, meaning: `This computer cannot find ${INGEST_HOST} in DNS. Check DNS, or the proxy that this computer must use.` },
|
|
964
2429
|
]
|
|
965
2430
|
|
|
966
2431
|
/**
|
|
@@ -1011,7 +2476,7 @@ export const SETTINGS_FILE_LOADERS = [
|
|
|
1011
2476
|
`# Run as root, or as the owner of ${KEY_FILE}. Only they can read it.`,
|
|
1012
2477
|
'set -a',
|
|
1013
2478
|
`. ./${SETTINGS_FILE.path} # the 5 settings`,
|
|
1014
|
-
`. ${KEY_FILE} # the key file from
|
|
2479
|
+
`. ${KEY_FILE} # the key file from "${STEP_TITLES.secret}"`,
|
|
1015
2480
|
'set +a',
|
|
1016
2481
|
'# Now run the start line for your stack.',
|
|
1017
2482
|
].join('\n'),
|
|
@@ -1028,7 +2493,7 @@ export const SETTINGS_FILE_LOADERS = [
|
|
|
1028
2493
|
'[Service]',
|
|
1029
2494
|
`# The 5 settings, from ${SETTINGS_FILE.path} in your repo.`,
|
|
1030
2495
|
`EnvironmentFile=/opt/${EXAMPLE_SERVICE}/${SETTINGS_FILE.path}`,
|
|
1031
|
-
|
|
2496
|
+
`# The key file from "${STEP_TITLES.secret}".`,
|
|
1032
2497
|
`EnvironmentFile=${KEY_FILE}`,
|
|
1033
2498
|
].join('\n'),
|
|
1034
2499
|
},
|
|
@@ -1049,7 +2514,7 @@ export const SETTINGS_FILE_LOADERS = [
|
|
|
1049
2514
|
{
|
|
1050
2515
|
id: 'alibaba-sae',
|
|
1051
2516
|
label: 'Alibaba Cloud SAE',
|
|
1052
|
-
summary:
|
|
2517
|
+
summary: `Open each app in the SAE console. Add the 5 settings as environment variables. If your stack has a line that loads OpenTelemetry, add it too. Then set the key as "${STEP_TITLES.secret}" says.`,
|
|
1053
2518
|
snippet: {
|
|
1054
2519
|
caption: 'Environment variables',
|
|
1055
2520
|
language: 'text',
|
|
@@ -1059,6 +2524,89 @@ export const SETTINGS_FILE_LOADERS = [
|
|
|
1059
2524
|
},
|
|
1060
2525
|
]
|
|
1061
2526
|
|
|
2527
|
+
/**
|
|
2528
|
+
* When the setup tool finds no Dockerfile, no Kubernetes file and no file in the repo that loads otel.env, it adds
|
|
2529
|
+
* no file. It prints the settings in the form of the platform instead. The page does not show these.
|
|
2530
|
+
*/
|
|
2531
|
+
export const SETTINGS_ELSEWHERE = {
|
|
2532
|
+
title: 'Add these settings where your app runs',
|
|
2533
|
+
why: `This tool found no Dockerfile, no Kubernetes file and no file in your repo that loads ${SETTINGS_FILE.path}. So it did not add ${SETTINGS_FILE.path}.`,
|
|
2534
|
+
/** Instead of why, when the repo has Dockerfiles that this tool did not pick for this service. It fills in {dockerfiles}. */
|
|
2535
|
+
notPicked: `This tool did not pick a Dockerfile for this service. It saw {dockerfiles}. It also found no Kubernetes file and no file in your repo that loads ${SETTINGS_FILE.path}, so it did not add ${SETTINGS_FILE.path}.`,
|
|
2536
|
+
steps: [
|
|
2537
|
+
'Add these settings to the environment of your app, where it runs. Use the form for your platform below.',
|
|
2538
|
+
`Or add ${SETTINGS_FILE.path} with these lines next to your app, and load it when the app starts: EnvironmentFile= in a systemd service, env_file: in Docker Compose, --env-file in docker run, or . ./${SETTINGS_FILE.path} in a start script. Then run npx @uknows/setup again. It keeps the file up to date.`,
|
|
2539
|
+
],
|
|
2540
|
+
/** A repo file loads otel.env, so the setup tool adds the settings there. It fills in {file}, {line} and {path}. */
|
|
2541
|
+
loaded: '{file} line {line} loads {path}, so this tool adds the settings to {path}.',
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/** The settings in the form of 1 platform, for SETTINGS_ELSEWHERE. app is the name of the app on the platform. */
|
|
2545
|
+
export function settingsSnippet(host, pairs, app = EXAMPLE_SERVICE, names = {}) {
|
|
2546
|
+
const env = pairs.map(([name, value]) => `${name}=${value}`)
|
|
2547
|
+
// names.slot (App Service only): the settings go on that deployment slot, as the deploy of the repo does.
|
|
2548
|
+
const az = (command , flag , slot = false) => [
|
|
2549
|
+
'# Run in bash.',
|
|
2550
|
+
...azureNames({ ...names, app: names.app ?? app, slot: slot ? names.slot : undefined }, true).filter((line) => !line.startsWith('[')),
|
|
2551
|
+
`${command} --subscription "$SUBSCRIPTION" -g "$RESOURCE_GROUP" -n "$APP"${slot && names.slot ? ' --slot "$SLOT"' : ''} -o none ${flag} \\`,
|
|
2552
|
+
env.map((line) => ` "${line}"`).join(' \\\n'),
|
|
2553
|
+
].join('\n')
|
|
2554
|
+
switch (host) {
|
|
2555
|
+
case 'azure-app-service':
|
|
2556
|
+
return { caption: 'App settings of your web app', language: 'bash', code: az('az webapp config appsettings set', '--settings', true) }
|
|
2557
|
+
case 'azure-functions':
|
|
2558
|
+
return { caption: 'App settings of your function app', language: 'bash', code: az('az functionapp config appsettings set', '--settings') }
|
|
2559
|
+
case 'azure-container-apps':
|
|
2560
|
+
return { caption: 'Environment variables of your container app', language: 'bash', code: az('az containerapp update', '--set-env-vars') }
|
|
2561
|
+
case 'aws-ecs':
|
|
2562
|
+
return { caption: 'The environment of the container in your task definition', language: 'json',
|
|
2563
|
+
code: ['"environment": [', pairs.map(([name, value]) => ` { "name": "${name}", "value": "${value}" }`).join(',\n'), ']'].join('\n') }
|
|
2564
|
+
case 'kubernetes':
|
|
2565
|
+
return { caption: 'The env of the container in your deployment', language: 'yaml',
|
|
2566
|
+
code: ['env:', ...pairs.flatMap(([name, value]) => [` - name: ${name}`, ` value: "${value}"`])].join('\n') }
|
|
2567
|
+
case 'alibaba-sae':
|
|
2568
|
+
return { caption: 'Environment variables of your app in the SAE console', language: 'text', code: env.join('\n') }
|
|
2569
|
+
default:
|
|
2570
|
+
return { caption: 'The settings', language: 'text', code: env.join('\n') }
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/** Where a person checks their key in their own terminal: the key check of the Reference. */
|
|
2575
|
+
export const KEY_CHECK_URL = `${DOCS_URL}/reference#check`
|
|
2576
|
+
|
|
2577
|
+
/** What a person chose for the AI prompt: the name of the app in UKNOWS, and the environment of the key they will use. */
|
|
2578
|
+
|
|
2579
|
+
/** The app name as OTEL_SERVICE_NAME takes it: lower case letters, digits, dots, dashes and underscores. Empty when none. */
|
|
2580
|
+
export function serviceName(raw) {
|
|
2581
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, '').slice(0, 63)
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/**
|
|
2585
|
+
* The prompt a person copies into an AI coding assistant. It has this build's
|
|
2586
|
+
* addresses and the app and environment the person chose. It never contains a
|
|
2587
|
+
* key: the person gets and stores the key with the key command, in their own terminal.
|
|
2588
|
+
*/
|
|
2589
|
+
export function aiPrompt({ app = '', env = 'Staging' } = {}) {
|
|
2590
|
+
const name = serviceName(app)
|
|
2591
|
+
const named = name ? ` --service-name ${name}` : ''
|
|
2592
|
+
return [
|
|
2593
|
+
'Set up UKNOWS monitoring in this repo with the UKNOWS setup tool.',
|
|
2594
|
+
`First read ${LLMS_URL} and follow its "Rules for AI assistants".`,
|
|
2595
|
+
'',
|
|
2596
|
+
`1. In the root folder of the repo, run: ${setupCommand(` --dry-run --json${named}`)}`,
|
|
2597
|
+
' It changes nothing. Show me the plan: each service, each file it changes and the packages it adds.',
|
|
2598
|
+
`2. When I say yes, run: ${setupCommand(` --yes${named}`)}`,
|
|
2599
|
+
' It makes exactly the change in the plan and asks nothing. If setup stops, change nothing and tell me why.',
|
|
2600
|
+
'3. Build the project and run its tests.',
|
|
2601
|
+
'4. Commit the change, with .uknows/setup.json, on a new branch. Open a pull request named "Add UKNOWS monitoring".',
|
|
2602
|
+
'5. Do not deploy, and do not change business logic.',
|
|
2603
|
+
'',
|
|
2604
|
+
'Never ask me for my UKNOWS key, and never write, print or commit it.',
|
|
2605
|
+
`When you are done, tell me to run ${setupCommand(' key')} in my own terminal, in this repo. I sign in and choose ${env}. Then it checks my ${env} key and stores it where the app runs.`,
|
|
2606
|
+
`Then I merge and deploy as usual, and watch ${name || 'the app'} connect at ${CONNECT_URL}?app=${name || '<app name>'}.`,
|
|
2607
|
+
].join('\n')
|
|
2608
|
+
}
|
|
2609
|
+
|
|
1062
2610
|
export const TROUBLESHOOTING = [
|
|
1063
2611
|
{
|
|
1064
2612
|
id: 'no-data',
|
|
@@ -1067,8 +2615,8 @@ export const TROUBLESHOOTING = [
|
|
|
1067
2615
|
'Send a few requests to your app. UKNOWS shows the requests that your app gets.',
|
|
1068
2616
|
'Restart or redeploy the app after you set the key.',
|
|
1069
2617
|
'Compare the names and values of the 5 settings with this page.',
|
|
1070
|
-
'
|
|
1071
|
-
'Open
|
|
2618
|
+
'Check the key, as "Check your key" says.',
|
|
2619
|
+
'Open Settings › Keys. It shows the uploads UKNOWS rejected for each key, and why.',
|
|
1072
2620
|
`Make sure that the app can reach ${INGEST_HOST} on port 443.`,
|
|
1073
2621
|
'Remove OTEL_SDK_DISABLED, or set it to false.',
|
|
1074
2622
|
'Read the app log for OpenTelemetry export errors. Some SDKs log nothing when UKNOWS refuses the key.',
|
|
@@ -1079,8 +2627,9 @@ export const TROUBLESHOOTING = [
|
|
|
1079
2627
|
problem: 'The key is not accepted (401)',
|
|
1080
2628
|
cause: 'UKNOWS does not accept the key. The key is wrong or in the wrong format, or it was revoked. Your app may not show this. Some OpenTelemetry SDKs, Node.js among them, drop the 401 and log nothing.',
|
|
1081
2629
|
steps: [
|
|
1082
|
-
'
|
|
1083
|
-
'Open
|
|
2630
|
+
'Check the key, as "Check your key" says. It says if the key is invalid or revoked.',
|
|
2631
|
+
'Open Settings › Keys. It shows the uploads UKNOWS rejected for each key, and why.',
|
|
2632
|
+
'If an Admin revoked the key, an Admin creates a new key there.',
|
|
1084
2633
|
'Copy the key again. Set the secret again.',
|
|
1085
2634
|
'Use the format x-uknows-key=<key>. Remove spaces and quotes.',
|
|
1086
2635
|
'Restart the app.',
|
|
@@ -1089,9 +2638,9 @@ export const TROUBLESHOOTING = [
|
|
|
1089
2638
|
{
|
|
1090
2639
|
id: 'forbidden',
|
|
1091
2640
|
problem: 'The app log shows 403 (access stopped)',
|
|
1092
|
-
cause: 'Your workspace is paused or
|
|
2641
|
+
cause: 'Your workspace is paused. A revoked or unknown key gets 401, not 403.',
|
|
1093
2642
|
steps: [
|
|
1094
|
-
'Talk to us to start the workspace again
|
|
2643
|
+
'Talk to us to start the workspace again.',
|
|
1095
2644
|
],
|
|
1096
2645
|
},
|
|
1097
2646
|
{
|
|
@@ -1138,17 +2687,50 @@ export const TROUBLESHOOTING = [
|
|
|
1138
2687
|
steps: [
|
|
1139
2688
|
'Set OTEL_SDK_DISABLED=true on the platform.',
|
|
1140
2689
|
'Restart the app.',
|
|
1141
|
-
'For .NET, set OTEL_DOTNET_AUTO_TRACES_ENABLED=false.',
|
|
2690
|
+
'For .NET, set OTEL_DOTNET_AUTO_TRACES_ENABLED=false and OTEL_DOTNET_AUTO_LOGS_ENABLED=false.',
|
|
2691
|
+
'If your app also sends to the monitoring built into Azure, remove OTEL_EXPORTER_OTLP_ENDPOINT instead, then restart the app. That turns UKNOWS off and keeps that monitoring. OTEL_SDK_DISABLED=true would stop it too.',
|
|
1142
2692
|
'To turn monitoring on again, remove the setting and restart the app.',
|
|
1143
2693
|
],
|
|
1144
2694
|
},
|
|
1145
2695
|
]
|
|
1146
2696
|
|
|
1147
2697
|
/**
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
2698
|
+
* Help that only the setup tool prints, when it stops for what it found in a repo. The pages leave it out: it names
|
|
2699
|
+
* the products that the tool finds, and the public pages name UKNOWS only.
|
|
2700
|
+
*/
|
|
2701
|
+
export const SETUP_TROUBLESHOOTING = [
|
|
2702
|
+
{
|
|
2703
|
+
id: 'otel-wrapper',
|
|
2704
|
+
problem: 'A library in your app brings its own OpenTelemetry',
|
|
2705
|
+
cause: 'A library that is not standard OpenTelemetry, such as a telemetry library of your company, brings OpenTelemetry and can limit its version. Standard OpenTelemetry can conflict with it.',
|
|
2706
|
+
steps: [
|
|
2707
|
+
'Remove that library and the code that calls it. Then your app can use standard OpenTelemetry.',
|
|
2708
|
+
'Or ask the owners of the library to allow a newer OpenTelemetry version.',
|
|
2709
|
+
],
|
|
2710
|
+
},
|
|
2711
|
+
{
|
|
2712
|
+
id: 'app-insights-classic',
|
|
2713
|
+
problem: 'Your app uses an Application Insights SDK that UKNOWS cannot join',
|
|
2714
|
+
cause: 'UKNOWS can add its exporter only beside the Azure Monitor OpenTelemetry packages. It cannot join the .NET SDK 2.x, the Node.js applicationinsights package 2.x, OpenCensus for Python, the Java agent or the codeless attach of App Service.',
|
|
2715
|
+
steps: [
|
|
2716
|
+
'Move to the Azure Monitor OpenTelemetry package for your language: Azure.Monitor.OpenTelemetry.AspNetCore for .NET, @azure/monitor-opentelemetry for Node.js or azure-monitor-opentelemetry for Python.',
|
|
2717
|
+
'Then keep Azure Monitor and add UKNOWS beside it. Use the steps in "Your app uses Azure Monitor?".',
|
|
2718
|
+
],
|
|
2719
|
+
},
|
|
2720
|
+
]
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* How many days UKNOWS keeps workspace data. This is an owner decision. The
|
|
2724
|
+
* trace store keeps traces for 7 or 15 days by default, so no number is
|
|
2725
|
+
* promised yet. Until the owner picks a number and the store is set and
|
|
2726
|
+
* checked, this stays null and the page gives no number. Set it to the
|
|
2727
|
+
* decided number of days, for example 30.
|
|
1153
2728
|
*/
|
|
1154
2729
|
export const RETENTION_DAYS = null
|
|
2730
|
+
|
|
2731
|
+
/** The signals UKNOWS checks every minute for an alert: Refused and Throttled, and the scan and revoked-key alerts
|
|
2732
|
+
when the owner turned them on. */
|
|
2733
|
+
export function checkedEveryMinute() {
|
|
2734
|
+
const alerts = ['Refused', 'Throttled', ...(SCAN_ALERT ? ['scans'] : []), ...(REVOKED_KEY_ALERT ? ['uploads with a revoked key'] : [])]
|
|
2735
|
+
return `${alerts.slice(0, -1).join(', ')} and ${alerts[alerts.length - 1]}`
|
|
2736
|
+
}
|