mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
refactor(API): Migrate GET /settings/otel to the decorator pattern (no-changelog) (#39720)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Uddish Verma <uddish.verma@n8n.io> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Declan Carroll <declancarroll@gmail.com>
This commit is contained in:
parent
41a0ccaf16
commit
7b12ec165d
|
|
@ -588,6 +588,10 @@ export {
|
|||
type OtlpProtocol,
|
||||
} from './otel/update-otel-settings.dto';
|
||||
export { TestOtelTraceDto } from './otel/test-otel-trace.dto';
|
||||
export {
|
||||
GetOtelSettingsQueryPublicDto,
|
||||
OtelSettingsPublicDto,
|
||||
} from './otel/otel-settings-public.dto';
|
||||
|
||||
export {
|
||||
PromotionChangesDto,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import '../../openapi-extend';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
otelSettingsPublicDescription,
|
||||
otelSettingsPublicFieldDocs as docs,
|
||||
} from './otel-settings-public.openapi';
|
||||
import { OTLP_PROTOCOLS } from './update-otel-settings.dto';
|
||||
import { Z } from '../../zod-class';
|
||||
|
||||
export class OtelSettingsPublicDto extends Z.class(
|
||||
{
|
||||
enabled: z.boolean().openapi(docs.enabled),
|
||||
exporterProtocol: z.string().openapi({ ...docs.exporterProtocol, enum: [...OTLP_PROTOCOLS] }),
|
||||
exporterEndpoint: z.string().openapi(docs.exporterEndpoint),
|
||||
exporterTracingPath: z.string().openapi(docs.exporterTracingPath),
|
||||
exporterServiceName: z.string().openapi(docs.exporterServiceName),
|
||||
exporterHeaders: z.string().openapi(docs.exporterHeaders),
|
||||
tracesSampleRate: z.number().openapi(docs.tracesSampleRate),
|
||||
startupConnectivityTimeoutMs: z.number().openapi(docs.startupConnectivityTimeoutMs),
|
||||
includeNodeSpans: z.boolean().openapi(docs.includeNodeSpans),
|
||||
injectOutbound: z.boolean().openapi(docs.injectOutbound),
|
||||
productionExecutionsOnly: z.boolean().openapi(docs.productionExecutionsOnly),
|
||||
},
|
||||
{ strict: true },
|
||||
) {
|
||||
static schema = super.schema.openapi({ description: otelSettingsPublicDescription });
|
||||
}
|
||||
|
||||
export class GetOtelSettingsQueryPublicDto extends Z.class({}, { strict: true }) {}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import type { ZodOpenAPIMetadata } from '@asteasolutions/zod-to-openapi';
|
||||
|
||||
export const otelSettingsPublicDescription =
|
||||
'The OpenTelemetry configuration, matching the fields exposed in the UI. On a write this is a ' +
|
||||
'full replacement: every field must be provided, except `exporterProtocol`, which defaults to ' +
|
||||
'`http/protobuf` when omitted. Fields managed declaratively via environment variables are ' +
|
||||
'returned with their effective value. A write can repeat these values, but changing them returns 409.';
|
||||
|
||||
export const otelSettingsPublicFieldDocs = {
|
||||
enabled: { description: 'Whether OpenTelemetry tracing is enabled.', example: true },
|
||||
exporterProtocol: {
|
||||
default: 'http/protobuf',
|
||||
description:
|
||||
'The wire protocol used to export spans. Collectors conventionally serve OTLP/HTTP on port ' +
|
||||
'4318 and OTLP/gRPC on port 4317. The endpoint scheme (`http://` or `https://`) controls ' +
|
||||
'TLS for both protocols. Optional: a write that omits it selects `http/protobuf`, but an ' +
|
||||
'instance that sets this field to another value with an environment variable rejects that ' +
|
||||
'write with 409.',
|
||||
example: 'http/protobuf',
|
||||
},
|
||||
exporterEndpoint: {
|
||||
format: 'uri',
|
||||
description:
|
||||
'The base URL of the OTLP collector to export traces to. The value must be an `http://` or ' +
|
||||
'`https://` URL, because the scheme selects TLS — for gRPC, `https://` uses SSL and ' +
|
||||
'`http://` is plaintext.',
|
||||
example: 'http://localhost:4318',
|
||||
},
|
||||
exporterTracingPath: {
|
||||
description:
|
||||
'The path appended to the endpoint for the OTLP traces signal. Ignored when ' +
|
||||
'`exporterProtocol` is `grpc`, which takes no URL path.',
|
||||
example: '/v1/traces',
|
||||
},
|
||||
exporterServiceName: {
|
||||
minLength: 1,
|
||||
description: 'The `service.name` resource attribute reported on every span.',
|
||||
example: 'n8n',
|
||||
},
|
||||
exporterHeaders: {
|
||||
description:
|
||||
'Additional headers sent to the OTLP collector, as a single string of comma-separated ' +
|
||||
'`key=value` pairs (e.g. `authorization=Bearer my-token,x-tenant-id=acme`). Whitespace ' +
|
||||
'around each key and value is trimmed; a value may contain spaces but not commas. Use an ' +
|
||||
'empty string when unused.',
|
||||
example: 'authorization=Bearer my-token,x-tenant-id=acme',
|
||||
},
|
||||
tracesSampleRate: {
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
description: 'The ratio of traces to sample, between 0 (none) and 1 (all).',
|
||||
example: 1,
|
||||
},
|
||||
startupConnectivityTimeoutMs: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
description:
|
||||
'How long, in milliseconds, to wait when checking the collector is reachable. Also used as ' +
|
||||
'the timeout for the test-trace endpoint.',
|
||||
example: 2000,
|
||||
},
|
||||
includeNodeSpans: {
|
||||
description: 'Whether to emit a span for each node execution in addition to the workflow span.',
|
||||
example: true,
|
||||
},
|
||||
injectOutbound: {
|
||||
description:
|
||||
'Whether to inject trace context headers into outbound HTTP requests made by nodes.',
|
||||
example: true,
|
||||
},
|
||||
productionExecutionsOnly: {
|
||||
description:
|
||||
'When true, only production executions of published (active) workflows are traced, not ' +
|
||||
'manual/test runs.',
|
||||
example: true,
|
||||
},
|
||||
} as const satisfies Record<string, ZodOpenAPIMetadata>;
|
||||
|
|
@ -205,7 +205,6 @@ export declare namespace SsoSamlRequest {
|
|||
// ----------------------------------
|
||||
|
||||
export declare namespace OtelSettingsRequest {
|
||||
type Get = AuthenticatedRequest;
|
||||
type Update = AuthenticatedRequest<{}, {}, UpdateOtelSettingsDto>;
|
||||
type Test = AuthenticatedRequest<{}, {}, TestOtelTraceDto>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import './executions.public.controller';
|
|||
import './folders.public.controller';
|
||||
import './insights.public.controller';
|
||||
import './node-type-policies.public.controller';
|
||||
import './otel.public.controller';
|
||||
import './projects.public.controller';
|
||||
import './promotions.public.controller';
|
||||
import './role-mapping-rules.public.controller';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { GetOtelSettingsQueryPublicDto, OtelSettingsPublicDto } from '@n8n/api-types';
|
||||
import type { AuthenticatedRequest } from '@n8n/db';
|
||||
import {
|
||||
ApiDescription,
|
||||
ApiKeyScope,
|
||||
ApiResponse,
|
||||
ApiSummary,
|
||||
ApiTags,
|
||||
Get,
|
||||
PublicApiController,
|
||||
Query,
|
||||
} from '@n8n/decorators';
|
||||
import type { Response } from 'express';
|
||||
|
||||
import { OtelSettingsService } from '@/modules/otel/otel-settings.service';
|
||||
import { toOtelSettingsResponse } from '@/public-api/v1/shared/otel.mapper';
|
||||
|
||||
@PublicApiController('/settings/otel')
|
||||
export class OtelPublicController {
|
||||
constructor(private readonly settingsService: OtelSettingsService) {}
|
||||
|
||||
@Get('/')
|
||||
@ApiKeyScope('otel:manage')
|
||||
@ApiSummary('Retrieve the OpenTelemetry configuration')
|
||||
@ApiDescription(
|
||||
'Retrieve the current OpenTelemetry configuration, including every field exposed in the UI. Requires the `otel:manage` scope.',
|
||||
)
|
||||
@ApiTags(['SettingsOtel'])
|
||||
@ApiResponse(200, OtelSettingsPublicDto)
|
||||
async getOtelSettings(
|
||||
_req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Query _query: GetOtelSettingsQueryPublicDto,
|
||||
): Promise<OtelSettingsPublicDto> {
|
||||
await this.settingsService.loadSettings();
|
||||
const settings = this.settingsService.getSettings();
|
||||
|
||||
return toOtelSettingsResponse(settings);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,28 +8,17 @@ import { OtelSettingsService } from '@/modules/otel/otel-settings.service';
|
|||
import { OtelService } from '@/modules/otel/otel.service';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
|
||||
import { toOtelSettingsResponse } from './otel.mapper';
|
||||
import { toOtelSettingsResponse } from '../../shared/otel.mapper';
|
||||
import type { OtelSettingsRequest } from '../../../types';
|
||||
import type { PublicAPIEndpoint } from '../../shared/handler.types';
|
||||
import { apiKeyHasScopeWithGlobalScopeFallback } from '../../shared/middlewares/global.middleware';
|
||||
|
||||
type OtelHandlers = {
|
||||
getOtelSettings: PublicAPIEndpoint<OtelSettingsRequest.Get>;
|
||||
updateOtelSettings: PublicAPIEndpoint<OtelSettingsRequest.Update>;
|
||||
testOtelTrace: PublicAPIEndpoint<OtelSettingsRequest.Test>;
|
||||
};
|
||||
|
||||
const otelHandlers: OtelHandlers = {
|
||||
getOtelSettings: [
|
||||
apiKeyHasScopeWithGlobalScopeFallback({ scope: 'otel:manage' }),
|
||||
async (_req, res) => {
|
||||
const settingsService = Container.get(OtelSettingsService);
|
||||
await settingsService.loadSettings();
|
||||
|
||||
return res.json(toOtelSettingsResponse(settingsService.getSettings()));
|
||||
},
|
||||
],
|
||||
|
||||
updateOtelSettings: [
|
||||
apiKeyHasScopeWithGlobalScopeFallback({ scope: 'otel:manage' }),
|
||||
async (req, res) => {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,3 @@
|
|||
get:
|
||||
x-eov-operation-id: getOtelSettings
|
||||
x-required-scope: otel:manage
|
||||
x-eov-operation-handler: v1/handlers/otel/otel.handler
|
||||
tags:
|
||||
- SettingsOtel
|
||||
summary: Retrieve the OpenTelemetry configuration
|
||||
description: >
|
||||
Retrieve the current OpenTelemetry configuration, including every field exposed in the UI.
|
||||
Requires the `otel:manage` scope.
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/otel-settings.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'403':
|
||||
$ref: '../../../../shared/spec/responses/forbidden.yml'
|
||||
put:
|
||||
x-eov-operation-id: updateOtelSettings
|
||||
x-required-scope: otel:manage
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: >
|
|||
The OpenTelemetry configuration, matching the fields exposed in the UI. On a write this is a
|
||||
full replacement: every field must be provided, except `exporterProtocol`, which defaults to
|
||||
`http/protobuf` when omitted. Fields managed declaratively via environment variables are
|
||||
returned with their effective value and ignored on write.
|
||||
returned with their effective value. A write can repeat these values, but changing them returns 409.
|
||||
required:
|
||||
- enabled
|
||||
- exporterEndpoint
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
operationId: getOtelSettings
|
||||
tags:
|
||||
- SettingsOtel
|
||||
summary: Retrieve the OpenTelemetry configuration
|
||||
description: Retrieve the current OpenTelemetry configuration, including every field exposed in the UI. Requires the `otel:manage` scope.
|
||||
x-required-scope: otel:manage
|
||||
x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether OpenTelemetry tracing is enabled.
|
||||
example: true
|
||||
exporterProtocol:
|
||||
type: string
|
||||
default: http/protobuf
|
||||
description: 'The wire protocol used to export spans. Collectors conventionally serve OTLP/HTTP on port 4318 and OTLP/gRPC on port 4317. The endpoint scheme (`http://` or `https://`) controls TLS for both protocols. Optional: a write that omits it selects `http/protobuf`, but an instance that sets this field to another value with an environment variable rejects that write with 409.'
|
||||
example: http/protobuf
|
||||
enum:
|
||||
- http/protobuf
|
||||
- grpc
|
||||
exporterEndpoint:
|
||||
type: string
|
||||
format: uri
|
||||
description: The base URL of the OTLP collector to export traces to. The value must be an `http://` or `https://` URL, because the scheme selects TLS — for gRPC, `https://` uses SSL and `http://` is plaintext.
|
||||
example: http://localhost:4318
|
||||
exporterTracingPath:
|
||||
type: string
|
||||
description: The path appended to the endpoint for the OTLP traces signal. Ignored when `exporterProtocol` is `grpc`, which takes no URL path.
|
||||
example: /v1/traces
|
||||
exporterServiceName:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: The `service.name` resource attribute reported on every span.
|
||||
example: n8n
|
||||
exporterHeaders:
|
||||
type: string
|
||||
description: Additional headers sent to the OTLP collector, as a single string of comma-separated `key=value` pairs (e.g. `authorization=Bearer my-token,x-tenant-id=acme`). Whitespace around each key and value is trimmed; a value may contain spaces but not commas. Use an empty string when unused.
|
||||
example: authorization=Bearer my-token,x-tenant-id=acme
|
||||
tracesSampleRate:
|
||||
type: number
|
||||
description: The ratio of traces to sample, between 0 (none) and 1 (all).
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
example: 1
|
||||
startupConnectivityTimeoutMs:
|
||||
type: integer
|
||||
description: How long, in milliseconds, to wait when checking the collector is reachable. Also used as the timeout for the test-trace endpoint.
|
||||
minimum: 0
|
||||
example: 2000
|
||||
includeNodeSpans:
|
||||
type: boolean
|
||||
description: Whether to emit a span for each node execution in addition to the workflow span.
|
||||
example: true
|
||||
injectOutbound:
|
||||
type: boolean
|
||||
description: Whether to inject trace context headers into outbound HTTP requests made by nodes.
|
||||
example: true
|
||||
productionExecutionsOnly:
|
||||
type: boolean
|
||||
description: When true, only production executions of published (active) workflows are traced, not manual/test runs.
|
||||
example: true
|
||||
required:
|
||||
- enabled
|
||||
- exporterProtocol
|
||||
- exporterEndpoint
|
||||
- exporterTracingPath
|
||||
- exporterServiceName
|
||||
- exporterHeaders
|
||||
- tracesSampleRate
|
||||
- startupConnectivityTimeoutMs
|
||||
- includeNodeSpans
|
||||
- injectOutbound
|
||||
- productionExecutionsOnly
|
||||
additionalProperties: false
|
||||
description: 'The OpenTelemetry configuration, matching the fields exposed in the UI. On a write this is a full replacement: every field must be provided, except `exporterProtocol`, which defaults to `http/protobuf` when omitted. Fields managed declaratively via environment variables are returned with their effective value. A write can repeat these values, but changing them returns 409.'
|
||||
'400':
|
||||
$ref: ../../../../shared/spec/responses/badRequest.yml
|
||||
'401':
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
|
|
@ -156,6 +156,9 @@ paths:
|
|||
/node-type-policies/scopes/{scopeId}/attachments:
|
||||
put:
|
||||
$ref: ./handlers/node-type-policies/spec/paths/replaceNodeTypePolicyAttachments.generated.yml
|
||||
/settings/otel:
|
||||
get:
|
||||
$ref: ./handlers/settings/spec/paths/getOtelSettings.generated.yml
|
||||
/projects:
|
||||
get:
|
||||
$ref: ./handlers/projects/spec/paths/getProjects.generated.yml
|
||||
|
|
|
|||
|
|
@ -74,6 +74,18 @@ describe('OpenTelemetry settings in Public API', () => {
|
|||
expect(typeof response.body.exporterEndpoint).toBe('string');
|
||||
});
|
||||
|
||||
it('returns saved settings with masked exporter headers', async () => {
|
||||
await testServer.publicApiAgentFor(owner).put('/settings/otel').send(validSettings);
|
||||
|
||||
const response = await testServer.publicApiAgentFor(owner).get('/settings/otel');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toStrictEqual({
|
||||
...validSettings,
|
||||
exporterHeaders: `authorization=${CREDENTIAL_BLANKING_VALUE}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes exactly the fields the UI configures, and nothing more', async () => {
|
||||
const response = await testServer.publicApiAgentFor(owner).get('/settings/otel');
|
||||
|
||||
|
|
@ -109,6 +121,17 @@ describe('OpenTelemetry settings in Public API', () => {
|
|||
const response = await testServer.publicApiAgentFor(scopedOwner).get('/settings/otel');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toEqual({ message: 'Forbidden' });
|
||||
});
|
||||
|
||||
it('rejects an unknown query parameter with 400', async () => {
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.get('/settings/otel')
|
||||
.query({ unexpected: 'value' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toContain('unexpected');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user