@uknows/setup 0.0.0-stage → 0.1.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/LICENSE +190 -0
- package/README.md +142 -2
- package/bin/uknows-setup.mjs +14 -0
- package/lib/check.mjs +211 -0
- package/lib/cli.mjs +166 -0
- package/lib/contract-docs.mjs +1154 -0
- package/lib/contract.mjs +66 -0
- package/lib/detect.mjs +575 -0
- package/lib/diff.mjs +147 -0
- package/lib/doc.mjs +98 -0
- package/lib/dockerfile.mjs +372 -0
- package/lib/endpoint.mjs +50 -0
- package/lib/engine.mjs +607 -0
- package/lib/existing.mjs +168 -0
- package/lib/git.mjs +47 -0
- package/lib/hosts.mjs +145 -0
- package/lib/manifest.mjs +112 -0
- package/lib/settings.mjs +280 -0
- package/lib/stacks/dotnet.mjs +165 -0
- package/lib/stacks/guided.mjs +104 -0
- package/lib/stacks/index.mjs +10 -0
- package/lib/stacks/java.mjs +56 -0
- package/lib/stacks/k8s.mjs +31 -0
- package/lib/stacks/node.mjs +235 -0
- package/lib/stacks/otel.mjs +11 -0
- package/lib/stacks/python-bootstrap.mjs +51 -0
- package/lib/stacks/python.mjs +434 -0
- package/lib/types.mjs +161 -0
- package/lib/ui.mjs +106 -0
- package/lib/workspace.mjs +198 -0
- package/package.json +42 -4
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
// GENERATED from src/docs/install.ts by tools/sync-contract.mjs. Do not edit.
|
|
2
|
+
// Change the docs file, then run: node tools/sync-contract.mjs
|
|
3
|
+
/*
|
|
4
|
+
* UKNOWS install content: the single source for the /docs page and for
|
|
5
|
+
* /llms.txt (built by tools/build-installation-docs.mjs).
|
|
6
|
+
*
|
|
7
|
+
* Keep this file data only. It has no imports, so the docs build can load it
|
|
8
|
+
* with a plain TypeScript transpile.
|
|
9
|
+
*
|
|
10
|
+
* Copy rules: plain simple English, one instruction per sentence, active
|
|
11
|
+
* voice, sentence case, digits for numbers, UKNOWS brand only, no em dashes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The site address and the ingest address are build settings. A test build
|
|
16
|
+
* sets VITE_UKNOWS_SITE_URL (for example https://uknows-test.example.com) and
|
|
17
|
+
* VITE_UKNOWS_INGEST_HOST (for example ingest-test.example.com). Without them,
|
|
18
|
+
* the production names below apply. Vite reads import.meta.env. The Node tools
|
|
19
|
+
* that build llms.txt and the setup CLI contract read process.env.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_SITE_URL = 'https://uknows.defacloud.my'
|
|
22
|
+
export const DEFAULT_INGEST_HOST = 'ingest.uknows.defacloud.my'
|
|
23
|
+
|
|
24
|
+
// Written as import.meta.env exactly, so Vite and Vitest replace it. Node has no import.meta.env.
|
|
25
|
+
export const BUILD_ENV = {}
|
|
26
|
+
|
|
27
|
+
/** 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 = BUILD_ENV[name]
|
|
30
|
+
const value = typeof raw === 'string' ? raw.trim().replace(/\/+$/, '') : ''
|
|
31
|
+
if (!value) return fallback
|
|
32
|
+
if (!valid.test(value)) throw new Error(`${name} is not a valid address: ${value}`)
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** https with a host name, or http on this computer for a local test. No path. */
|
|
37
|
+
export const SITE_URL = buildSetting('VITE_UKNOWS_SITE_URL', DEFAULT_SITE_URL, /^(?:https:\/\/[a-z0-9]([a-z0-9.-]*[a-z0-9])?|http:\/\/(?:localhost|127\.0\.0\.1))(?::\d{1,5})?$/i)
|
|
38
|
+
/** A host name only, with an optional port. The apps always send over https. */
|
|
39
|
+
export const INGEST_HOST = buildSetting('VITE_UKNOWS_INGEST_HOST', DEFAULT_INGEST_HOST, /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(?::\d{1,5})?$/i)
|
|
40
|
+
export const DOCS_URL = `${SITE_URL}/docs`
|
|
41
|
+
export const LLMS_URL = `${SITE_URL}/llms.txt`
|
|
42
|
+
export const INGEST_ENDPOINT = `https://${INGEST_HOST}/otlp`
|
|
43
|
+
/** The route that tells whether a key works. The site serves it, not the ingest host. */
|
|
44
|
+
export const PING_URL = `${SITE_URL}/api/v1/ingest/ping`
|
|
45
|
+
export const EXAMPLE_SERVICE = 'orders-api'
|
|
46
|
+
|
|
47
|
+
/** The 5 plain settings. Commit them in the repo of each app. */
|
|
48
|
+
export const SETTINGS = [
|
|
49
|
+
{ name: 'OTEL_SERVICE_NAME', value: EXAMPLE_SERVICE, summary: 'The name of this app in UKNOWS. Use a different name for each app.' },
|
|
50
|
+
{ name: 'OTEL_EXPORTER_OTLP_ENDPOINT', value: INGEST_ENDPOINT, summary: 'The UKNOWS address that gets your data.' },
|
|
51
|
+
{ 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 only.' },
|
|
53
|
+
{ name: 'OTEL_LOGS_EXPORTER', value: 'none', summary: 'Send traces only. Your logs stay where they are now.' },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
/** The 1 secret. Set it once for each environment on the platform, never in the repo. */
|
|
57
|
+
export const SECRET = {
|
|
58
|
+
name: 'OTEL_EXPORTER_OTLP_HEADERS',
|
|
59
|
+
header: 'x-uknows-key',
|
|
60
|
+
placeholder: '<key>',
|
|
61
|
+
value: 'x-uknows-key=<key>',
|
|
62
|
+
summary: 'Set your key as a secret on your platform, once for each environment. Keep it out of your repo.',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const SETTINGS_ENV = SETTINGS.map((setting) => `${setting.name}=${setting.value}`).join('\n')
|
|
66
|
+
export const SECRET_ENV = `${SECRET.name}=${SECRET.value}`
|
|
67
|
+
|
|
68
|
+
export function dockerEnv(extra = []) {
|
|
69
|
+
const pairs = [...SETTINGS.map((setting) => `${setting.name}=${setting.value}`), ...extra]
|
|
70
|
+
return `ENV ${pairs.join(' \\\n ')}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The ENV lines for the final stage of a Dockerfile. */
|
|
74
|
+
export const SETTINGS_DOCKERFILE = dockerEnv()
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The 5 settings as an env file, for apps that do not run from a Dockerfile.
|
|
78
|
+
* It has no key, so it is safe to commit. SETTINGS_FILE_LOADERS shows how to
|
|
79
|
+
* load it.
|
|
80
|
+
*/
|
|
81
|
+
export const SETTINGS_FILE = {
|
|
82
|
+
path: 'otel.env',
|
|
83
|
+
language: 'sh',
|
|
84
|
+
summary: 'Use this file when your app does not start from a Dockerfile. It has no key, so it is safe to commit.',
|
|
85
|
+
content: SETTINGS_ENV,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The setup tool, npx @uknows/setup (packages/uknows-setup). A build with
|
|
90
|
+
* 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. Production
|
|
92
|
+
* gives the plain command.
|
|
93
|
+
*/
|
|
94
|
+
export const SETUP_ADDRESSES = [
|
|
95
|
+
...(SITE_URL === DEFAULT_SITE_URL ? [] : [`UKNOWS_SITE_URL=${SITE_URL}`]),
|
|
96
|
+
...(INGEST_HOST === DEFAULT_INGEST_HOST ? [] : [`UKNOWS_INGEST_ENDPOINT=${INGEST_ENDPOINT}`]),
|
|
97
|
+
]
|
|
98
|
+
const setupCommand = (args = '') => [...SETUP_ADDRESSES, `npx @uknows/setup${args}`].join(' ')
|
|
99
|
+
|
|
100
|
+
export const NETWORK = {
|
|
101
|
+
host: INGEST_HOST,
|
|
102
|
+
port: 443,
|
|
103
|
+
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.`,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Extra steps that the page can show folded, under the title. */
|
|
107
|
+
|
|
108
|
+
/** The upstream OpenTelemetry Java agent, pinned. Checksum from the release checksums.txt. */
|
|
109
|
+
export const JAVA_AGENT = {
|
|
110
|
+
version: '2.32.0',
|
|
111
|
+
file: 'opentelemetry-javaagent.jar',
|
|
112
|
+
url: 'https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.32.0/opentelemetry-javaagent.jar',
|
|
113
|
+
sha256: 'f787eb6c7f3d18e69a431e108a15278d25ee37f83d68b678f621e063f3988f82',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const NODE_REGISTER = '@opentelemetry/auto-instrumentations-node/register'
|
|
117
|
+
/** For ES modules: the loader hook gives each request its route. No space after = so 1 value holds both options. */
|
|
118
|
+
export const NODE_ESM_OPTIONS = `--experimental-loader=@opentelemetry/instrumentation/hook.mjs --import=${NODE_REGISTER}`
|
|
119
|
+
|
|
120
|
+
/** .NET writes its own OpenTelemetry log files. The default folder is not writable on macOS or as a user that is not root. */
|
|
121
|
+
export const DOTNET_LOG_DIRECTORY = 'OTEL_DOTNET_AUTO_LOG_DIRECTORY=/tmp/otel-dotnet'
|
|
122
|
+
|
|
123
|
+
export const GO_INIT = `package main
|
|
124
|
+
|
|
125
|
+
import (
|
|
126
|
+
"context"
|
|
127
|
+
"net/http"
|
|
128
|
+
"os"
|
|
129
|
+
"strings"
|
|
130
|
+
|
|
131
|
+
"go.opentelemetry.io/otel"
|
|
132
|
+
"go.opentelemetry.io/otel/attribute"
|
|
133
|
+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
134
|
+
"go.opentelemetry.io/otel/propagation"
|
|
135
|
+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
136
|
+
"go.opentelemetry.io/otel/trace"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// setupOTel sends traces to the address in the OTEL_ settings.
|
|
140
|
+
// Call the returned function when the app stops. It sends the last data.
|
|
141
|
+
func setupOTel(ctx context.Context) (func(context.Context) error, error) {
|
|
142
|
+
if os.Getenv("OTEL_SDK_DISABLED") == "true" {
|
|
143
|
+
return func(context.Context) error { return nil }, nil
|
|
144
|
+
}
|
|
145
|
+
exporter, err := otlptracehttp.New(ctx)
|
|
146
|
+
if err != nil {
|
|
147
|
+
return nil, err
|
|
148
|
+
}
|
|
149
|
+
provider := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
|
|
150
|
+
otel.SetTracerProvider(provider)
|
|
151
|
+
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
|
152
|
+
propagation.TraceContext{}, propagation.Baggage{},
|
|
153
|
+
))
|
|
154
|
+
return provider.Shutdown, nil
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// withRoute adds the route of each request, such as /orders/{id}, to its
|
|
158
|
+
// trace, after the net/http ServeMux picks the route. Without it, UKNOWS
|
|
159
|
+
// shows all requests of the app as 1 endpoint.
|
|
160
|
+
func withRoute(mux http.Handler) http.Handler {
|
|
161
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
162
|
+
mux.ServeHTTP(w, r)
|
|
163
|
+
// r.Pattern is, for example, "GET /orders/{id}". The route starts at the first /.
|
|
164
|
+
if i := strings.IndexByte(r.Pattern, '/'); i >= 0 {
|
|
165
|
+
setRoute(r, r.Pattern[i:])
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// setRoute adds a route to the trace of the request and names the trace
|
|
171
|
+
// after it, for example GET /orders/{id}.
|
|
172
|
+
func setRoute(r *http.Request, route string) {
|
|
173
|
+
if route != "" {
|
|
174
|
+
span := trace.SpanFromContext(r.Context())
|
|
175
|
+
span.SetName(r.Method + " " + route)
|
|
176
|
+
span.SetAttributes(attribute.String("http.route", route))
|
|
177
|
+
}
|
|
178
|
+
}`
|
|
179
|
+
|
|
180
|
+
export const GO_MAIN = `// Imports for this code: "context", "errors", "log", "net/http",
|
|
181
|
+
// "os", "os/signal", "syscall" and "time".
|
|
182
|
+
|
|
183
|
+
// 1. Start OpenTelemetry at the start of main.
|
|
184
|
+
shutdown, err := setupOTel(context.Background())
|
|
185
|
+
if err != nil {
|
|
186
|
+
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
|
|
191
|
+
|
|
192
|
+
// net/http: wrap the ServeMux. withRoute is in otel.go.
|
|
193
|
+
// 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.
|
|
197
|
+
// import "github.com/go-chi/chi/v5" and otelhttp
|
|
198
|
+
router.Use(func(next http.Handler) http.Handler {
|
|
199
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
200
|
+
next.ServeHTTP(w, r)
|
|
201
|
+
setRoute(r, chi.RouteContext(r.Context()).RoutePattern())
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
handler = otelhttp.NewHandler(router, "http.server")
|
|
205
|
+
|
|
206
|
+
// Gin: add the middleware. Then use the router as the handler.
|
|
207
|
+
// import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
|
208
|
+
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.
|
|
212
|
+
// import echootel "github.com/labstack/echo-otel/v4"
|
|
213
|
+
e.Use(echootel.NewMiddleware(""))
|
|
214
|
+
handler = e
|
|
215
|
+
|
|
216
|
+
// 3. Run the server. On Ctrl+C or SIGTERM, stop the server.
|
|
217
|
+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
218
|
+
defer stop()
|
|
219
|
+
srv := &http.Server{Addr: ":8080", Handler: handler}
|
|
220
|
+
serveErr := make(chan error, 1)
|
|
221
|
+
go func() { serveErr <- srv.ListenAndServe() }()
|
|
222
|
+
select {
|
|
223
|
+
case err = <-serveErr:
|
|
224
|
+
case <-ctx.Done():
|
|
225
|
+
stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
226
|
+
err = srv.Shutdown(stopCtx)
|
|
227
|
+
cancel()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 4. Send the last data before the app exits.
|
|
231
|
+
flushCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
232
|
+
_ = shutdown(flushCtx)
|
|
233
|
+
cancel()
|
|
234
|
+
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
235
|
+
log.Fatal(err)
|
|
236
|
+
}`
|
|
237
|
+
|
|
238
|
+
export const RUBY_INITIALIZER = `# UKNOWS monitoring. The OTEL_ settings configure it.
|
|
239
|
+
unless ENV["OTEL_SDK_DISABLED"] == "true"
|
|
240
|
+
require "opentelemetry/sdk"
|
|
241
|
+
require "opentelemetry/exporter/otlp"
|
|
242
|
+
require "opentelemetry/instrumentation/all"
|
|
243
|
+
|
|
244
|
+
OpenTelemetry::SDK.configure do |c|
|
|
245
|
+
c.use_all
|
|
246
|
+
end
|
|
247
|
+
end`
|
|
248
|
+
|
|
249
|
+
/** Adds UKNOWS beside the exporters that a Collector has now. otlphttp adds /v1/traces to the endpoint. */
|
|
250
|
+
export const COLLECTOR_CONFIG = [
|
|
251
|
+
'exporters:',
|
|
252
|
+
' otlphttp/uknows:',
|
|
253
|
+
` endpoint: ${INGEST_ENDPOINT}`,
|
|
254
|
+
' headers:',
|
|
255
|
+
' x-uknows-key: ${env:UKNOWS_KEY}',
|
|
256
|
+
'',
|
|
257
|
+
'service:',
|
|
258
|
+
' pipelines:',
|
|
259
|
+
' traces:',
|
|
260
|
+
' # Keep your exporters. otlp/current stands for the one you have now.',
|
|
261
|
+
' exporters: [otlp/current, otlphttp/uknows]',
|
|
262
|
+
].join('\n')
|
|
263
|
+
|
|
264
|
+
/** UKNOWS counts the spans it gets. It does not scale them up by a sample rate. */
|
|
265
|
+
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
|
+
|
|
267
|
+
export const STACKS = [
|
|
268
|
+
{
|
|
269
|
+
id: 'node',
|
|
270
|
+
label: 'Node.js',
|
|
271
|
+
summary: 'Add the OpenTelemetry Node.js packages. Load them when the app starts.',
|
|
272
|
+
manifests: ['package.json'],
|
|
273
|
+
frameworks: ['Express', 'Fastify', 'NestJS', 'Koa', 'Hapi'],
|
|
274
|
+
install: 'npm install --save @opentelemetry/api @opentelemetry/auto-instrumentations-node',
|
|
275
|
+
files: [],
|
|
276
|
+
dockerfile: [
|
|
277
|
+
'# Final stage, after your app files and node_modules',
|
|
278
|
+
`ENV NODE_OPTIONS="--require ${NODE_REGISTER}"`,
|
|
279
|
+
SETTINGS_DOCKERFILE,
|
|
280
|
+
'CMD ["node", "server.js"]',
|
|
281
|
+
].join('\n'),
|
|
282
|
+
start: `node --require ${NODE_REGISTER} server.js`,
|
|
283
|
+
// --require= has no space. With a space, a shell that loads otel.env runs the package path as a command.
|
|
284
|
+
platformEnv: [`NODE_OPTIONS=--require=${NODE_REGISTER}`],
|
|
285
|
+
option: {
|
|
286
|
+
title: 'Your app uses ES modules?',
|
|
287
|
+
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.',
|
|
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
|
+
],
|
|
299
|
+
},
|
|
300
|
+
notes: [
|
|
301
|
+
'Replace server.js with the file that starts your server.',
|
|
302
|
+
'Install the packages as normal dependencies. Your production image must include them.',
|
|
303
|
+
'If your image already sets NODE_OPTIONS, add the --require option to that value.',
|
|
304
|
+
'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
|
+
'If your repo uses yarn or pnpm, add the same packages with that tool.',
|
|
306
|
+
'OpenTelemetry listens for SIGTERM to send the last data. Because of that, Node no longer stops on SIGTERM by itself. Add this line to the file that starts your server: process.on("SIGTERM", () => { server.close(); setTimeout(() => process.exit(0), 5000).unref() }). Here server is what app.listen() returns. The line closes your server and stops the app in 5 seconds at most, also when a database or Redis client is still open. Without it, Docker, Kubernetes and systemd wait for their time limit and then kill the app.',
|
|
307
|
+
'Outside Google Cloud, the app log can show a MetadataLookupWarning when the app starts. OpenTelemetry looks for Google Cloud details and finds none. You can ignore it.',
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'python',
|
|
312
|
+
label: 'Python',
|
|
313
|
+
summary: 'Add the OpenTelemetry packages for Python and for the libraries that your app uses. Start the app with opentelemetry-instrument.',
|
|
314
|
+
manifests: ['requirements.txt', 'pyproject.toml', 'Pipfile'],
|
|
315
|
+
frameworks: ['Django', 'Flask', 'FastAPI', 'Starlette', 'Falcon', 'Tornado'],
|
|
316
|
+
install: [
|
|
317
|
+
'[ -z "$(tail -c1 requirements.txt)" ] || echo >> requirements.txt',
|
|
318
|
+
'pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http',
|
|
319
|
+
"pip freeze | grep -E '^opentelemetry-(distro|exporter-otlp-proto-http)==' >> requirements.txt",
|
|
320
|
+
'opentelemetry-bootstrap -a requirements >> requirements.txt',
|
|
321
|
+
'pip install -r requirements.txt',
|
|
322
|
+
].join('\n'),
|
|
323
|
+
files: [],
|
|
324
|
+
dockerfile: [
|
|
325
|
+
'# requirements.txt now lists the OpenTelemetry packages',
|
|
326
|
+
'RUN pip install --no-cache-dir -r requirements.txt',
|
|
327
|
+
SETTINGS_DOCKERFILE,
|
|
328
|
+
'CMD ["opentelemetry-instrument", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]',
|
|
329
|
+
].join('\n'),
|
|
330
|
+
start: 'opentelemetry-instrument uvicorn main:app --host 0.0.0.0 --port 8000',
|
|
331
|
+
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.',
|
|
332
|
+
notes: [
|
|
333
|
+
'Use Python 3.10 or later. The OpenTelemetry packages need it.',
|
|
334
|
+
'Run the install commands in the virtual environment of the app. Install the app packages first, because opentelemetry-bootstrap reads them.',
|
|
335
|
+
'Put opentelemetry-instrument in front of the command that starts your app now.',
|
|
336
|
+
'For Django, set DJANGO_SETTINGS_MODULE before the app starts.',
|
|
337
|
+
'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
|
+
'Remove the --reload option in production.',
|
|
339
|
+
'If your repo uses Poetry or uv, add the same packages with poetry add or uv add.',
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: 'dotnet',
|
|
344
|
+
label: '.NET',
|
|
345
|
+
summary: 'Add the OpenTelemetry.AutoInstrumentation package. Start the app with instrument.sh.',
|
|
346
|
+
manifests: ['*.csproj', '*.sln'],
|
|
347
|
+
frameworks: ['ASP.NET Core', 'Minimal APIs', 'gRPC for ASP.NET Core'],
|
|
348
|
+
install: [
|
|
349
|
+
'dotnet add package OpenTelemetry.AutoInstrumentation',
|
|
350
|
+
'dotnet publish -c Release -r linux-x64 -o ./publish',
|
|
351
|
+
].join('\n'),
|
|
352
|
+
files: [],
|
|
353
|
+
dockerfile: [
|
|
354
|
+
'# Build stage',
|
|
355
|
+
'RUN dotnet publish -c Release -r linux-x64 -o /app/publish',
|
|
356
|
+
'',
|
|
357
|
+
'# Final stage',
|
|
358
|
+
'WORKDIR /app',
|
|
359
|
+
'COPY --from=build /app/publish .',
|
|
360
|
+
dockerEnv([DOTNET_LOG_DIRECTORY]),
|
|
361
|
+
'ENTRYPOINT ["sh", "./instrument.sh", "dotnet", "OrdersApi.dll"]',
|
|
362
|
+
].join('\n'),
|
|
363
|
+
start: 'sh ./publish/instrument.sh dotnet ./publish/OrdersApi.dll',
|
|
364
|
+
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
|
+
extraSettings: {
|
|
366
|
+
lines: [DOTNET_LOG_DIRECTORY],
|
|
367
|
+
why: 'OpenTelemetry writes its log files to this folder. The app must be able to write to it.',
|
|
368
|
+
},
|
|
369
|
+
notes: [
|
|
370
|
+
'Replace OrdersApi.dll with the dll of your app.',
|
|
371
|
+
'Use .NET 8 or later.',
|
|
372
|
+
'The build needs a runtime identifier. Use linux-x64 or linux-arm64. Use linux-musl-x64 for Alpine images. On a Mac, use osx-arm64.',
|
|
373
|
+
'The package puts instrument.sh in the publish folder. The script starts your app with monitoring on.',
|
|
374
|
+
`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
|
+
'If your app uses StackExchange.Redis, 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.',
|
|
376
|
+
'To turn monitoring off for .NET, set OTEL_DOTNET_AUTO_TRACES_ENABLED=false.',
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: 'java',
|
|
381
|
+
label: 'Java',
|
|
382
|
+
summary: `Add the OpenTelemetry Java agent ${JAVA_AGENT.version}. Start the JVM with -javaagent.`,
|
|
383
|
+
manifests: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
384
|
+
frameworks: ['Spring Boot', 'Spring WebFlux', 'Quarkus', 'Micronaut', 'Jakarta EE', 'Dropwizard'],
|
|
385
|
+
install: [
|
|
386
|
+
`curl -fsSL -o ${JAVA_AGENT.file} ${JAVA_AGENT.url}`,
|
|
387
|
+
`echo "${JAVA_AGENT.sha256} ${JAVA_AGENT.file}" | sha256sum -c -`,
|
|
388
|
+
].join('\n'),
|
|
389
|
+
files: [],
|
|
390
|
+
dockerfile: [
|
|
391
|
+
'# Final stage',
|
|
392
|
+
`ADD --checksum=sha256:${JAVA_AGENT.sha256} --chmod=755 ${JAVA_AGENT.url} /otel/${JAVA_AGENT.file}`,
|
|
393
|
+
`ENV JAVA_TOOL_OPTIONS="-javaagent:/otel/${JAVA_AGENT.file}"`,
|
|
394
|
+
SETTINGS_DOCKERFILE,
|
|
395
|
+
'CMD ["java", "-jar", "app.jar"]',
|
|
396
|
+
].join('\n'),
|
|
397
|
+
start: `java -javaagent:./${JAVA_AGENT.file} -jar app.jar`,
|
|
398
|
+
platformEnv: [`JAVA_TOOL_OPTIONS=-javaagent:/path/to/${JAVA_AGENT.file}`],
|
|
399
|
+
notes: [
|
|
400
|
+
`The agent is version ${JAVA_AGENT.version}. Check its SHA-256 checksum before you use it.`,
|
|
401
|
+
'In the JAVA_TOOL_OPTIONS line, replace /path/to with the folder of the agent.',
|
|
402
|
+
'On macOS, use shasum -a 256 -c - instead of sha256sum -c -.',
|
|
403
|
+
'Replace app.jar with your built app. Put -javaagent before -jar. Keep your other JVM options.',
|
|
404
|
+
'If your image already sets JAVA_TOOL_OPTIONS, add the -javaagent option to that value.',
|
|
405
|
+
'Keep --chmod=755 in the ADD line. It lets an app that runs as a non-root user open the /otel folder and load the agent.',
|
|
406
|
+
'ADD --checksum needs Docker 25 or later. On older Docker, put # syntax=docker/dockerfile:1 on the first line of the Dockerfile.',
|
|
407
|
+
'Use Java 8 or later. Use a JVM build. Native images need the OpenTelemetry starter for your framework.',
|
|
408
|
+
'To turn the agent off completely, set OTEL_JAVAAGENT_ENABLED=false.',
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: 'go',
|
|
413
|
+
label: 'Go',
|
|
414
|
+
summary: 'Add the OpenTelemetry Go SDK and the middleware for your router. Add a short init file.',
|
|
415
|
+
manifests: ['go.mod'],
|
|
416
|
+
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'),
|
|
427
|
+
files: [
|
|
428
|
+
{ path: 'otel.go', language: 'go', summary: 'Add this file to your main package.', content: GO_INIT },
|
|
429
|
+
{ path: 'main.go', language: 'go', summary: 'Change main like this. Keep only the middleware for your router.', content: GO_MAIN },
|
|
430
|
+
],
|
|
431
|
+
dockerfile: [
|
|
432
|
+
'# Build stage: no change. otel.go builds into your binary.',
|
|
433
|
+
'# Final stage',
|
|
434
|
+
SETTINGS_DOCKERFILE,
|
|
435
|
+
].join('\n'),
|
|
436
|
+
start: `go build -o ${EXAMPLE_SERVICE} . && ./${EXAMPLE_SERVICE}`,
|
|
437
|
+
notes: [
|
|
438
|
+
'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.',
|
|
439
|
+
'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
|
+
'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
|
+
'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.',
|
|
442
|
+
'For chi, add the route middleware before your routes. chi stops with an error when a middleware comes after a route.',
|
|
443
|
+
'For another router, call setRoute with the route of the request after the router picks it.',
|
|
444
|
+
'For outgoing HTTP calls, use otelhttp.NewTransport in your HTTP client. Pass the request context to each call.',
|
|
445
|
+
'For Echo v5, use github.com/labstack/echo-otel/v5.',
|
|
446
|
+
'Run go mod tidy. Then run your tests and build.',
|
|
447
|
+
'These steps add code at startup only. Your business logic stays the same.',
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: 'php',
|
|
452
|
+
label: 'PHP',
|
|
453
|
+
summary: 'Add the OpenTelemetry extension and the Composer packages. Turn on autoload with OTEL_PHP_AUTOLOAD_ENABLED.',
|
|
454
|
+
manifests: ['composer.json'],
|
|
455
|
+
frameworks: ['Laravel', 'Symfony', 'Slim'],
|
|
456
|
+
install: [
|
|
457
|
+
'pecl install opentelemetry',
|
|
458
|
+
'composer config allow-plugins.php-http/discovery false',
|
|
459
|
+
'composer require --ignore-platform-req=ext-opentelemetry open-telemetry/sdk open-telemetry/exporter-otlp open-telemetry/opentelemetry-auto-laravel',
|
|
460
|
+
].join('\n'),
|
|
461
|
+
files: [
|
|
462
|
+
{ path: 'php.ini', language: 'ini', summary: 'Turn on the extension. Check it with php -m | grep opentelemetry.', content: '[opentelemetry]\nextension=opentelemetry.so' },
|
|
463
|
+
],
|
|
464
|
+
dockerfile: [
|
|
465
|
+
'# Official php images',
|
|
466
|
+
'RUN pecl install opentelemetry \\',
|
|
467
|
+
' && docker-php-ext-enable opentelemetry',
|
|
468
|
+
dockerEnv(['OTEL_PHP_AUTOLOAD_ENABLED=true']),
|
|
469
|
+
].join('\n'),
|
|
470
|
+
start: 'OTEL_PHP_AUTOLOAD_ENABLED=true php artisan serve --no-reload',
|
|
471
|
+
platformEnv: ['OTEL_PHP_AUTOLOAD_ENABLED=true'],
|
|
472
|
+
notes: [
|
|
473
|
+
'Set OTEL_PHP_AUTOLOAD_ENABLED=true with the 5 settings. PHP needs it to start OpenTelemetry.',
|
|
474
|
+
'Keep --no-reload with php artisan serve. Without it, artisan serve does not pass the OTEL_ settings to your app.',
|
|
475
|
+
'For Symfony, use open-telemetry/opentelemetry-auto-symfony instead of the Laravel package.',
|
|
476
|
+
'For Slim, use open-telemetry/opentelemetry-auto-slim instead of the Laravel package.',
|
|
477
|
+
'The exporter needs a PSR-18 HTTP client. Laravel apps include Guzzle, which works. For other apps, run composer require guzzlehttp/guzzle.',
|
|
478
|
+
'The --ignore-platform-req option lets composer add the packages on a computer without the extension. The image installs the extension. If your Dockerfile runs composer install in a stage without the extension, such as the composer image, add --ignore-platform-req=ext-opentelemetry to that line too.',
|
|
479
|
+
'PHP-FPM removes environment variables by default. Set clear_env = no in the pool config, or pass each setting with env[NAME].',
|
|
480
|
+
'On Alpine images, install the build tools first with apk add --no-cache $PHPIZE_DEPS.',
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
id: 'ruby',
|
|
485
|
+
label: 'Ruby',
|
|
486
|
+
summary: 'Add the OpenTelemetry gems. Add an initializer that turns on all instrumentation.',
|
|
487
|
+
manifests: ['Gemfile'],
|
|
488
|
+
frameworks: ['Rails', 'Sinatra', 'Rack'],
|
|
489
|
+
install: 'bundle add opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-all',
|
|
490
|
+
files: [
|
|
491
|
+
{ path: 'config/initializers/opentelemetry.rb', language: 'ruby', summary: 'Add this file for Rails.', content: RUBY_INITIALIZER },
|
|
492
|
+
],
|
|
493
|
+
dockerfile: [
|
|
494
|
+
'# bundle install adds the gems from Gemfile.lock',
|
|
495
|
+
'RUN bundle install',
|
|
496
|
+
SETTINGS_DOCKERFILE,
|
|
497
|
+
'CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]',
|
|
498
|
+
].join('\n'),
|
|
499
|
+
start: 'bundle exec rails server',
|
|
500
|
+
notes: [
|
|
501
|
+
'For Sinatra, put the same code in config.ru after require "sinatra" and before your app loads.',
|
|
502
|
+
'For a plain Rack app, put the same code in config.ru. Below it, add use(*OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.middleware_args).',
|
|
503
|
+
'Commit Gemfile and Gemfile.lock.',
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: 'otel',
|
|
508
|
+
label: 'I already use OpenTelemetry',
|
|
509
|
+
summary: 'Keep your OpenTelemetry setup. Point it at UKNOWS with the same settings.',
|
|
510
|
+
manifests: ['package.json', 'requirements.txt', 'pyproject.toml', '*.csproj', 'pom.xml', 'build.gradle', 'go.mod', 'composer.json', 'Gemfile'],
|
|
511
|
+
frameworks: ['Any app that already lists OpenTelemetry packages: @opentelemetry/*, opentelemetry-*, OpenTelemetry.*, io.opentelemetry, go.opentelemetry.io or open-telemetry/*'],
|
|
512
|
+
install: '# No new packages. Keep the OpenTelemetry packages that you use now.',
|
|
513
|
+
files: [],
|
|
514
|
+
dockerfile: ['# Final stage. Your SDK reads these settings at startup.', SETTINGS_DOCKERFILE].join('\n'),
|
|
515
|
+
start: '# Keep your start command. Your SDK reads the OTEL_ settings when the app starts.',
|
|
516
|
+
warning: 'These settings send your traces to UKNOWS instead of the backend that gets them now. OTEL_METRICS_EXPORTER=none and OTEL_LOGS_EXPORTER=none also stop your metrics and logs. To keep your current backend, add UKNOWS to an OpenTelemetry Collector instead. See "Keep your current backend".',
|
|
517
|
+
option: {
|
|
518
|
+
title: 'Keep your current backend: add UKNOWS to your Collector',
|
|
519
|
+
summary: 'Your apps keep their settings and send to your OpenTelemetry Collector. The Collector sends traces to your current backend and to UKNOWS.',
|
|
520
|
+
steps: [
|
|
521
|
+
'Add the otlphttp/uknows exporter below to your Collector config.',
|
|
522
|
+
'Add otlphttp/uknows to the exporters of your traces pipeline. Keep the exporters that you have.',
|
|
523
|
+
'Set UKNOWS_KEY as a secret on the Collector, as in step 3. Use the key only, without x-uknows-key=.',
|
|
524
|
+
'Restart the Collector.',
|
|
525
|
+
'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.',
|
|
526
|
+
],
|
|
527
|
+
snippets: [{ caption: 'Collector config', language: 'yaml', code: COLLECTOR_CONFIG }],
|
|
528
|
+
},
|
|
529
|
+
notes: [
|
|
530
|
+
'Remove exporter addresses and headers from your code. Then the OTEL_ settings apply.',
|
|
531
|
+
'Set OTEL_TRACES_EXPORTER to otlp, or remove it.',
|
|
532
|
+
'UKNOWS takes OTLP over HTTP. If your code creates a gRPC exporter, change it to the HTTP exporter.',
|
|
533
|
+
'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".',
|
|
535
|
+
SAMPLING_NOTE,
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The settings of 1 stack as env lines, for otel.env and the settings tab:
|
|
542
|
+
* the 5 settings, then for each extra group a # line and its lines. A shell,
|
|
543
|
+
* systemd and docker run --env-file skip the # lines.
|
|
544
|
+
*/
|
|
545
|
+
export function stackSettingsEnv(stack) {
|
|
546
|
+
const lines = [SETTINGS_ENV]
|
|
547
|
+
if (stack.extraSettings) lines.push(`# For ${stack.label}: ${stack.extraSettings.why}`, ...stack.extraSettings.lines)
|
|
548
|
+
if (stack.platformEnv?.length) lines.push(`# For ${stack.label}: this loads OpenTelemetry when the app starts.`, ...stack.platformEnv)
|
|
549
|
+
return lines.join('\n')
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** A stricter way to store the key, shown folded under the host. */
|
|
553
|
+
|
|
554
|
+
/** The prompt line, exactly. bash only: zsh reads -p as a coprocess. */
|
|
555
|
+
export const KEY_PROMPT = "read -rsp 'Paste your UKNOWS key: ' UKNOWS_KEY; echo"
|
|
556
|
+
/** Stops the script, not the terminal, when no key was pasted. */
|
|
557
|
+
export const KEY_EMPTY_GUARD = '[ -n "$UKNOWS_KEY" ] || { echo "No key entered. Nothing changed."; exit 1; }'
|
|
558
|
+
/** The path of the key file on a VM or Docker host. Only root can read it. */
|
|
559
|
+
export const KEY_FILE = '/etc/uknows.env'
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* A bash script that asks for the key without showing it. The ( ) block is a
|
|
563
|
+
* subshell: exit stops the block, not the terminal, and the key is gone when
|
|
564
|
+
* the block ends. bash reads the whole block before it runs, so pasted lines
|
|
565
|
+
* never become the key.
|
|
566
|
+
*/
|
|
567
|
+
export function keyScript(setup, body, where = 'Run in bash') {
|
|
568
|
+
return [
|
|
569
|
+
`# ${where}. The key does not show on the screen. It is gone when the ( ) block ends.`,
|
|
570
|
+
'(',
|
|
571
|
+
...[...setup, KEY_PROMPT, KEY_EMPTY_GUARD, ...body].map((line) => ` ${line}`),
|
|
572
|
+
')',
|
|
573
|
+
].join('\n')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export const BASH_STEP = 'Open a bash terminal. On macOS, type bash first. On Windows, use Azure Cloud Shell or WSL.'
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* The lines before the key prompt of a script that changes apps one by one.
|
|
580
|
+
* With DRY_RUN=1 the script only checks that each app exists and says what it
|
|
581
|
+
* would change. It never asks for the key.
|
|
582
|
+
*/
|
|
583
|
+
export function dryRun(apps, show, change) {
|
|
584
|
+
return [
|
|
585
|
+
'RESOURCE_GROUP=orders-prod',
|
|
586
|
+
`APPS="${apps}" # only these apps get the key`,
|
|
587
|
+
'DRY_RUN=1 # 1: list the changes only. 0: make them.',
|
|
588
|
+
'if [ "$DRY_RUN" = 1 ]; then',
|
|
589
|
+
' for APP in $APPS; do',
|
|
590
|
+
` ${show} -g "$RESOURCE_GROUP" -n "$APP" -o none &&`,
|
|
591
|
+
` echo "Would set the key on $APP. ${change}"`,
|
|
592
|
+
' done',
|
|
593
|
+
' exit 0',
|
|
594
|
+
'fi',
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export const QUIET_TIME = 'Run it at a quiet time.'
|
|
599
|
+
|
|
600
|
+
export const HOSTS = [
|
|
601
|
+
{
|
|
602
|
+
id: 'azure-container-apps',
|
|
603
|
+
label: 'Azure Container Apps',
|
|
604
|
+
summary: 'Set the key as a secret on the container apps that you name.',
|
|
605
|
+
steps: [
|
|
606
|
+
BASH_STEP,
|
|
607
|
+
'Sign in with az login.',
|
|
608
|
+
'Set RESOURCE_GROUP to the resource group of your apps. Set APPS to the apps that send data to UKNOWS. To list the apps, run az containerapp list -g orders-prod --query "[].name" -o tsv.',
|
|
609
|
+
'Run the script with DRY_RUN=1 first. It checks each app and says what it would change. It changes nothing.',
|
|
610
|
+
'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 starts a new revision with the key. ${QUIET_TIME}`,
|
|
612
|
+
],
|
|
613
|
+
snippets: [
|
|
614
|
+
{
|
|
615
|
+
caption: 'Set the key on your container apps',
|
|
616
|
+
language: 'bash',
|
|
617
|
+
code: keyScript(dryRun('orders-api payments', 'az containerapp show', 'It would start a new revision.'), [
|
|
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
|
+
]),
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
notes: [
|
|
629
|
+
'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.',
|
|
630
|
+
'When you add a new app, run the script again with APPS set to the new app.',
|
|
631
|
+
'If an app has more than 1 container, add --container-name to the update command.',
|
|
632
|
+
],
|
|
633
|
+
stricter: {
|
|
634
|
+
title: 'Stricter option: keep the key in Azure Key Vault',
|
|
635
|
+
summary: 'The key stays in Key Vault. Each app reads it with its managed identity. The key never passes through a terminal.',
|
|
636
|
+
steps: [
|
|
637
|
+
'In the Azure portal, open your key vault. Create a secret named uknows-otlp-headers. Set its value to x-uknows-key=<key>.',
|
|
638
|
+
'Turn on the system-assigned managed identity of each container app.',
|
|
639
|
+
'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
|
+
},
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: 'azure-app-service',
|
|
666
|
+
label: 'Azure App Service',
|
|
667
|
+
summary: 'Set the key as an app setting on the web apps that you name.',
|
|
668
|
+
steps: [
|
|
669
|
+
BASH_STEP,
|
|
670
|
+
'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.',
|
|
672
|
+
'Set DRY_RUN=0 and run it again. Paste your key when it asks. The key does not show on the screen.',
|
|
673
|
+
`App Service restarts each app with the key. ${QUIET_TIME}`,
|
|
674
|
+
],
|
|
675
|
+
snippets: [
|
|
676
|
+
{
|
|
677
|
+
caption: 'Set the key on your web apps',
|
|
678
|
+
language: 'bash',
|
|
679
|
+
code: keyScript(dryRun('orders-api payments', 'az webapp show', 'It would restart the app.'), [
|
|
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
|
+
]),
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
notes: [
|
|
689
|
+
'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
|
+
'If your app does not start from your Dockerfile, add the 5 settings as app settings too.',
|
|
691
|
+
'For Python and .NET, put the start line of your stack in the startup command of the app.',
|
|
692
|
+
'For a deployment slot, add --slot with the name of the slot to both az commands.',
|
|
693
|
+
],
|
|
694
|
+
stricter: {
|
|
695
|
+
title: 'Stricter option: keep the key in Azure Key Vault',
|
|
696
|
+
summary: 'The key stays in Key Vault. Each app reads it with its managed identity. The key never passes through a terminal.',
|
|
697
|
+
steps: [
|
|
698
|
+
'In the Azure portal, open your key vault. Create a secret named uknows-otlp-headers. Set its value to x-uknows-key=<key>.',
|
|
699
|
+
'Turn on the system-assigned managed identity of each web app.',
|
|
700
|
+
'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.',
|
|
702
|
+
],
|
|
703
|
+
snippets: [
|
|
704
|
+
{
|
|
705
|
+
caption: 'Use the Key Vault secret on your web apps',
|
|
706
|
+
language: 'bash',
|
|
707
|
+
code: [
|
|
708
|
+
'# Run in bash. This script never sees the key.',
|
|
709
|
+
'(',
|
|
710
|
+
...dryRun('orders-api payments', 'az webapp show', 'It would restart the app.').map((line) => ` ${line}`),
|
|
711
|
+
' SECRET_URI=https://my-vault.vault.azure.net/secrets/uknows-otlp-headers',
|
|
712
|
+
' for APP in $APPS; do',
|
|
713
|
+
' az webapp config appsettings set -g "$RESOURCE_GROUP" -n "$APP" -o none \\',
|
|
714
|
+
' --settings "OTEL_EXPORTER_OTLP_HEADERS=@Microsoft.KeyVault(SecretUri=$SECRET_URI)" &&',
|
|
715
|
+
' echo "Set the key on $APP."',
|
|
716
|
+
' done',
|
|
717
|
+
')',
|
|
718
|
+
].join('\n'),
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
id: 'aws-ecs',
|
|
725
|
+
label: 'AWS ECS and Fargate',
|
|
726
|
+
summary: 'Store the key in AWS Secrets Manager. Each task definition reads it as a secret.',
|
|
727
|
+
steps: [
|
|
728
|
+
'Open a bash terminal. On macOS, type bash first. On Windows, use AWS CloudShell or WSL.',
|
|
729
|
+
'Sign in to the AWS CLI, for example with aws sso login.',
|
|
730
|
+
'Run the script once for each environment. Paste your key when it asks. The script prints the ARN of the secret.',
|
|
731
|
+
'In the task definition of each app, add the secret to the container. Use the ARN that the script printed.',
|
|
732
|
+
'Let the task execution role of each app read the secret: it needs secretsmanager:GetSecretValue on it.',
|
|
733
|
+
`Register the new task definition. Then update the service to use it. ECS replaces the running tasks. ${QUIET_TIME}`,
|
|
734
|
+
],
|
|
735
|
+
snippets: [
|
|
736
|
+
{
|
|
737
|
+
caption: 'Store the key in Secrets Manager',
|
|
738
|
+
language: 'bash',
|
|
739
|
+
code: keyScript(['SECRET_NAME=uknows-otlp-headers-prod'], [
|
|
740
|
+
"printf 'x-uknows-key=%s' \"$UKNOWS_KEY\" \\",
|
|
741
|
+
' | aws secretsmanager create-secret --name "$SECRET_NAME" \\',
|
|
742
|
+
' --secret-string file:///dev/stdin --query ARN --output text',
|
|
743
|
+
]),
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
caption: 'Add the secret to the container in your task definition',
|
|
747
|
+
language: 'json',
|
|
748
|
+
code: [
|
|
749
|
+
'"secrets": [',
|
|
750
|
+
' {',
|
|
751
|
+
' "name": "OTEL_EXPORTER_OTLP_HEADERS",',
|
|
752
|
+
' "valueFrom": "arn:aws:secretsmanager:<region>:<account>:secret:uknows-otlp-headers-prod-AbCdEf"',
|
|
753
|
+
' }',
|
|
754
|
+
']',
|
|
755
|
+
].join('\n'),
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
notes: [
|
|
759
|
+
'The key goes to the AWS CLI through a pipe, not as an argument. Other users of the computer cannot see it.',
|
|
760
|
+
'If your image does not have ENV lines for the 5 settings, add the 5 settings to the environment of the container in the task definition.',
|
|
761
|
+
'ECS reads the secret when a task starts. To change the key, store the new key with aws secretsmanager put-secret-value. Then update the service with --force-new-deployment.',
|
|
762
|
+
'You can use AWS Systems Manager Parameter Store instead. Store a SecureString parameter and put its ARN in valueFrom.',
|
|
763
|
+
'For EKS, use the Kubernetes steps. For EC2, use the VM steps.',
|
|
764
|
+
],
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
id: 'kubernetes',
|
|
768
|
+
label: 'Kubernetes (any cluster)',
|
|
769
|
+
summary: 'Store the key in a Kubernetes secret. Load it into each deployment with envFrom.',
|
|
770
|
+
steps: [
|
|
771
|
+
BASH_STEP,
|
|
772
|
+
'Create the secret once in each namespace that runs your apps. Paste your key when the script asks.',
|
|
773
|
+
'Add envFrom to each deployment. Commit this change with the 5 settings.',
|
|
774
|
+
'If your image does not have ENV lines for the 5 settings, add them to env in the deployment too.',
|
|
775
|
+
'Apply the deployment. Kubernetes starts new pods with the key.',
|
|
776
|
+
],
|
|
777
|
+
snippets: [
|
|
778
|
+
{
|
|
779
|
+
caption: 'Create the secret',
|
|
780
|
+
language: 'bash',
|
|
781
|
+
code: keyScript(['NAMESPACE=orders'], [
|
|
782
|
+
"printf 'x-uknows-key=%s' \"$UKNOWS_KEY\" \\",
|
|
783
|
+
' | kubectl create secret generic uknows -n "$NAMESPACE" \\',
|
|
784
|
+
' --from-file=OTEL_EXPORTER_OTLP_HEADERS=/dev/stdin --dry-run=client -o yaml \\',
|
|
785
|
+
' | kubectl apply -f -',
|
|
786
|
+
]),
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
caption: 'Load the secret in your deployment',
|
|
790
|
+
language: 'yaml',
|
|
791
|
+
code: [
|
|
792
|
+
'# Adds OTEL_EXPORTER_OTLP_HEADERS from the uknows secret.',
|
|
793
|
+
'spec:',
|
|
794
|
+
' template:',
|
|
795
|
+
' spec:',
|
|
796
|
+
' containers:',
|
|
797
|
+
` - name: ${EXAMPLE_SERVICE}`,
|
|
798
|
+
' envFrom:',
|
|
799
|
+
' - secretRef:',
|
|
800
|
+
' name: uknows',
|
|
801
|
+
].join('\n'),
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
caption: 'The 5 settings in your deployment, if your image does not have them',
|
|
805
|
+
language: 'yaml',
|
|
806
|
+
code: [
|
|
807
|
+
'spec:',
|
|
808
|
+
' template:',
|
|
809
|
+
' spec:',
|
|
810
|
+
' containers:',
|
|
811
|
+
` - name: ${EXAMPLE_SERVICE}`,
|
|
812
|
+
' env:',
|
|
813
|
+
...SETTINGS.flatMap((setting) => [` - name: ${setting.name}`, ` value: ${setting.value}`]),
|
|
814
|
+
].join('\n'),
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
caption: 'Helm: the same in values.yaml, if your chart passes env and envFrom to the container',
|
|
818
|
+
language: 'yaml',
|
|
819
|
+
code: [
|
|
820
|
+
'# Charts differ. Use the keys that your chart reads.',
|
|
821
|
+
'env:',
|
|
822
|
+
...SETTINGS.flatMap((setting) => [` - name: ${setting.name}`, ` value: ${setting.value}`]),
|
|
823
|
+
'envFrom:',
|
|
824
|
+
' - secretRef:',
|
|
825
|
+
' name: uknows',
|
|
826
|
+
].join('\n'),
|
|
827
|
+
},
|
|
828
|
+
],
|
|
829
|
+
notes: [
|
|
830
|
+
'The same steps work on any cluster: Amazon EKS, Azure AKS, Google GKE, Alibaba ACK and your own clusters.',
|
|
831
|
+
'The key goes to kubectl through a pipe, not as an argument. Other users of the computer cannot see it.',
|
|
832
|
+
'To change the key, run the script again. Then run kubectl rollout restart for each deployment.',
|
|
833
|
+
],
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
id: 'alibaba-sae',
|
|
837
|
+
label: 'Alibaba Cloud SAE',
|
|
838
|
+
summary: 'Store the key in an SAE secret. Reference it in the environment variables of each app.',
|
|
839
|
+
steps: [
|
|
840
|
+
'Open the SAE console. Select the region and namespace of your apps.',
|
|
841
|
+
'Create a secret named uknows with the key and value below.',
|
|
842
|
+
'Open each app. Edit its environment variables.',
|
|
843
|
+
'Add a variable named OTEL_EXPORTER_OTLP_HEADERS. Make it reference the secret uknows and the key OTEL_EXPORTER_OTLP_HEADERS.',
|
|
844
|
+
'If your image does not have ENV lines for the 5 settings, add the 5 settings as environment variables too.',
|
|
845
|
+
'Deploy the app. SAE restarts it with the key.',
|
|
846
|
+
],
|
|
847
|
+
snippets: [
|
|
848
|
+
{
|
|
849
|
+
caption: 'Secret content',
|
|
850
|
+
language: 'text',
|
|
851
|
+
code: ['Secret name: uknows', 'Key: OTEL_EXPORTER_OTLP_HEADERS', `Value: ${SECRET.value}`].join('\n'),
|
|
852
|
+
},
|
|
853
|
+
],
|
|
854
|
+
notes: [
|
|
855
|
+
'Replace <key> with your key in the SAE console. Do not put the key in a file or a chat.',
|
|
856
|
+
'SAE restarts the app when you deploy. Deploy at a quiet time.',
|
|
857
|
+
],
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
id: 'vm-docker',
|
|
861
|
+
label: 'Any VM or Docker',
|
|
862
|
+
summary: `Store the key in ${KEY_FILE} on each host. Load the file when the app starts.`,
|
|
863
|
+
steps: [
|
|
864
|
+
'On each host, run the script in bash. Paste your key when it asks.',
|
|
865
|
+
`The script creates ${KEY_FILE}. Only root can read it.`,
|
|
866
|
+
`For Docker, add --env-file ${KEY_FILE} to docker run.`,
|
|
867
|
+
`For systemd, add EnvironmentFile=${KEY_FILE} to the service.`,
|
|
868
|
+
'Restart the app.',
|
|
869
|
+
],
|
|
870
|
+
snippets: [
|
|
871
|
+
{
|
|
872
|
+
caption: 'Create the key file',
|
|
873
|
+
language: 'bash',
|
|
874
|
+
code: keyScript([], [
|
|
875
|
+
`sudo touch ${KEY_FILE}`,
|
|
876
|
+
`sudo chmod 600 ${KEY_FILE}`,
|
|
877
|
+
`printf 'OTEL_EXPORTER_OTLP_HEADERS=x-uknows-key=%s\\n' "$UKNOWS_KEY" | sudo tee ${KEY_FILE} > /dev/null &&`,
|
|
878
|
+
`echo "Saved the key in ${KEY_FILE}."`,
|
|
879
|
+
], 'Run in bash on each host'),
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
caption: 'Docker',
|
|
883
|
+
language: 'bash',
|
|
884
|
+
code: ['# Loads OTEL_EXPORTER_OTLP_HEADERS from the key file.', `sudo docker run -d --env-file ${KEY_FILE} ${EXAMPLE_SERVICE}:latest`].join('\n'),
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
caption: 'systemd service',
|
|
888
|
+
language: 'ini',
|
|
889
|
+
code: [
|
|
890
|
+
'[Service]',
|
|
891
|
+
`# The 5 settings, from ${SETTINGS_FILE.path} in your repo.`,
|
|
892
|
+
`EnvironmentFile=/opt/${EXAMPLE_SERVICE}/${SETTINGS_FILE.path}`,
|
|
893
|
+
'# Loads OTEL_EXPORTER_OTLP_HEADERS from the key file.',
|
|
894
|
+
`EnvironmentFile=${KEY_FILE}`,
|
|
895
|
+
].join('\n'),
|
|
896
|
+
},
|
|
897
|
+
],
|
|
898
|
+
notes: [
|
|
899
|
+
'Docker reads the file as the user that runs docker. Run docker with sudo or as root.',
|
|
900
|
+
'If your image does not have ENV lines for the 5 settings, add --env-file otel.env to docker run too.',
|
|
901
|
+
`For Docker Compose, add env_file: ${KEY_FILE} to the service.`,
|
|
902
|
+
'After you change a systemd service, run sudo systemctl daemon-reload. Then restart the service.',
|
|
903
|
+
`If you start the app from a shell as a user that is not root, make that user the owner of the key file: sudo chown <user> ${KEY_FILE}.`,
|
|
904
|
+
'The same steps work on any Linux VM or server: Amazon EC2, Azure VMs, Google Compute Engine, Alibaba ECS and servers in your own data center.',
|
|
905
|
+
],
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
id: 'github-actions',
|
|
909
|
+
label: 'GitHub Actions',
|
|
910
|
+
summary: 'Store the key as a GitHub environment secret, once for each environment. Your deploy workflow sets it on your app.',
|
|
911
|
+
steps: [
|
|
912
|
+
'Install the GitHub CLI. Sign in with gh auth login.',
|
|
913
|
+
'Run the command once for each environment. Paste x-uknows-key=<key> for that environment when it asks for the value. The value does not show on the screen.',
|
|
914
|
+
'In your deploy workflow, add environment: production to the deploy job. Then add the step below before your deploy step. It reads the secret and sets it on your app. The example is for Azure Container Apps.',
|
|
915
|
+
],
|
|
916
|
+
snippets: [
|
|
917
|
+
{
|
|
918
|
+
caption: 'Set the environment secret',
|
|
919
|
+
language: 'bash',
|
|
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
|
+
},
|
|
940
|
+
],
|
|
941
|
+
notes: [
|
|
942
|
+
'Replace my-org/orders-api with your repo. Replace production with the name of your GitHub environment.',
|
|
943
|
+
'Each environment has its own key. gh secret set OTEL_EXPORTER_OTLP_HEADERS --env production sets the key for production only. For staging, run it again with --env staging and the staging key.',
|
|
944
|
+
'An organization secret holds 1 key. Use it only when all the repos that read it deploy to the same environment.',
|
|
945
|
+
'--visibility private shares the organization secret with the private repos of the organization. Public repos cannot use it.',
|
|
946
|
+
'To share it with some repos only, use --visibility selected --repos orders-api,payments instead.',
|
|
947
|
+
'In the example, replace orders-prod and orders-api with your resource group and app.',
|
|
948
|
+
'The app must load the secret. Make your deploy command set OTEL_EXPORTER_OTLP_HEADERS=secretref:uknows-otlp-headers, as the Azure Container Apps script does.',
|
|
949
|
+
'On other platforms, pass the secret the same way to the step that sets your app settings.',
|
|
950
|
+
],
|
|
951
|
+
},
|
|
952
|
+
]
|
|
953
|
+
|
|
954
|
+
/** Said plainly, on the page and in llms.txt. */
|
|
955
|
+
export const SERVERLESS = 'Serverless functions are not supported yet: AWS Lambda, Azure Functions, Google Cloud Functions and Alibaba Function Compute. Apps in containers, on Kubernetes and on VMs work.'
|
|
956
|
+
|
|
957
|
+
const KEY_ANSWERS = [
|
|
958
|
+
{ answer: '"workspace" and "environment"', meaning: 'The key works. It sends data to that workspace and environment.' },
|
|
959
|
+
{ 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.' },
|
|
961
|
+
{ 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
|
+
{ answer: 'workspace_paused', meaning: 'Your workspace is paused. Talk to us to start it again.' },
|
|
963
|
+
{ answer: 'rate_limited', meaning: 'Too many checks from your address. Wait 1 minute.' },
|
|
964
|
+
]
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Check a key before you deploy: the ping route of the site says which
|
|
968
|
+
* workspace and environment the key sends to, or why it is refused. A second
|
|
969
|
+
* line checks that a host reaches the ingest address.
|
|
970
|
+
*/
|
|
971
|
+
export const KEY_CHECK = {
|
|
972
|
+
id: 'check-key',
|
|
973
|
+
title: 'Check your key',
|
|
974
|
+
summary: 'Check the key before you deploy. Then you know that the key works, and for which workspace and environment.',
|
|
975
|
+
steps: [
|
|
976
|
+
'Run the script in bash on your own computer. Paste your key when it asks. The key does not show on the screen.',
|
|
977
|
+
'Read the answer. The list below says what each answer means.',
|
|
978
|
+
'Run the network check on a host that runs your app.',
|
|
979
|
+
],
|
|
980
|
+
snippet: {
|
|
981
|
+
caption: 'Check the key',
|
|
982
|
+
language: 'bash',
|
|
983
|
+
code: keyScript([], [
|
|
984
|
+
`printf '${SECRET.header}: %s\\n' "$UKNOWS_KEY" | curl -sS -H @- ${PING_URL}`,
|
|
985
|
+
'echo',
|
|
986
|
+
]),
|
|
987
|
+
},
|
|
988
|
+
answers: KEY_ANSWERS,
|
|
989
|
+
network: {
|
|
990
|
+
caption: 'Check that your app can reach UKNOWS',
|
|
991
|
+
language: 'bash',
|
|
992
|
+
code: `curl -sS -o /dev/null -w '%{http_code}\\n' https://${INGEST_HOST}/otlp/v1/traces`,
|
|
993
|
+
},
|
|
994
|
+
networkText: 'Any 3-digit number means the host reaches UKNOWS. 000 or an error means it cannot. Then check DNS, the firewall and any proxy.',
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* How to load otel.env when the app does not start from a Dockerfile with the
|
|
999
|
+
* ENV lines. The Dockerfile ENV lines stay the main path.
|
|
1000
|
+
*/
|
|
1001
|
+
export const SETTINGS_FILE_LOADERS = [
|
|
1002
|
+
{
|
|
1003
|
+
id: 'shell',
|
|
1004
|
+
label: 'Plain start',
|
|
1005
|
+
summary: `Load the file and the key file in the shell that starts your app. Then run the start line for your stack. The key file belongs to root. Run the start as root. Or make the app user the owner with sudo chown <user> ${KEY_FILE}, then run the start as that user. Do not use chmod to let more users read the key file.`,
|
|
1006
|
+
snippet: {
|
|
1007
|
+
caption: 'Load the settings and the key, then start the app',
|
|
1008
|
+
language: 'bash',
|
|
1009
|
+
code: [
|
|
1010
|
+
'# Run in bash, in the folder of the service.',
|
|
1011
|
+
`# Run as root, or as the owner of ${KEY_FILE}. Only they can read it.`,
|
|
1012
|
+
'set -a',
|
|
1013
|
+
`. ./${SETTINGS_FILE.path} # the 5 settings`,
|
|
1014
|
+
`. ${KEY_FILE} # the key file from step 3`,
|
|
1015
|
+
'set +a',
|
|
1016
|
+
'# Now run the start line for your stack.',
|
|
1017
|
+
].join('\n'),
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
id: 'systemd',
|
|
1022
|
+
label: 'systemd',
|
|
1023
|
+
summary: 'Add the file and the key file to the service. Then restart the service.',
|
|
1024
|
+
snippet: {
|
|
1025
|
+
caption: 'systemd service',
|
|
1026
|
+
language: 'ini',
|
|
1027
|
+
code: [
|
|
1028
|
+
'[Service]',
|
|
1029
|
+
`# The 5 settings, from ${SETTINGS_FILE.path} in your repo.`,
|
|
1030
|
+
`EnvironmentFile=/opt/${EXAMPLE_SERVICE}/${SETTINGS_FILE.path}`,
|
|
1031
|
+
'# The key file from step 3.',
|
|
1032
|
+
`EnvironmentFile=${KEY_FILE}`,
|
|
1033
|
+
].join('\n'),
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
id: 'docker',
|
|
1038
|
+
label: 'Docker',
|
|
1039
|
+
summary: 'If your Dockerfile does not have the ENV lines, pass the file and the key file to docker run.',
|
|
1040
|
+
snippet: {
|
|
1041
|
+
caption: 'docker run',
|
|
1042
|
+
language: 'bash',
|
|
1043
|
+
code: [
|
|
1044
|
+
'# Loads the 5 settings and the key.',
|
|
1045
|
+
`sudo docker run -d --env-file ${SETTINGS_FILE.path} --env-file ${KEY_FILE} ${EXAMPLE_SERVICE}:latest`,
|
|
1046
|
+
].join('\n'),
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
id: 'alibaba-sae',
|
|
1051
|
+
label: 'Alibaba Cloud SAE',
|
|
1052
|
+
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 in step 3.',
|
|
1053
|
+
snippet: {
|
|
1054
|
+
caption: 'Environment variables',
|
|
1055
|
+
language: 'text',
|
|
1056
|
+
code: SETTINGS_ENV,
|
|
1057
|
+
},
|
|
1058
|
+
listsSettings: true,
|
|
1059
|
+
},
|
|
1060
|
+
]
|
|
1061
|
+
|
|
1062
|
+
export const TROUBLESHOOTING = [
|
|
1063
|
+
{
|
|
1064
|
+
id: 'no-data',
|
|
1065
|
+
problem: 'No data after 10 minutes',
|
|
1066
|
+
steps: [
|
|
1067
|
+
'Send a few requests to your app. UKNOWS shows the requests that your app gets.',
|
|
1068
|
+
'Restart or redeploy the app after you set the key.',
|
|
1069
|
+
'Compare the names and values of the 5 settings with this page.',
|
|
1070
|
+
'Run "Check your key" in step 3.',
|
|
1071
|
+
'Open Ingest keys on your workspace home. It shows the uploads UKNOWS rejected for each key, and why.',
|
|
1072
|
+
`Make sure that the app can reach ${INGEST_HOST} on port 443.`,
|
|
1073
|
+
'Remove OTEL_SDK_DISABLED, or set it to false.',
|
|
1074
|
+
'Read the app log for OpenTelemetry export errors. Some SDKs log nothing when UKNOWS refuses the key.',
|
|
1075
|
+
],
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
id: 'unauthorized',
|
|
1079
|
+
problem: 'The key is not accepted (401)',
|
|
1080
|
+
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
|
+
steps: [
|
|
1082
|
+
'Run "Check your key" in step 3. It says if the key is invalid or revoked.',
|
|
1083
|
+
'Open Ingest keys on your workspace home. It shows the uploads UKNOWS rejected for each key, and why.',
|
|
1084
|
+
'Copy the key again. Set the secret again.',
|
|
1085
|
+
'Use the format x-uknows-key=<key>. Remove spaces and quotes.',
|
|
1086
|
+
'Restart the app.',
|
|
1087
|
+
],
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
id: 'forbidden',
|
|
1091
|
+
problem: 'The app log shows 403 (access stopped)',
|
|
1092
|
+
cause: 'Your workspace is paused or your key is revoked.',
|
|
1093
|
+
steps: [
|
|
1094
|
+
'Talk to us to start the workspace again or to get a new key.',
|
|
1095
|
+
],
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
id: 'too-many',
|
|
1099
|
+
problem: 'The app log shows 429 (too many requests)',
|
|
1100
|
+
cause: 'Each key can send 200 requests a second, with short bursts up to 400. Your apps sent more.',
|
|
1101
|
+
steps: [
|
|
1102
|
+
'Keep the batch settings of OpenTelemetry. They send many traces in 1 request.',
|
|
1103
|
+
'Talk to us if your apps need more.',
|
|
1104
|
+
],
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
id: 'missing-jobs',
|
|
1108
|
+
problem: 'Queue consumers or batch jobs do not show',
|
|
1109
|
+
cause: 'UKNOWS drops traces that have links. Queue consumers and batch jobs often add links.',
|
|
1110
|
+
steps: [
|
|
1111
|
+
'Check the requests of the same app. They still show.',
|
|
1112
|
+
'Talk to us if you need these traces. We are working on it.',
|
|
1113
|
+
],
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
id: 'low-counts',
|
|
1117
|
+
problem: 'UKNOWS shows fewer requests than your app gets',
|
|
1118
|
+
cause: 'Your app samples traces. UKNOWS counts only the requests that it gets.',
|
|
1119
|
+
steps: [
|
|
1120
|
+
'Remove OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG, or set OTEL_TRACES_SAMPLER=parentbased_always_on.',
|
|
1121
|
+
'Remove any sampler from the OpenTelemetry code of the app.',
|
|
1122
|
+
'Restart the app.',
|
|
1123
|
+
],
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
id: 'other-apm',
|
|
1127
|
+
problem: 'Your app already uses another APM agent',
|
|
1128
|
+
cause: '2 tracing agents in 1 app can conflict.',
|
|
1129
|
+
steps: [
|
|
1130
|
+
'Use 1 tracing agent in each app.',
|
|
1131
|
+
'Remove the other agent, or turn off its tracing, before you add UKNOWS.',
|
|
1132
|
+
'If your app already uses OpenTelemetry, use the steps for "I already use OpenTelemetry".',
|
|
1133
|
+
],
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
id: 'turn-off',
|
|
1137
|
+
problem: 'Turn monitoring off',
|
|
1138
|
+
steps: [
|
|
1139
|
+
'Set OTEL_SDK_DISABLED=true on the platform.',
|
|
1140
|
+
'Restart the app.',
|
|
1141
|
+
'For .NET, set OTEL_DOTNET_AUTO_TRACES_ENABLED=false.',
|
|
1142
|
+
'To turn monitoring on again, remove the setting and restart the app.',
|
|
1143
|
+
],
|
|
1144
|
+
},
|
|
1145
|
+
]
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* OWNER DECISION 7 (PROGRAM.md, finding D-27): how many days UKNOWS keeps
|
|
1149
|
+
* workspace data. The page said 30 days, but no store setting keeps data that
|
|
1150
|
+
* long: SigNoz keeps traces for 7 or 15 days by default. Until the owner picks
|
|
1151
|
+
* a number and the store is set and checked, this stays null and the page
|
|
1152
|
+
* gives no number. Set it to the decided number of days, for example 30.
|
|
1153
|
+
*/
|
|
1154
|
+
export const RETENTION_DAYS = null
|