@n8n/frontend-module-otel 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.md +88 -0
- package/README.md +42 -0
- package/biome.jsonc +4 -0
- package/eslint.config.mjs +4 -0
- package/package.json +57 -0
- package/src/OtelSettingsRow.vue +96 -0
- package/src/OtelStatusControl.vue +109 -0
- package/src/OtelStatusDot.vue +36 -0
- package/src/SettingsOpenTelemetryView.test.ts +645 -0
- package/src/SettingsOpenTelemetryView.vue +791 -0
- package/src/__tests__/render.ts +36 -0
- package/src/__tests__/setup.ts +21 -0
- package/src/index.ts +4 -0
- package/src/otel.api.test.ts +86 -0
- package/src/otel.api.ts +48 -0
- package/src/otel.constants.ts +26 -0
- package/src/otel.module.test.ts +75 -0
- package/src/otel.module.ts +52 -0
- package/src/otel.store.test.ts +391 -0
- package/src/otel.store.ts +145 -0
- package/src/otel.utils.test.ts +44 -0
- package/src/otel.utils.ts +38 -0
- package/stylelint.config.mjs +9 -0
- package/vite.config.ts +51 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useDocumentTitle } from '@n8n/composables/useDocumentTitle';
|
|
3
|
+
import { useTelemetry } from '@n8n/composables/useTelemetry';
|
|
4
|
+
import { useToast } from '@n8n/composables/useToast';
|
|
5
|
+
import {
|
|
6
|
+
N8nButton,
|
|
7
|
+
N8nCheckbox,
|
|
8
|
+
N8nDialog,
|
|
9
|
+
N8nDialogClose,
|
|
10
|
+
N8nDialogFooter,
|
|
11
|
+
N8nIcon,
|
|
12
|
+
N8nInput,
|
|
13
|
+
N8nInputLabel,
|
|
14
|
+
N8nSettingsLayout,
|
|
15
|
+
N8nSettingsPageHeader,
|
|
16
|
+
N8nSettingsRowGroup,
|
|
17
|
+
N8nSettingsSaveBar,
|
|
18
|
+
N8nSettingsSection,
|
|
19
|
+
} from '@n8n/design-system';
|
|
20
|
+
import { useI18n } from '@n8n/i18n';
|
|
21
|
+
import { useSettingsStore } from '@n8n/stores/settings.store';
|
|
22
|
+
import { computed, ref, watch, onMounted } from 'vue';
|
|
23
|
+
import { onBeforeRouteLeave, type NavigationGuardNext } from 'vue-router';
|
|
24
|
+
|
|
25
|
+
import { OTEL_FIELD_ENV_VARS, OTEL_TEST_SPAN_NAME } from './otel.constants';
|
|
26
|
+
import { useOtelStore, headersStringToPairs, headersPairsToString } from './otel.store';
|
|
27
|
+
import { createSampleRateFormat } from './otel.utils';
|
|
28
|
+
import OtelSettingsRow from './OtelSettingsRow.vue';
|
|
29
|
+
import OtelStatusControl from './OtelStatusControl.vue';
|
|
30
|
+
|
|
31
|
+
const OTEL_DOCS_URL = 'https://docs.n8n.io/hosting/logging-monitoring/opentelemetry/';
|
|
32
|
+
|
|
33
|
+
const i18n = useI18n();
|
|
34
|
+
const telemetry = useTelemetry();
|
|
35
|
+
const toast = useToast();
|
|
36
|
+
// The shell's wrapper adds a claim guard for `setDocumentTitle`, which only the
|
|
37
|
+
// canvas calls. This view calls `set`, so it uses the platform composable directly
|
|
38
|
+
// and passes the release channel the wrapper would have supplied.
|
|
39
|
+
const documentTitle = useDocumentTitle({
|
|
40
|
+
releaseChannel: useSettingsStore().settings.releaseChannel,
|
|
41
|
+
});
|
|
42
|
+
const otelStore = useOtelStore();
|
|
43
|
+
|
|
44
|
+
const headerPairs = ref<Array<{ key: string; value: string }>>([]);
|
|
45
|
+
|
|
46
|
+
const showUnsavedChangesDialog = ref(false);
|
|
47
|
+
const pendingNext = ref<NavigationGuardNext | null>(null);
|
|
48
|
+
|
|
49
|
+
function syncHeaderPairsFromStore() {
|
|
50
|
+
headerPairs.value = headersStringToPairs(otelStore.settings.exporterHeaders);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function syncHeaderPairsToStore() {
|
|
54
|
+
otelStore.settings.exporterHeaders = headersPairsToString(headerPairs.value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function addHeader() {
|
|
58
|
+
headerPairs.value.push({ key: '', value: '' });
|
|
59
|
+
syncHeaderPairsToStore();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function removeHeader(index: number) {
|
|
63
|
+
headerPairs.value.splice(index, 1);
|
|
64
|
+
syncHeaderPairsToStore();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onHeaderChange(index: number, field: 'key' | 'value', value: string) {
|
|
68
|
+
headerPairs.value = headerPairs.value.map((pair, i) =>
|
|
69
|
+
i === index ? { ...pair, [field]: value } : pair,
|
|
70
|
+
);
|
|
71
|
+
syncHeaderPairsToStore();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isEnvManaged(field: keyof typeof OTEL_FIELD_ENV_VARS): boolean {
|
|
75
|
+
return otelStore.envManagedFields.includes(field);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// State-first copy: the row tells the admin whether tracing is live right now,
|
|
79
|
+
// instead of describing what the disabled state would mean hypothetically.
|
|
80
|
+
const statusDescription = computed(() =>
|
|
81
|
+
otelStore.settings.enabled
|
|
82
|
+
? i18n.baseText('settings.opentelemetry.status.enabledDescription')
|
|
83
|
+
: i18n.baseText('settings.opentelemetry.status.disabledDescription'),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
function envTooltip(field: keyof typeof OTEL_FIELD_ENV_VARS): string {
|
|
87
|
+
const envVariable = i18n.baseText('settings.opentelemetry.envVarTooltip', {
|
|
88
|
+
interpolate: { envVar: OTEL_FIELD_ENV_VARS[field] },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return isEnvManaged(field)
|
|
92
|
+
? `${i18n.baseText('settings.opentelemetry.envVarManagedTooltip')}. ${envVariable}`
|
|
93
|
+
: envVariable;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function save(): Promise<boolean> {
|
|
97
|
+
try {
|
|
98
|
+
const wasEnabled = otelStore.savedSettings.enabled;
|
|
99
|
+
await otelStore.saveSettings();
|
|
100
|
+
const isNowEnabled = otelStore.settings.enabled;
|
|
101
|
+
|
|
102
|
+
if (!wasEnabled && isNowEnabled) {
|
|
103
|
+
telemetry.track('Activated otel via UI', {
|
|
104
|
+
includeNodeSpans: otelStore.settings.includeNodeSpans,
|
|
105
|
+
productionExecutionsOnly: otelStore.settings.productionExecutionsOnly,
|
|
106
|
+
tracesSampleRate: otelStore.settings.tracesSampleRate,
|
|
107
|
+
injectOutbound: otelStore.settings.injectOutbound,
|
|
108
|
+
});
|
|
109
|
+
} else if (wasEnabled && !isNowEnabled) {
|
|
110
|
+
telemetry.track('Disabled otel via UI');
|
|
111
|
+
} else {
|
|
112
|
+
telemetry.track('Updated otel via UI', {
|
|
113
|
+
enabled: isNowEnabled,
|
|
114
|
+
includeNodeSpans: otelStore.settings.includeNodeSpans,
|
|
115
|
+
productionExecutionsOnly: otelStore.settings.productionExecutionsOnly,
|
|
116
|
+
tracesSampleRate: otelStore.settings.tracesSampleRate,
|
|
117
|
+
injectOutbound: otelStore.settings.injectOutbound,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
toast.showMessage({
|
|
122
|
+
title: i18n.baseText('settings.opentelemetry.savedSuccess'),
|
|
123
|
+
type: 'success',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
toast.showError(error, i18n.baseText('settings.opentelemetry.savedError'));
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function discard() {
|
|
134
|
+
otelStore.discardChanges();
|
|
135
|
+
syncHeaderPairsFromStore();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const statusSaving = ref(false);
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* Enabling/disabling is a live status change, not a form draft: it applies
|
|
142
|
+
* immediately by committing the current on-screen draft with the flag set.
|
|
143
|
+
* Saving only the flag would silently enable with stale, previously-saved
|
|
144
|
+
* config while the screen shows unsaved edits.
|
|
145
|
+
*/
|
|
146
|
+
async function onToggleEnabled(enabled: boolean) {
|
|
147
|
+
otelStore.settings.enabled = enabled;
|
|
148
|
+
statusSaving.value = true;
|
|
149
|
+
try {
|
|
150
|
+
const saved = await save();
|
|
151
|
+
if (!saved) {
|
|
152
|
+
otelStore.settings.enabled = !enabled;
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
statusSaving.value = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onLeaveWithoutSaving() {
|
|
160
|
+
showUnsavedChangesDialog.value = false;
|
|
161
|
+
pendingNext.value?.();
|
|
162
|
+
pendingNext.value = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function onSaveAndLeave() {
|
|
166
|
+
const saved = await save();
|
|
167
|
+
if (!saved) return;
|
|
168
|
+
showUnsavedChangesDialog.value = false;
|
|
169
|
+
pendingNext.value?.();
|
|
170
|
+
pendingNext.value = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function onKeepEditing() {
|
|
174
|
+
showUnsavedChangesDialog.value = false;
|
|
175
|
+
pendingNext.value?.(false);
|
|
176
|
+
pendingNext.value = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onBeforeRouteLeave((_to, _from, next) => {
|
|
180
|
+
if (!otelStore.isDirty) {
|
|
181
|
+
next();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
pendingNext.value = next;
|
|
185
|
+
showUnsavedChangesDialog.value = true;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
onMounted(async () => {
|
|
189
|
+
documentTitle.set(i18n.baseText('settings.opentelemetry.title'));
|
|
190
|
+
await otelStore.fetchSettings();
|
|
191
|
+
syncHeaderPairsFromStore();
|
|
192
|
+
syncSampleRateInput();
|
|
193
|
+
syncConnectivityTimeoutInput();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
watch(
|
|
197
|
+
() => otelStore.settings?.exporterHeaders,
|
|
198
|
+
(newVal) => {
|
|
199
|
+
const currentString = headersPairsToString(headerPairs.value);
|
|
200
|
+
if (newVal !== currentString) {
|
|
201
|
+
headerPairs.value = headersStringToPairs(newVal ?? '');
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const canTestTrace = computed(
|
|
207
|
+
() => !!otelStore.settings.exporterEndpoint && otelStore.testState !== 'sending',
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
/*
|
|
211
|
+
* The sample rate is a text input formatted by us, not a native number input:
|
|
212
|
+
* native number inputs render their value with the OS-region decimal separator,
|
|
213
|
+
* which JS can neither read nor override, so the "of 1.00" copy next to it
|
|
214
|
+
* could never be guaranteed to match. Formatting both the input display and the
|
|
215
|
+
* copy through the same Intl formatter makes them consistent by construction.
|
|
216
|
+
*/
|
|
217
|
+
const { format: formatSampleRate, parse: parseSampleRate } = createSampleRateFormat();
|
|
218
|
+
const sampleRateMax = formatSampleRate(1);
|
|
219
|
+
|
|
220
|
+
const sampleRateInput = ref('');
|
|
221
|
+
const connectivityTimeoutInput = ref('');
|
|
222
|
+
|
|
223
|
+
function syncSampleRateInput() {
|
|
224
|
+
sampleRateInput.value = formatSampleRate(otelStore.settings.tracesSampleRate);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function syncConnectivityTimeoutInput() {
|
|
228
|
+
connectivityTimeoutInput.value = String(otelStore.settings.startupConnectivityTimeoutMs);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseConnectivityTimeout(text: string): number | null {
|
|
232
|
+
const trimmed = text.trim();
|
|
233
|
+
if (!trimmed) return null;
|
|
234
|
+
const parsed = Number(trimmed);
|
|
235
|
+
return Number.isFinite(parsed) ? Math.max(0, Math.round(parsed)) : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function commitSampleRate() {
|
|
239
|
+
const parsed = parseSampleRate(sampleRateInput.value);
|
|
240
|
+
if (parsed !== null) {
|
|
241
|
+
otelStore.settings.tracesSampleRate = parsed;
|
|
242
|
+
}
|
|
243
|
+
syncSampleRateInput();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function commitConnectivityTimeout() {
|
|
247
|
+
const parsed = parseConnectivityTimeout(connectivityTimeoutInput.value);
|
|
248
|
+
if (parsed !== null) {
|
|
249
|
+
otelStore.settings.startupConnectivityTimeoutMs = parsed;
|
|
250
|
+
}
|
|
251
|
+
syncConnectivityTimeoutInput();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Arrow-key stepping, mirroring the native number-input affordance these text inputs replaced
|
|
255
|
+
// (steps match the previous N8nInputNumber config: 0.01 for the rate, 100ms for the timeout).
|
|
256
|
+
function stepSampleRate(direction: 1 | -1) {
|
|
257
|
+
const current = parseSampleRate(sampleRateInput.value) ?? otelStore.settings.tracesSampleRate;
|
|
258
|
+
const next = Math.min(1, Math.max(0, Math.round((current + direction * 0.01) * 100) / 100));
|
|
259
|
+
otelStore.settings.tracesSampleRate = next;
|
|
260
|
+
syncSampleRateInput();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function stepConnectivityTimeout(direction: 1 | -1) {
|
|
264
|
+
const current =
|
|
265
|
+
parseConnectivityTimeout(connectivityTimeoutInput.value) ??
|
|
266
|
+
otelStore.settings.startupConnectivityTimeoutMs;
|
|
267
|
+
otelStore.settings.startupConnectivityTimeoutMs = Math.max(0, current + direction * 100);
|
|
268
|
+
syncConnectivityTimeoutInput();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Commit parseable drafts to the store as the user types, so dirty state (and the save bar)
|
|
272
|
+
// reacts live like every other field. The display is NOT reformatted here — that would fight
|
|
273
|
+
// the caret mid-edit; blur/Enter does the reformat.
|
|
274
|
+
watch(sampleRateInput, (text) => {
|
|
275
|
+
const parsed = parseSampleRate(text);
|
|
276
|
+
if (parsed !== null) {
|
|
277
|
+
otelStore.settings.tracesSampleRate = parsed;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
watch(connectivityTimeoutInput, (text) => {
|
|
281
|
+
const parsed = parseConnectivityTimeout(text);
|
|
282
|
+
if (parsed !== null) {
|
|
283
|
+
otelStore.settings.startupConnectivityTimeoutMs = parsed;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Keep the formatted display in sync when the store changes underneath (discard, save
|
|
288
|
+
// response, env-managed refresh) — but not when the change came from the draft being typed
|
|
289
|
+
// above, which would clobber the caret with a reformat on every keystroke.
|
|
290
|
+
watch(
|
|
291
|
+
() => otelStore.settings.tracesSampleRate,
|
|
292
|
+
(value) => {
|
|
293
|
+
if (parseSampleRate(sampleRateInput.value) !== value) syncSampleRateInput();
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
watch(
|
|
297
|
+
() => otelStore.settings.startupConnectivityTimeoutMs,
|
|
298
|
+
(value) => {
|
|
299
|
+
if (parseConnectivityTimeout(connectivityTimeoutInput.value) !== value) {
|
|
300
|
+
syncConnectivityTimeoutInput();
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const testTraceSubtitle = computed(() => {
|
|
306
|
+
if (otelStore.testState === 'sent') {
|
|
307
|
+
return i18n.baseText('settings.opentelemetry.testTrace.success', {
|
|
308
|
+
interpolate: { spanName: OTEL_TEST_SPAN_NAME, time: otelStore.testTimestamp },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (otelStore.testState === 'error') {
|
|
312
|
+
return i18n.baseText('settings.opentelemetry.testTrace.error', {
|
|
313
|
+
interpolate: { error: otelStore.testError },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return i18n.baseText('settings.opentelemetry.testTrace.description');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
async function onSendTestTrace() {
|
|
320
|
+
await otelStore.sendTestTrace();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Connection changes invalidate the previous test result
|
|
324
|
+
watch(
|
|
325
|
+
() => [
|
|
326
|
+
otelStore.settings.exporterEndpoint,
|
|
327
|
+
otelStore.settings.exporterTracingPath,
|
|
328
|
+
otelStore.settings.exporterServiceName,
|
|
329
|
+
otelStore.settings.exporterHeaders,
|
|
330
|
+
otelStore.settings.startupConnectivityTimeoutMs,
|
|
331
|
+
],
|
|
332
|
+
() => {
|
|
333
|
+
if (otelStore.testState !== 'idle') {
|
|
334
|
+
otelStore.resetTestState();
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
</script>
|
|
339
|
+
|
|
340
|
+
<template>
|
|
341
|
+
<N8nSettingsLayout :class="$style.layout">
|
|
342
|
+
<N8nSettingsPageHeader
|
|
343
|
+
:title="i18n.baseText('settings.opentelemetry.title')"
|
|
344
|
+
:description="i18n.baseText('settings.opentelemetry.description')"
|
|
345
|
+
:docs-url="OTEL_DOCS_URL"
|
|
346
|
+
/>
|
|
347
|
+
|
|
348
|
+
<div v-if="otelStore.loading" :class="$style.loading" data-test-id="otel-loading">
|
|
349
|
+
<N8nIcon icon="spinner" spin />
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div v-else :class="$style.settingsContent">
|
|
353
|
+
<N8nSettingsSection>
|
|
354
|
+
<N8nSettingsRowGroup>
|
|
355
|
+
<OtelSettingsRow
|
|
356
|
+
:title="i18n.baseText('settings.opentelemetry.status.label')"
|
|
357
|
+
:description="statusDescription"
|
|
358
|
+
:env-tooltip="envTooltip('enabled')"
|
|
359
|
+
>
|
|
360
|
+
<template #action>
|
|
361
|
+
<OtelStatusControl
|
|
362
|
+
:enabled="otelStore.settings.enabled"
|
|
363
|
+
:disabled="isEnvManaged('enabled')"
|
|
364
|
+
:loading="statusSaving"
|
|
365
|
+
@update:enabled="onToggleEnabled"
|
|
366
|
+
/>
|
|
367
|
+
</template>
|
|
368
|
+
</OtelSettingsRow>
|
|
369
|
+
</N8nSettingsRowGroup>
|
|
370
|
+
</N8nSettingsSection>
|
|
371
|
+
|
|
372
|
+
<N8nSettingsSection
|
|
373
|
+
:title="i18n.baseText('settings.opentelemetry.collectorConnection.title')"
|
|
374
|
+
>
|
|
375
|
+
<N8nSettingsRowGroup>
|
|
376
|
+
<OtelSettingsRow
|
|
377
|
+
:title="i18n.baseText('settings.opentelemetry.exporterEndpoint.label')"
|
|
378
|
+
:description="i18n.baseText('settings.opentelemetry.exporterEndpoint.description')"
|
|
379
|
+
:env-tooltip="envTooltip('exporterEndpoint')"
|
|
380
|
+
action-fill
|
|
381
|
+
>
|
|
382
|
+
<template #action>
|
|
383
|
+
<N8nInput
|
|
384
|
+
v-model="otelStore.settings.exporterEndpoint"
|
|
385
|
+
:class="$style.control"
|
|
386
|
+
:placeholder="i18n.baseText('settings.opentelemetry.exporterEndpoint.placeholder')"
|
|
387
|
+
:disabled="isEnvManaged('exporterEndpoint')"
|
|
388
|
+
data-test-id="otel-exporter-endpoint"
|
|
389
|
+
/>
|
|
390
|
+
</template>
|
|
391
|
+
</OtelSettingsRow>
|
|
392
|
+
|
|
393
|
+
<OtelSettingsRow
|
|
394
|
+
:title="i18n.baseText('settings.opentelemetry.exporterServiceName.label')"
|
|
395
|
+
:description="i18n.baseText('settings.opentelemetry.exporterServiceName.description')"
|
|
396
|
+
:env-tooltip="envTooltip('exporterServiceName')"
|
|
397
|
+
action-fill
|
|
398
|
+
>
|
|
399
|
+
<template #action>
|
|
400
|
+
<N8nInput
|
|
401
|
+
v-model="otelStore.settings.exporterServiceName"
|
|
402
|
+
:class="$style.control"
|
|
403
|
+
:placeholder="
|
|
404
|
+
i18n.baseText('settings.opentelemetry.exporterServiceName.placeholder')
|
|
405
|
+
"
|
|
406
|
+
:disabled="isEnvManaged('exporterServiceName')"
|
|
407
|
+
data-test-id="otel-service-name"
|
|
408
|
+
/>
|
|
409
|
+
</template>
|
|
410
|
+
</OtelSettingsRow>
|
|
411
|
+
|
|
412
|
+
<OtelSettingsRow
|
|
413
|
+
:title="i18n.baseText('settings.opentelemetry.exporterHeaders.label')"
|
|
414
|
+
:description="i18n.baseText('settings.opentelemetry.exporterHeaders.description')"
|
|
415
|
+
:env-tooltip="envTooltip('exporterHeaders')"
|
|
416
|
+
layout="vertical"
|
|
417
|
+
action-fill
|
|
418
|
+
:action-max-width="false"
|
|
419
|
+
>
|
|
420
|
+
<template #action>
|
|
421
|
+
<div :class="$style.headersBlock">
|
|
422
|
+
<div v-for="(pair, index) in headerPairs" :key="index" :class="$style.headerRow">
|
|
423
|
+
<N8nInputLabel
|
|
424
|
+
:label="
|
|
425
|
+
index === 0
|
|
426
|
+
? i18n.baseText('settings.opentelemetry.exporterHeaders.keyLabel')
|
|
427
|
+
: undefined
|
|
428
|
+
"
|
|
429
|
+
size="small"
|
|
430
|
+
>
|
|
431
|
+
<N8nInput
|
|
432
|
+
:model-value="pair.key"
|
|
433
|
+
:placeholder="
|
|
434
|
+
i18n.baseText('settings.opentelemetry.exporterHeaders.keyPlaceholder')
|
|
435
|
+
"
|
|
436
|
+
:disabled="isEnvManaged('exporterHeaders')"
|
|
437
|
+
data-test-id="otel-header-key"
|
|
438
|
+
@update:model-value="(v: string) => onHeaderChange(index, 'key', v)"
|
|
439
|
+
/>
|
|
440
|
+
</N8nInputLabel>
|
|
441
|
+
<N8nInputLabel
|
|
442
|
+
:label="
|
|
443
|
+
index === 0
|
|
444
|
+
? i18n.baseText('settings.opentelemetry.exporterHeaders.valueLabel')
|
|
445
|
+
: undefined
|
|
446
|
+
"
|
|
447
|
+
size="small"
|
|
448
|
+
>
|
|
449
|
+
<N8nInput
|
|
450
|
+
:model-value="pair.value"
|
|
451
|
+
:placeholder="
|
|
452
|
+
i18n.baseText('settings.opentelemetry.exporterHeaders.valuePlaceholder')
|
|
453
|
+
"
|
|
454
|
+
:disabled="isEnvManaged('exporterHeaders')"
|
|
455
|
+
data-test-id="otel-header-value"
|
|
456
|
+
@update:model-value="(v: string) => onHeaderChange(index, 'value', v)"
|
|
457
|
+
/>
|
|
458
|
+
</N8nInputLabel>
|
|
459
|
+
<div :class="$style.headerRemove">
|
|
460
|
+
<N8nButton
|
|
461
|
+
icon="trash-2"
|
|
462
|
+
variant="ghost"
|
|
463
|
+
size="small"
|
|
464
|
+
native-type="button"
|
|
465
|
+
:disabled="isEnvManaged('exporterHeaders')"
|
|
466
|
+
:aria-label="i18n.baseText('settings.opentelemetry.exporterHeaders.remove')"
|
|
467
|
+
data-test-id="otel-header-remove"
|
|
468
|
+
@click.stop.prevent="removeHeader(index)"
|
|
469
|
+
/>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
<N8nButton
|
|
473
|
+
icon="plus"
|
|
474
|
+
variant="subtle"
|
|
475
|
+
size="small"
|
|
476
|
+
native-type="button"
|
|
477
|
+
:disabled="isEnvManaged('exporterHeaders')"
|
|
478
|
+
:class="$style.addHeaderButton"
|
|
479
|
+
data-test-id="otel-header-add"
|
|
480
|
+
@click.stop.prevent="addHeader"
|
|
481
|
+
>
|
|
482
|
+
{{ i18n.baseText('settings.opentelemetry.exporterHeaders.addHeader') }}
|
|
483
|
+
</N8nButton>
|
|
484
|
+
</div>
|
|
485
|
+
</template>
|
|
486
|
+
</OtelSettingsRow>
|
|
487
|
+
|
|
488
|
+
<OtelSettingsRow
|
|
489
|
+
:title="i18n.baseText('settings.opentelemetry.exporterTracingPath.label')"
|
|
490
|
+
:description="i18n.baseText('settings.opentelemetry.exporterTracingPath.description')"
|
|
491
|
+
:env-tooltip="envTooltip('exporterTracingPath')"
|
|
492
|
+
action-fill
|
|
493
|
+
>
|
|
494
|
+
<template #action>
|
|
495
|
+
<N8nInput
|
|
496
|
+
v-model="otelStore.settings.exporterTracingPath"
|
|
497
|
+
:class="$style.control"
|
|
498
|
+
:placeholder="
|
|
499
|
+
i18n.baseText('settings.opentelemetry.exporterTracingPath.placeholder')
|
|
500
|
+
"
|
|
501
|
+
:disabled="isEnvManaged('exporterTracingPath')"
|
|
502
|
+
data-test-id="otel-tracing-path"
|
|
503
|
+
/>
|
|
504
|
+
</template>
|
|
505
|
+
</OtelSettingsRow>
|
|
506
|
+
|
|
507
|
+
<OtelSettingsRow
|
|
508
|
+
:title="i18n.baseText('settings.opentelemetry.startupConnectivityTimeoutMs.label')"
|
|
509
|
+
:description="
|
|
510
|
+
i18n.baseText('settings.opentelemetry.startupConnectivityTimeoutMs.description')
|
|
511
|
+
"
|
|
512
|
+
:env-tooltip="envTooltip('startupConnectivityTimeoutMs')"
|
|
513
|
+
>
|
|
514
|
+
<template #action>
|
|
515
|
+
<div :class="$style.inputWithSlug">
|
|
516
|
+
<N8nInput
|
|
517
|
+
v-model="connectivityTimeoutInput"
|
|
518
|
+
:disabled="isEnvManaged('startupConnectivityTimeoutMs')"
|
|
519
|
+
:aria-label="
|
|
520
|
+
i18n.baseText('settings.opentelemetry.startupConnectivityTimeoutMs.label')
|
|
521
|
+
"
|
|
522
|
+
data-test-id="otel-connectivity-timeout"
|
|
523
|
+
@blur="commitConnectivityTimeout"
|
|
524
|
+
@keydown.enter="commitConnectivityTimeout"
|
|
525
|
+
@keydown.up.prevent="stepConnectivityTimeout(1)"
|
|
526
|
+
@keydown.down.prevent="stepConnectivityTimeout(-1)"
|
|
527
|
+
/>
|
|
528
|
+
<span :class="$style.slug">
|
|
529
|
+
{{ i18n.baseText('settings.opentelemetry.startupConnectivityTimeoutMs.slug') }}
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
532
|
+
</template>
|
|
533
|
+
</OtelSettingsRow>
|
|
534
|
+
|
|
535
|
+
<OtelSettingsRow
|
|
536
|
+
:title="i18n.baseText('settings.opentelemetry.testTrace.label')"
|
|
537
|
+
:description="testTraceSubtitle"
|
|
538
|
+
:description-error="otelStore.testState === 'error'"
|
|
539
|
+
>
|
|
540
|
+
<template #action>
|
|
541
|
+
<N8nButton
|
|
542
|
+
v-if="otelStore.testState === 'sent'"
|
|
543
|
+
variant="outline"
|
|
544
|
+
icon="check"
|
|
545
|
+
native-type="button"
|
|
546
|
+
data-test-id="otel-test-trace-button"
|
|
547
|
+
@click.stop.prevent="onSendTestTrace"
|
|
548
|
+
>
|
|
549
|
+
{{ i18n.baseText('settings.opentelemetry.testTrace.sent') }}
|
|
550
|
+
</N8nButton>
|
|
551
|
+
<N8nButton
|
|
552
|
+
v-else
|
|
553
|
+
variant="outline"
|
|
554
|
+
:loading="otelStore.testState === 'sending'"
|
|
555
|
+
:disabled="!canTestTrace"
|
|
556
|
+
native-type="button"
|
|
557
|
+
data-test-id="otel-test-trace-button"
|
|
558
|
+
@click.stop.prevent="onSendTestTrace"
|
|
559
|
+
>
|
|
560
|
+
{{
|
|
561
|
+
otelStore.testState === 'sending'
|
|
562
|
+
? i18n.baseText('settings.opentelemetry.testTrace.sending')
|
|
563
|
+
: i18n.baseText('settings.opentelemetry.testTrace.send')
|
|
564
|
+
}}
|
|
565
|
+
</N8nButton>
|
|
566
|
+
</template>
|
|
567
|
+
</OtelSettingsRow>
|
|
568
|
+
</N8nSettingsRowGroup>
|
|
569
|
+
</N8nSettingsSection>
|
|
570
|
+
|
|
571
|
+
<N8nSettingsSection :title="i18n.baseText('settings.opentelemetry.tracing.title')">
|
|
572
|
+
<N8nSettingsRowGroup>
|
|
573
|
+
<OtelSettingsRow
|
|
574
|
+
:title="i18n.baseText('settings.opentelemetry.tracesSampleRate.label')"
|
|
575
|
+
:description="
|
|
576
|
+
i18n.baseText('settings.opentelemetry.tracesSampleRate.description', {
|
|
577
|
+
interpolate: { max: sampleRateMax },
|
|
578
|
+
})
|
|
579
|
+
"
|
|
580
|
+
:env-tooltip="envTooltip('tracesSampleRate')"
|
|
581
|
+
>
|
|
582
|
+
<template #action>
|
|
583
|
+
<div :class="$style.inputWithSlug">
|
|
584
|
+
<N8nInput
|
|
585
|
+
v-model="sampleRateInput"
|
|
586
|
+
:disabled="isEnvManaged('tracesSampleRate')"
|
|
587
|
+
:aria-label="i18n.baseText('settings.opentelemetry.tracesSampleRate.label')"
|
|
588
|
+
data-test-id="otel-sample-rate"
|
|
589
|
+
@blur="commitSampleRate"
|
|
590
|
+
@keydown.enter="commitSampleRate"
|
|
591
|
+
@keydown.up.prevent="stepSampleRate(1)"
|
|
592
|
+
@keydown.down.prevent="stepSampleRate(-1)"
|
|
593
|
+
/>
|
|
594
|
+
<span :class="$style.slug">
|
|
595
|
+
{{
|
|
596
|
+
i18n.baseText('settings.opentelemetry.tracesSampleRate.slug', {
|
|
597
|
+
interpolate: { max: sampleRateMax },
|
|
598
|
+
})
|
|
599
|
+
}}
|
|
600
|
+
</span>
|
|
601
|
+
</div>
|
|
602
|
+
</template>
|
|
603
|
+
</OtelSettingsRow>
|
|
604
|
+
|
|
605
|
+
<OtelSettingsRow
|
|
606
|
+
:title="i18n.baseText('settings.opentelemetry.includeNodeSpans.label')"
|
|
607
|
+
:description="i18n.baseText('settings.opentelemetry.includeNodeSpans.description')"
|
|
608
|
+
:env-tooltip="envTooltip('includeNodeSpans')"
|
|
609
|
+
>
|
|
610
|
+
<template #action>
|
|
611
|
+
<N8nCheckbox
|
|
612
|
+
:model-value="otelStore.settings.includeNodeSpans"
|
|
613
|
+
:disabled="isEnvManaged('includeNodeSpans')"
|
|
614
|
+
data-test-id="otel-include-node-spans"
|
|
615
|
+
@update:model-value="otelStore.settings.includeNodeSpans = Boolean($event)"
|
|
616
|
+
/>
|
|
617
|
+
</template>
|
|
618
|
+
</OtelSettingsRow>
|
|
619
|
+
|
|
620
|
+
<OtelSettingsRow
|
|
621
|
+
:title="i18n.baseText('settings.opentelemetry.injectOutbound.label')"
|
|
622
|
+
:description="i18n.baseText('settings.opentelemetry.injectOutbound.description')"
|
|
623
|
+
:env-tooltip="envTooltip('injectOutbound')"
|
|
624
|
+
>
|
|
625
|
+
<template #action>
|
|
626
|
+
<N8nCheckbox
|
|
627
|
+
:model-value="otelStore.settings.injectOutbound"
|
|
628
|
+
:disabled="isEnvManaged('injectOutbound')"
|
|
629
|
+
data-test-id="otel-inject-outbound"
|
|
630
|
+
@update:model-value="otelStore.settings.injectOutbound = Boolean($event)"
|
|
631
|
+
/>
|
|
632
|
+
</template>
|
|
633
|
+
</OtelSettingsRow>
|
|
634
|
+
|
|
635
|
+
<OtelSettingsRow
|
|
636
|
+
:title="i18n.baseText('settings.opentelemetry.productionExecutionsOnly.label')"
|
|
637
|
+
:description="
|
|
638
|
+
i18n.baseText('settings.opentelemetry.productionExecutionsOnly.description')
|
|
639
|
+
"
|
|
640
|
+
:env-tooltip="envTooltip('productionExecutionsOnly')"
|
|
641
|
+
>
|
|
642
|
+
<template #action>
|
|
643
|
+
<N8nCheckbox
|
|
644
|
+
:model-value="otelStore.settings.productionExecutionsOnly"
|
|
645
|
+
:disabled="isEnvManaged('productionExecutionsOnly')"
|
|
646
|
+
data-test-id="otel-production-only"
|
|
647
|
+
@update:model-value="otelStore.settings.productionExecutionsOnly = Boolean($event)"
|
|
648
|
+
/>
|
|
649
|
+
</template>
|
|
650
|
+
</OtelSettingsRow>
|
|
651
|
+
</N8nSettingsRowGroup>
|
|
652
|
+
</N8nSettingsSection>
|
|
653
|
+
|
|
654
|
+
<N8nSettingsSaveBar
|
|
655
|
+
:class="$style.saveBar"
|
|
656
|
+
:visible="otelStore.isDirty"
|
|
657
|
+
:message="i18n.baseText('settings.opentelemetry.unsavedChanges.title')"
|
|
658
|
+
:save-label="i18n.baseText('settings.opentelemetry.save')"
|
|
659
|
+
:discard-label="i18n.baseText('settings.opentelemetry.discard')"
|
|
660
|
+
:saving="otelStore.saving"
|
|
661
|
+
floating
|
|
662
|
+
@save="save"
|
|
663
|
+
@discard="discard"
|
|
664
|
+
/>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<N8nDialog
|
|
668
|
+
v-model:open="showUnsavedChangesDialog"
|
|
669
|
+
:header="i18n.baseText('settings.opentelemetry.unsavedChanges.title')"
|
|
670
|
+
:description="i18n.baseText('settings.opentelemetry.unsavedChanges.message')"
|
|
671
|
+
size="medium"
|
|
672
|
+
>
|
|
673
|
+
<div data-test-id="otel-unsaved-changes-dialog">
|
|
674
|
+
<N8nDialogFooter>
|
|
675
|
+
<N8nDialogClose as-child>
|
|
676
|
+
<N8nButton
|
|
677
|
+
variant="outline"
|
|
678
|
+
:label="i18n.baseText('settings.opentelemetry.unsavedChanges.cancel')"
|
|
679
|
+
@click="onKeepEditing"
|
|
680
|
+
/>
|
|
681
|
+
</N8nDialogClose>
|
|
682
|
+
<N8nButton
|
|
683
|
+
variant="outline"
|
|
684
|
+
:label="i18n.baseText('settings.opentelemetry.unsavedChanges.leaveWithoutSaving')"
|
|
685
|
+
@click="onLeaveWithoutSaving"
|
|
686
|
+
/>
|
|
687
|
+
<N8nButton
|
|
688
|
+
variant="solid"
|
|
689
|
+
:label="i18n.baseText('settings.opentelemetry.unsavedChanges.saveAndLeave')"
|
|
690
|
+
:loading="otelStore.saving"
|
|
691
|
+
@click="onSaveAndLeave"
|
|
692
|
+
/>
|
|
693
|
+
</N8nDialogFooter>
|
|
694
|
+
</div>
|
|
695
|
+
</N8nDialog>
|
|
696
|
+
</N8nSettingsLayout>
|
|
697
|
+
</template>
|
|
698
|
+
|
|
699
|
+
<style lang="scss" module>
|
|
700
|
+
.layout {
|
|
701
|
+
padding-top: 0;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.settingsContent {
|
|
705
|
+
display: flex;
|
|
706
|
+
flex-direction: column;
|
|
707
|
+
width: 100%;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.saveBar {
|
|
711
|
+
/*
|
|
712
|
+
* Same separation sections keep between each other (32px), so the bar reads as a
|
|
713
|
+
* page-level action rather than part of the preceding Tracing section.
|
|
714
|
+
*/
|
|
715
|
+
margin-block-start: var(--spacing--xl);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.loading {
|
|
719
|
+
display: flex;
|
|
720
|
+
align-items: center;
|
|
721
|
+
justify-content: center;
|
|
722
|
+
padding: var(--spacing--2xl);
|
|
723
|
+
color: var(--icon-color--subtle);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.control {
|
|
727
|
+
width: 100%;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.headersBlock {
|
|
731
|
+
display: flex;
|
|
732
|
+
flex-direction: column;
|
|
733
|
+
align-items: flex-start;
|
|
734
|
+
gap: var(--spacing--2xs);
|
|
735
|
+
width: 100%;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.headerRow {
|
|
739
|
+
display: flex;
|
|
740
|
+
align-items: flex-end;
|
|
741
|
+
gap: var(--spacing--2xs);
|
|
742
|
+
width: 100%;
|
|
743
|
+
|
|
744
|
+
> div {
|
|
745
|
+
flex: 1;
|
|
746
|
+
min-width: 0;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.headerRemove {
|
|
751
|
+
display: flex;
|
|
752
|
+
flex: 0 0 auto !important;
|
|
753
|
+
align-items: center;
|
|
754
|
+
justify-content: center;
|
|
755
|
+
height: var(--height--lg);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.addHeaderButton {
|
|
759
|
+
margin-top: var(--spacing--4xs);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.inputWithSlug {
|
|
763
|
+
display: flex;
|
|
764
|
+
align-items: stretch;
|
|
765
|
+
|
|
766
|
+
// Compact numeric field: hug short values instead of filling the action area.
|
|
767
|
+
// N8nInput exposes per-corner radius custom properties; square the right side
|
|
768
|
+
// so the unit slug visually continues the input.
|
|
769
|
+
> :first-child {
|
|
770
|
+
--input--radius--top-right: 0;
|
|
771
|
+
--input--radius--bottom-right: 0;
|
|
772
|
+
|
|
773
|
+
width: var(--spacing--4xl);
|
|
774
|
+
min-width: 0;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.slug {
|
|
779
|
+
display: inline-flex;
|
|
780
|
+
align-items: center;
|
|
781
|
+
padding: 0 var(--spacing--2xs);
|
|
782
|
+
border: var(--border-width) solid var(--border-color);
|
|
783
|
+
border-left: none;
|
|
784
|
+
border-top-right-radius: var(--radius);
|
|
785
|
+
border-bottom-right-radius: var(--radius);
|
|
786
|
+
background: var(--background--hover);
|
|
787
|
+
color: var(--text-color--subtle);
|
|
788
|
+
font-size: var(--font-size--xs);
|
|
789
|
+
white-space: nowrap;
|
|
790
|
+
}
|
|
791
|
+
</style>
|