refactor(API): Migrate the log streaming destination writes to the decorator pattern (no-changelog) (#40397)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Uddish Verma 2026-10-07 12:34:47 +00:00 committed by GitHub
parent e7d8533880
commit 741ba22943
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1337 additions and 566 deletions

View File

@ -339,10 +339,12 @@ export {
LogStreamingDestinationListPublicDto,
LogStreamingDestinationPublicDto,
LogStreamingEventTypesPublicDto,
PublicCreateDestinationDto,
CreateLogStreamingDestinationPublicDto,
UpdateLogStreamingDestinationPublicDto,
type LogStreamingDestinationPublic,
type PublicCreateDestination,
type PublicDestinationType,
type CreateLogStreamingDestinationPublic,
type UpdateLogStreamingDestinationPublic,
type LogStreamingDestinationPublicType,
} from './log-streaming/log-streaming-public.dto';
export { PaginationDto, MAX_ITEMS_PER_PAGE } from './pagination/pagination.dto';

View File

@ -15,6 +15,7 @@ import {
logStreamingDestinationCommonFieldDocs,
logStreamingDestinationDocs,
logStreamingDestinationListFieldDocs,
logStreamingDestinationRequestReadOnlyFieldDocs,
logStreamingEventTypesFieldDocs,
logStreamingParameterFieldDocs,
logStreamingParameterListDocs,
@ -23,6 +24,7 @@ import {
logStreamingWebhookFieldDocs,
logStreamingWebhookOptionsFieldDocs,
} from './log-streaming-public.openapi';
import { readOnlyPublicSchema } from '../../schemas/read-only-public.schema';
import { Z } from '../../zod-class';
const logStreamingEventTypesPublicSchema = z
@ -166,29 +168,34 @@ const publicSentrySchema = z.object({
dsn: sentryShape.dsn.openapi(sentryDocs.dsn),
});
export const PublicCreateDestinationDto = z.discriminatedUnion('type', [
publicWebhookSchema,
publicSyslogSchema,
publicSentrySchema,
]);
export type PublicCreateDestination = z.infer<typeof PublicCreateDestinationDto>;
export type PublicDestinationType = PublicCreateDestination['type'];
const idSchema = z.string().openapi(commonDocs.id);
export const logStreamingDestinationPublicSchema = z
.discriminatedUnion('type', [
publicWebhookSchema.extend({ id: idSchema }),
publicSyslogSchema.extend({ id: idSchema }),
publicSentrySchema.extend({ id: idSchema }),
])
const readOnlyIdSchema = readOnlyPublicSchema(logStreamingDestinationRequestReadOnlyFieldDocs.id);
const destinationUnion = <Id extends z.ZodTypeAny>(id: Id) =>
z
.discriminatedUnion('type', [
publicWebhookSchema.extend({ id }),
publicSyslogSchema.extend({ id }),
publicSentrySchema.extend({ id }),
])
.openapi(logStreamingDestinationDocs);
export const logStreamingDestinationPublicSchema = destinationUnion(idSchema);
const createLogStreamingDestinationPublicSchema = destinationUnion(readOnlyIdSchema);
const updateLogStreamingDestinationPublicSchema = z
.discriminatedUnion('type', [publicWebhookSchema, publicSyslogSchema, publicSentrySchema])
.openapi(logStreamingDestinationDocs);
export type LogStreamingDestinationPublic = z.infer<typeof logStreamingDestinationPublicSchema>;
export type CreateLogStreamingDestinationPublic = z.infer<
typeof createLogStreamingDestinationPublicSchema
>;
export type UpdateLogStreamingDestinationPublic = z.infer<
typeof updateLogStreamingDestinationPublicSchema
>;
export type LogStreamingDestinationPublicType = LogStreamingDestinationPublic['type'];
/** A discriminated union has no object shape, so this cannot extend `Z.class`. */
export class LogStreamingDestinationPublicDto {
static schema = logStreamingDestinationPublicSchema;
@ -201,6 +208,32 @@ export class LogStreamingDestinationPublicDto {
}
}
export type CreateLogStreamingDestinationPublicDto = CreateLogStreamingDestinationPublic;
export const CreateLogStreamingDestinationPublicDto = class {
static schema = createLogStreamingDestinationPublicSchema;
static safeParse(data: unknown) {
return createLogStreamingDestinationPublicSchema.safeParse(data);
}
static parse(data: unknown): CreateLogStreamingDestinationPublic {
return createLogStreamingDestinationPublicSchema.parse(data);
}
};
export type UpdateLogStreamingDestinationPublicDto = UpdateLogStreamingDestinationPublic;
export const UpdateLogStreamingDestinationPublicDto = class {
static schema = updateLogStreamingDestinationPublicSchema;
static safeParse(data: unknown) {
return updateLogStreamingDestinationPublicSchema.safeParse(data);
}
static parse(data: unknown): UpdateLogStreamingDestinationPublic {
return updateLogStreamingDestinationPublicSchema.parse(data);
}
};
const logStreamingDestinationListPublicSchema = z
.object({
data: z

View File

@ -39,6 +39,10 @@ export const logStreamingDestinationCommonFieldDocs = {
},
} as const satisfies Record<string, ZodOpenAPIMetadata>;
export const logStreamingDestinationRequestReadOnlyFieldDocs = {
id: { type: 'string', readOnly: true, ...logStreamingDestinationCommonFieldDocs.id },
} as const satisfies Record<string, ZodOpenAPIMetadata>;
export const logStreamingCircuitBreakerFieldDocs = {
maxFailures: {
description: 'Maximum failures within the sliding window before the breaker opens.',

View File

@ -1,5 +1,4 @@
import type {
PublicCreateDestination,
UpdateOtelSettingsDto,
TestOtelTraceDto,
UpdateSamlConfigurationDto,
@ -115,8 +114,6 @@ export declare namespace CommunityPackageRequest {
}
export declare namespace LogStreamingRequest {
type CreateDestination = AuthenticatedRequest<{}, {}, PublicCreateDestination>;
type UpdateDestination = AuthenticatedRequest<{ id: string }, {}, PublicCreateDestination>;
type TestDestination = AuthenticatedRequest<{ id: string }>;
type DeleteDestination = AuthenticatedRequest<{ id: string }>;
}

View File

@ -1,16 +1,36 @@
import { LogStreamingDestinationPublicDto, type PublicDestinationType } from '@n8n/api-types';
import {
LogStreamingDestinationPublicDto,
type UpdateLogStreamingDestinationPublic,
type LogStreamingDestinationPublicType,
} from '@n8n/api-types';
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
const INTERNAL_TO_PUBLIC: Partial<
Record<MessageEventBusDestinationTypeNames, PublicDestinationType>
Record<MessageEventBusDestinationTypeNames, LogStreamingDestinationPublicType>
> = {
[MessageEventBusDestinationTypeNames.webhook]: 'webhook',
[MessageEventBusDestinationTypeNames.syslog]: 'syslog',
[MessageEventBusDestinationTypeNames.sentry]: 'sentry',
};
const PUBLIC_TO_INTERNAL: Record<
LogStreamingDestinationPublicType,
MessageEventBusDestinationTypeNames
> = {
webhook: MessageEventBusDestinationTypeNames.webhook,
syslog: MessageEventBusDestinationTypeNames.syslog,
sentry: MessageEventBusDestinationTypeNames.sentry,
};
export function toLogStreamingDestinationPublic(options: MessageEventBusDestinationOptions) {
const type = options.__type ? INTERNAL_TO_PUBLIC[options.__type] : undefined;
return LogStreamingDestinationPublicDto.parse({ ...options, type });
}
export function toInternalDestinationOptions(
input: UpdateLogStreamingDestinationPublic,
): MessageEventBusDestinationOptions {
const { type, ...rest } = input;
return { ...rest, __type: PUBLIC_TO_INTERNAL[type] };
}

View File

@ -1,11 +1,16 @@
import {
CreateLogStreamingDestinationPublicDto,
UpdateLogStreamingDestinationPublicDto,
LogStreamingDestinationListPublicDto,
LogStreamingDestinationPublicDto,
LogStreamingEventTypesPublicDto,
logStreamingDestinationIdParamSchema,
} from '@n8n/api-types';
import { OutboundHttp } from '@n8n/backend-network';
import { CredentialsFinderService } from '@n8n/backend-services';
import { InstanceSettingsLoaderConfig } from '@n8n/config';
import { LICENSE_FEATURES } from '@n8n/constants';
import type { AuthenticatedRequest } from '@n8n/db';
import type { AuthenticatedRequest, User } from '@n8n/db';
import {
ApiDescription,
ApiErrorResponse,
@ -13,25 +18,40 @@ import {
ApiResponse,
ApiSummary,
ApiTags,
Body,
Get,
Licensed,
Param,
Post,
PublicApiController,
Put,
} from '@n8n/decorators';
import type { Response } from 'express';
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import { NotFoundError } from '@n8n/errors';
import { ConflictError, NotFoundError } from '@n8n/errors';
import { eventNamesAll } from '@/eventbus/event-message-classes';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { createMessageEventBusDestination } from '@/modules/log-streaming.ee/create-message-event-bus-destination';
import { assertUserCanUseDestinationCredentials } from '@/modules/log-streaming.ee/destinations/destination-credentials-access';
import { LogStreamingDestinationService } from '@/modules/log-streaming.ee/log-streaming-destination.service';
import { toLogStreamingDestinationPublic } from './log-streaming.mapper';
import {
toInternalDestinationOptions,
toLogStreamingDestinationPublic,
} from './log-streaming.mapper';
const tags = ['LogStreaming'];
@PublicApiController('/settings/log-streaming')
export class LogStreamingPublicController {
constructor(private readonly destinationService: LogStreamingDestinationService) {}
constructor(
private readonly destinationService: LogStreamingDestinationService,
private readonly eventBus: MessageEventBus,
private readonly instanceSettingsLoaderConfig: InstanceSettingsLoaderConfig,
private readonly outboundHttp: OutboundHttp,
private readonly credentialsFinderService: CredentialsFinderService,
) {}
@Get('/event-types')
@ApiKeyScope('eventBusDestination:list')
@ -81,6 +101,70 @@ export class LogStreamingPublicController {
return toLogStreamingDestinationPublic(destination);
}
@Post('/destinations')
@ApiKeyScope('eventBusDestination:create')
@Licensed(LICENSE_FEATURES.LOG_STREAMING)
@ApiSummary('Create a log streaming destination')
@ApiDescription(
'Create a log streaming destination. The destination takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:create` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is created; reads still return the current values.',
)
@ApiTags(tags)
@ApiResponse(200, LogStreamingDestinationPublicDto)
@ApiErrorResponse(409)
async createLogStreamingDestination(
req: AuthenticatedRequest,
_res: Response,
@Body body: CreateLogStreamingDestinationPublicDto,
): Promise<LogStreamingDestinationPublicDto> {
this.assertNotManagedByEnv();
return await this.saveDestination(req.user, toInternalDestinationOptions(body));
}
@Put('/destinations/:id')
@ApiKeyScope('eventBusDestination:update')
@Licensed(LICENSE_FEATURES.LOG_STREAMING)
@ApiSummary('Update a log streaming destination')
@ApiDescription(
'Replace an existing log streaming destination. The update takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:update` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is changed; reads still return the current values.',
)
@ApiTags(tags)
@ApiResponse(200, LogStreamingDestinationPublicDto)
@ApiErrorResponse(404)
@ApiErrorResponse(409)
async updateLogStreamingDestination(
req: AuthenticatedRequest,
_res: Response,
@Param('id', logStreamingDestinationIdParamSchema) id: string,
@Body body: UpdateLogStreamingDestinationPublicDto,
): Promise<LogStreamingDestinationPublicDto> {
this.assertNotManagedByEnv();
await this.findDestinationOrFail(id);
// `addDestination` replaces the stored destination that has this id.
return await this.saveDestination(req.user, { ...toInternalDestinationOptions(body), id });
}
private assertNotManagedByEnv() {
if (this.instanceSettingsLoaderConfig.logStreamingManagedByEnv) {
throw new ConflictError(
'Log streaming destinations are managed via environment variables and cannot be modified through the API',
);
}
}
private async saveDestination(
user: User,
options: MessageEventBusDestinationOptions,
): Promise<LogStreamingDestinationPublicDto> {
await assertUserCanUseDestinationCredentials(this.credentialsFinderService, user, options);
const destination = createMessageEventBusDestination(this.eventBus, this.outboundHttp, options);
const result = await this.destinationService.addDestination(destination);
return toLogStreamingDestinationPublic(result.serialize());
}
private async findDestinationOrFail(id: string): Promise<MessageEventBusDestinationOptions> {
const [destination] = await this.destinationService.findDestination(id);
if (!destination) {

View File

@ -1,16 +1,11 @@
import { PublicCreateDestinationDto } from '@n8n/api-types';
import { OutboundHttp } from '@n8n/backend-network';
import { InstanceSettingsLoaderConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import { BadRequestError, ConflictError, NotFoundError } from '@n8n/errors';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { createMessageEventBusDestination } from '@/modules/log-streaming.ee/create-message-event-bus-destination';
import { ConflictError, NotFoundError } from '@n8n/errors';
import { assertUserCanUseDestinationCredentials } from '@/modules/log-streaming.ee/destinations/destination-credentials-access';
import { LogStreamingDestinationService } from '@/modules/log-streaming.ee/log-streaming-destination.service';
import { toInternalDestinationOptions } from './log-streaming.mapper';
import { toLogStreamingDestinationPublic } from '../../controllers/log-streaming.mapper';
import type { LogStreamingRequest } from '../../../types';
import type { PublicAPIEndpoint } from '../../shared/handler.types';
@ -41,79 +36,11 @@ const getCredentialsFinderService = async () => {
};
type LogStreamingHandlers = {
createDestination: PublicAPIEndpoint<LogStreamingRequest.CreateDestination>;
updateDestination: PublicAPIEndpoint<LogStreamingRequest.UpdateDestination>;
testDestination: PublicAPIEndpoint<LogStreamingRequest.TestDestination>;
deleteDestination: PublicAPIEndpoint<LogStreamingRequest.DeleteDestination>;
};
const logStreamingHandlers: LogStreamingHandlers = {
createDestination: [
isLicensed('feat:logStreaming'),
apiKeyHasScopeWithGlobalScopeFallback({ scope: 'eventBusDestination:create' }),
async (req, res) => {
assertNotManagedByEnv();
const parseResult = PublicCreateDestinationDto.safeParse(req.body);
if (!parseResult.success) {
throw new BadRequestError(parseResult.error.errors[0].message);
}
const options = toInternalDestinationOptions(parseResult.data);
await assertUserCanUseDestinationCredentials(
await getCredentialsFinderService(),
req.user,
options,
);
const destination = createMessageEventBusDestination(
Container.get(MessageEventBus),
Container.get(OutboundHttp),
options,
);
const result = await Container.get(LogStreamingDestinationService).addDestination(
destination,
);
return res.json(toLogStreamingDestinationPublic(result.serialize()));
},
],
updateDestination: [
isLicensed('feat:logStreaming'),
apiKeyHasScopeWithGlobalScopeFallback({ scope: 'eventBusDestination:update' }),
async (req, res) => {
assertNotManagedByEnv();
await findDestinationOrFail(req.params.id);
const parseResult = PublicCreateDestinationDto.safeParse(req.body);
if (!parseResult.success) {
throw new BadRequestError(parseResult.error.errors[0].message);
}
// the path id is the update target; addDestination upserts on it
const options = { ...toInternalDestinationOptions(parseResult.data), id: req.params.id };
await assertUserCanUseDestinationCredentials(
await getCredentialsFinderService(),
req.user,
options,
);
const destination = createMessageEventBusDestination(
Container.get(MessageEventBus),
Container.get(OutboundHttp),
options,
);
const result = await Container.get(LogStreamingDestinationService).addDestination(
destination,
);
return res.json(toLogStreamingDestinationPublic(result.serialize()));
},
],
testDestination: [
isLicensed('feat:logStreaming'),
apiKeyHasScopeWithGlobalScopeFallback({ scope: 'eventBusDestination:test' }),

View File

@ -1,17 +0,0 @@
import { type PublicCreateDestination, type PublicDestinationType } from '@n8n/api-types';
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
// The public API uses a friendly `type` discriminator; the internal service uses `__type`.
const PUBLIC_TO_INTERNAL: Record<PublicDestinationType, MessageEventBusDestinationTypeNames> = {
webhook: MessageEventBusDestinationTypeNames.webhook,
syslog: MessageEventBusDestinationTypeNames.syslog,
sentry: MessageEventBusDestinationTypeNames.sentry,
};
export function toInternalDestinationOptions(
input: PublicCreateDestination,
): MessageEventBusDestinationOptions {
const { type, ...rest } = input;
return { ...rest, __type: PUBLIC_TO_INTERNAL[type] };
}

View File

@ -1,47 +1,3 @@
put:
x-eov-operation-id: updateDestination
x-required-scope: eventBusDestination:update
x-eov-operation-handler: v1/handlers/log-streaming/log-streaming.handler
tags:
- LogStreaming
summary: Update a log streaming destination
description: >
Replace an existing log streaming destination. The update takes effect exactly as it would
from the UI, using the same validation. Requires the `eventBusDestination:update` scope and
the Log Streaming feature to be licensed. When destinations are managed via environment
variables, the write is rejected with 409 and nothing is changed; reads still return the
current values.
parameters:
- name: id
in: path
description: The id of the log streaming destination.
required: true
schema:
type: string
requestBody:
description: The destination fields to apply.
required: true
content:
application/json:
schema:
$ref: '../schemas/destination.yml'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '../schemas/destination.yml'
'400':
$ref: '../../../../shared/spec/responses/badRequest.yml'
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
'403':
$ref: '../../../../shared/spec/responses/forbidden.yml'
'404':
$ref: '../../../../shared/spec/responses/notFound.yml'
'409':
$ref: '../../../../shared/spec/responses/conflict.yml'
delete:
x-eov-operation-id: deleteDestination
x-required-scope: eventBusDestination:delete

View File

@ -1,35 +0,0 @@
post:
x-eov-operation-id: createDestination
x-required-scope: eventBusDestination:create
x-eov-operation-handler: v1/handlers/log-streaming/log-streaming.handler
tags:
- LogStreaming
summary: Create a log streaming destination
description: >
Create a log streaming destination. The destination takes effect exactly as it would from
the UI, using the same validation. Requires the `eventBusDestination:create` scope and the
Log Streaming feature to be licensed. When destinations are managed via environment
variables, the write is rejected with 409 and nothing is created; reads still return the
current values.
requestBody:
description: The destination to create.
required: true
content:
application/json:
schema:
$ref: '../schemas/destination.yml'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '../schemas/destination.yml'
'400':
$ref: '../../../../shared/spec/responses/badRequest.yml'
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
'403':
$ref: '../../../../shared/spec/responses/forbidden.yml'
'409':
$ref: '../../../../shared/spec/responses/conflict.yml'

View File

@ -0,0 +1,385 @@
operationId: createLogStreamingDestination
tags:
- LogStreaming
summary: Create a log streaming destination
description: Create a log streaming destination. The destination takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:create` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is created; reads still return the current values.
x-required-scope: eventBusDestination:create
x-eov-operation-id: unreachable
x-eov-operation-handler: v1/handlers/decorator-routed.handler
x-decorator-routed: true
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- webhook
description: Sends each event as an HTTP request to a URL.
example: webhook
url:
type: string
format: uri
description: Target URL that receives the event payload.
example: https://example.com/n8n-events
method:
type: string
description: HTTP method used for the request.
example: POST
sendHeaders:
type: boolean
description: Whether to attach custom headers to the request.
example: false
specifyHeaders:
type: string
description: How custom headers are provided — `keypair` or `json`.
example: keypair
headerParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonHeaders:
type: string
description: Custom headers as a JSON string when `specifyHeaders` is `json`.
sendQuery:
type: boolean
description: Whether to attach query parameters to the request.
example: false
specifyQuery:
type: string
description: How query parameters are provided — `keypair` or `json`.
queryParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonQuery:
type: string
description: Query parameters as a JSON string when `specifyQuery` is `json`.
options:
type: object
properties:
timeout:
type: integer
minimum: 0
exclusiveMinimum: true
description: Request timeout in milliseconds.
allowUnauthorizedCerts:
type: boolean
description: Accept self-signed or otherwise invalid TLS certificates.
queryParameterArrays:
type: string
enum:
- indices
- brackets
- repeat
description: How array-valued query parameters are serialized into the query string.
redirect:
type: object
properties:
redirect:
type: object
properties:
followRedirects:
type: boolean
description: Whether to follow HTTP redirects.
maxRedirects:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum number of redirects to follow.
required:
- redirect
description: Redirect-following behaviour.
proxy:
type: object
properties:
proxy:
type: object
properties:
protocol:
type: string
enum:
- https
- http
description: Proxy protocol.
host:
type: string
description: Proxy host, without protocol or port.
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Proxy port.
required:
- protocol
- host
- port
required:
- proxy
description: Outbound proxy configuration.
socket:
type: object
properties:
keepAlive:
type: boolean
description: Whether to keep sockets open for reuse.
maxSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum sockets per host kept open at once.
maxFreeSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum idle sockets per host kept open.
description: Connection socket options.
description: Additional HTTP request options.
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
readOnly: true
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- url
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- syslog
description: Sends each event to a syslog server.
example: syslog
host:
type: string
minLength: 1
description: Syslog server host.
example: syslog.example.com
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Syslog server port.
example: 514
protocol:
type: string
enum:
- udp
- tcp
- tls
description: Transport protocol.
example: udp
facility:
type: integer
minimum: 0
maximum: 23
description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.'
example: 16
app_name:
type: string
description: Application name reported in the syslog message.
example: n8n
tlsCa:
type: string
description: PEM-encoded CA certificate used when `protocol` is `tls`.
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
readOnly: true
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- host
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- sentry
description: Sends each event to a Sentry project.
example: sentry
dsn:
type: string
format: uri
description: Sentry DSN the events are sent to.
example: https://examplePublicKey@o0.ingest.sentry.io/0
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
readOnly: true
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- dsn
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: ../../../../shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml
'400':
$ref: ../../../../shared/spec/responses/badRequest.yml
'401':
$ref: ../../../../shared/spec/responses/unauthorized.yml
'403':
$ref: ../../../../shared/spec/responses/forbidden.yml
'409':
$ref: ../../../../shared/spec/responses/conflict.yml
'415':
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml

View File

@ -20,360 +20,7 @@ responses:
content:
application/json:
schema:
oneOf:
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- webhook
description: Sends each event as an HTTP request to a URL.
example: webhook
url:
type: string
format: uri
description: Target URL that receives the event payload.
example: https://example.com/n8n-events
method:
type: string
description: HTTP method used for the request.
example: POST
sendHeaders:
type: boolean
description: Whether to attach custom headers to the request.
example: false
specifyHeaders:
type: string
description: How custom headers are provided — `keypair` or `json`.
example: keypair
headerParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonHeaders:
type: string
description: Custom headers as a JSON string when `specifyHeaders` is `json`.
sendQuery:
type: boolean
description: Whether to attach query parameters to the request.
example: false
specifyQuery:
type: string
description: How query parameters are provided — `keypair` or `json`.
queryParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonQuery:
type: string
description: Query parameters as a JSON string when `specifyQuery` is `json`.
options:
type: object
properties:
timeout:
type: integer
minimum: 0
exclusiveMinimum: true
description: Request timeout in milliseconds.
allowUnauthorizedCerts:
type: boolean
description: Accept self-signed or otherwise invalid TLS certificates.
queryParameterArrays:
type: string
enum:
- indices
- brackets
- repeat
description: How array-valued query parameters are serialized into the query string.
redirect:
type: object
properties:
redirect:
type: object
properties:
followRedirects:
type: boolean
description: Whether to follow HTTP redirects.
maxRedirects:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum number of redirects to follow.
required:
- redirect
description: Redirect-following behaviour.
proxy:
type: object
properties:
proxy:
type: object
properties:
protocol:
type: string
enum:
- https
- http
description: Proxy protocol.
host:
type: string
description: Proxy host, without protocol or port.
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Proxy port.
required:
- protocol
- host
- port
required:
- proxy
description: Outbound proxy configuration.
socket:
type: object
properties:
keepAlive:
type: boolean
description: Whether to keep sockets open for reuse.
maxSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum sockets per host kept open at once.
maxFreeSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum idle sockets per host kept open.
description: Connection socket options.
description: Additional HTTP request options.
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- url
- id
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- syslog
description: Sends each event to a syslog server.
example: syslog
host:
type: string
minLength: 1
description: Syslog server host.
example: syslog.example.com
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Syslog server port.
example: 514
protocol:
type: string
enum:
- udp
- tcp
- tls
description: Transport protocol.
example: udp
facility:
type: integer
minimum: 0
maximum: 23
description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.'
example: 16
app_name:
type: string
description: Application name reported in the syslog message.
example: n8n
tlsCa:
type: string
description: PEM-encoded CA certificate used when `protocol` is `tls`.
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- host
- id
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- sentry
description: Sends each event to a Sentry project.
example: sentry
dsn:
type: string
format: uri
description: Sentry DSN the events are sent to.
example: https://examplePublicKey@o0.ingest.sentry.io/0
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- dsn
- id
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'
$ref: ../../../../shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml
'401':
$ref: ../../../../shared/spec/responses/unauthorized.yml
'403':

View File

@ -0,0 +1,379 @@
operationId: updateLogStreamingDestination
tags:
- LogStreaming
summary: Update a log streaming destination
description: Replace an existing log streaming destination. The update takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:update` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is changed; reads still return the current values.
x-required-scope: eventBusDestination:update
x-eov-operation-id: unreachable
x-eov-operation-handler: v1/handlers/decorator-routed.handler
x-decorator-routed: true
parameters:
- schema:
type: string
required: true
description: The id of the log streaming destination.
name: id
in: path
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- webhook
description: Sends each event as an HTTP request to a URL.
example: webhook
url:
type: string
format: uri
description: Target URL that receives the event payload.
example: https://example.com/n8n-events
method:
type: string
description: HTTP method used for the request.
example: POST
sendHeaders:
type: boolean
description: Whether to attach custom headers to the request.
example: false
specifyHeaders:
type: string
description: How custom headers are provided — `keypair` or `json`.
example: keypair
headerParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonHeaders:
type: string
description: Custom headers as a JSON string when `specifyHeaders` is `json`.
sendQuery:
type: boolean
description: Whether to attach query parameters to the request.
example: false
specifyQuery:
type: string
description: How query parameters are provided — `keypair` or `json`.
queryParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonQuery:
type: string
description: Query parameters as a JSON string when `specifyQuery` is `json`.
options:
type: object
properties:
timeout:
type: integer
minimum: 0
exclusiveMinimum: true
description: Request timeout in milliseconds.
allowUnauthorizedCerts:
type: boolean
description: Accept self-signed or otherwise invalid TLS certificates.
queryParameterArrays:
type: string
enum:
- indices
- brackets
- repeat
description: How array-valued query parameters are serialized into the query string.
redirect:
type: object
properties:
redirect:
type: object
properties:
followRedirects:
type: boolean
description: Whether to follow HTTP redirects.
maxRedirects:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum number of redirects to follow.
required:
- redirect
description: Redirect-following behaviour.
proxy:
type: object
properties:
proxy:
type: object
properties:
protocol:
type: string
enum:
- https
- http
description: Proxy protocol.
host:
type: string
description: Proxy host, without protocol or port.
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Proxy port.
required:
- protocol
- host
- port
required:
- proxy
description: Outbound proxy configuration.
socket:
type: object
properties:
keepAlive:
type: boolean
description: Whether to keep sockets open for reuse.
maxSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum sockets per host kept open at once.
maxFreeSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum idle sockets per host kept open.
description: Connection socket options.
description: Additional HTTP request options.
required:
- type
- url
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- syslog
description: Sends each event to a syslog server.
example: syslog
host:
type: string
minLength: 1
description: Syslog server host.
example: syslog.example.com
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Syslog server port.
example: 514
protocol:
type: string
enum:
- udp
- tcp
- tls
description: Transport protocol.
example: udp
facility:
type: integer
minimum: 0
maximum: 23
description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.'
example: 16
app_name:
type: string
description: Application name reported in the syslog message.
example: n8n
tlsCa:
type: string
description: PEM-encoded CA certificate used when `protocol` is `tls`.
required:
- type
- host
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- sentry
description: Sends each event to a Sentry project.
example: sentry
dsn:
type: string
format: uri
description: Sentry DSN the events are sent to.
example: https://examplePublicKey@o0.ingest.sentry.io/0
required:
- type
- dsn
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: ../../../../shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml
'400':
$ref: ../../../../shared/spec/responses/badRequest.yml
'401':
$ref: ../../../../shared/spec/responses/unauthorized.yml
'403':
$ref: ../../../../shared/spec/responses/forbidden.yml
'404':
$ref: ../../../../shared/spec/responses/notFound.yml
'409':
$ref: ../../../../shared/spec/responses/conflict.yml
'415':
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml

View File

@ -171,9 +171,13 @@ paths:
/settings/log-streaming/destinations:
get:
$ref: ./handlers/settings/spec/paths/getLogStreamingDestinations.generated.yml
post:
$ref: ./handlers/settings/spec/paths/createLogStreamingDestination.generated.yml
/settings/log-streaming/destinations/{id}:
get:
$ref: ./handlers/settings/spec/paths/getLogStreamingDestination.generated.yml
put:
$ref: ./handlers/settings/spec/paths/updateLogStreamingDestination.generated.yml
/node-type-policies/instance:
get:
$ref: ./handlers/node-type-policies/spec/paths/getNodeTypeInstancePolicy.generated.yml

View File

@ -85,8 +85,6 @@ paths:
$ref: './handlers/n8n-packages/spec/paths/n8n-packages.import.yml'
/n8n-packages/import-selection:
$ref: './handlers/n8n-packages/spec/paths/n8n-packages.import-selection.yml'
/settings/log-streaming/destinations:
$ref: './handlers/log-streaming/spec/paths/destinations.yml'
/settings/log-streaming/destinations/{id}/test:
$ref: './handlers/log-streaming/spec/paths/destinations.id.test.yml'
/settings/log-streaming/destinations/{id}:

View File

@ -0,0 +1,354 @@
oneOf:
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- webhook
description: Sends each event as an HTTP request to a URL.
example: webhook
url:
type: string
format: uri
description: Target URL that receives the event payload.
example: https://example.com/n8n-events
method:
type: string
description: HTTP method used for the request.
example: POST
sendHeaders:
type: boolean
description: Whether to attach custom headers to the request.
example: false
specifyHeaders:
type: string
description: How custom headers are provided — `keypair` or `json`.
example: keypair
headerParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonHeaders:
type: string
description: Custom headers as a JSON string when `specifyHeaders` is `json`.
sendQuery:
type: boolean
description: Whether to attach query parameters to the request.
example: false
specifyQuery:
type: string
description: How query parameters are provided — `keypair` or `json`.
queryParameters:
type: object
properties:
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: Authorization
value:
anyOf:
- type: string
- type: number
- type: boolean
- {}
- {}
description: Parameter value. Usually a string; numbers, booleans and null are also accepted.
example: Bearer <token>
required:
- name
- value
required:
- parameters
description: Key-value pairs used when the matching `specify*` field is `keypair`.
jsonQuery:
type: string
description: Query parameters as a JSON string when `specifyQuery` is `json`.
options:
type: object
properties:
timeout:
type: integer
minimum: 0
exclusiveMinimum: true
description: Request timeout in milliseconds.
allowUnauthorizedCerts:
type: boolean
description: Accept self-signed or otherwise invalid TLS certificates.
queryParameterArrays:
type: string
enum:
- indices
- brackets
- repeat
description: How array-valued query parameters are serialized into the query string.
redirect:
type: object
properties:
redirect:
type: object
properties:
followRedirects:
type: boolean
description: Whether to follow HTTP redirects.
maxRedirects:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum number of redirects to follow.
required:
- redirect
description: Redirect-following behaviour.
proxy:
type: object
properties:
proxy:
type: object
properties:
protocol:
type: string
enum:
- https
- http
description: Proxy protocol.
host:
type: string
description: Proxy host, without protocol or port.
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Proxy port.
required:
- protocol
- host
- port
required:
- proxy
description: Outbound proxy configuration.
socket:
type: object
properties:
keepAlive:
type: boolean
description: Whether to keep sockets open for reuse.
maxSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum sockets per host kept open at once.
maxFreeSockets:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum idle sockets per host kept open.
description: Connection socket options.
description: Additional HTTP request options.
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- url
- id
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- syslog
description: Sends each event to a syslog server.
example: syslog
host:
type: string
minLength: 1
description: Syslog server host.
example: syslog.example.com
port:
type: integer
minimum: 0
exclusiveMinimum: true
description: Syslog server port.
example: 514
protocol:
type: string
enum:
- udp
- tcp
- tls
description: Transport protocol.
example: udp
facility:
type: integer
minimum: 0
maximum: 23
description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.'
example: 16
app_name:
type: string
description: Application name reported in the syslog message.
example: n8n
tlsCa:
type: string
description: PEM-encoded CA certificate used when `protocol` is `tls`.
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- host
- id
- type: object
properties:
label:
type: string
minLength: 1
description: Human-readable name shown in the UI.
example: My destination
enabled:
type: boolean
description: Whether the destination currently receives events.
example: true
subscribedEvents:
type: array
items:
type: string
description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`.
example:
- n8n.workflow
- n8n.audit
anonymizeAuditMessages:
type: boolean
description: Whether audit message payloads are anonymized before being sent.
example: false
circuitBreaker:
type: object
properties:
maxFailures:
type: integer
minimum: 0
exclusiveMinimum: true
description: Maximum failures within the sliding window before the breaker opens.
failureWindow:
type: integer
minimum: 0
exclusiveMinimum: true
description: Sliding window, in milliseconds, over which failures are counted.
description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted.
type:
type: string
enum:
- sentry
description: Sends each event to a Sentry project.
example: sentry
dsn:
type: string
format: uri
description: Sentry DSN the events are sent to.
example: https://examplePublicKey@o0.ingest.sentry.io/0
id:
type: string
description: Server-generated unique id of the destination. Returned in responses; not accepted in requests.
example: 88be6560-bfb4-455c-8aa1-06971e9e5522
required:
- type
- dsn
- id
description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.'

View File

@ -151,7 +151,8 @@ describe('GET /discover', () => {
expect(withSchema.length).toBeGreaterThan(0);
for (const endpoint of withSchema) {
expect((endpoint as Record<string, unknown>).requestSchema).toHaveProperty('type');
const schema = (endpoint as { requestSchema: Record<string, unknown> }).requestSchema;
expect('type' in schema || 'oneOf' in schema).toBe(true);
}
});

View File

@ -279,6 +279,22 @@ describe('Log streaming in Public API', () => {
expect(response.status).toBe(400);
});
it('drops fields that do not belong to the destination type', async () => {
const response = await testServer
.publicApiAgentFor(owner)
.post('/settings/log-streaming/destinations')
.send({
...webhookPayload,
dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
unknownField: 'value',
});
expect(response.status).toBe(200);
expect(response.body.type).toBe('webhook');
expect(response.body).not.toHaveProperty('dsn');
expect(response.body).not.toHaveProperty('unknownField');
});
it('creates a fully-populated webhook destination', async () => {
const payload = {
type: 'webhook',
@ -304,12 +320,14 @@ describe('Log streaming in Public API', () => {
.send(payload);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toMatchObject(payload);
expect(response.body).not.toHaveProperty('responseCodeMustMatch');
expect(response.body).not.toHaveProperty('sendPayload');
expect(response.body).not.toHaveProperty('authentication');
expect(response.body).not.toHaveProperty('credentials');
// Strict match: `responseCodeMustMatch`, `sendPayload`, `authentication` and
// `credentials` must not appear.
expect(response.body).toStrictEqual({
id: expect.any(String),
...payload,
jsonHeaders: '',
jsonQuery: '',
});
});
it('creates a fully-populated syslog destination', async () => {
@ -333,8 +351,7 @@ describe('Log streaming in Public API', () => {
.send(payload);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toMatchObject(payload);
expect(response.body).toStrictEqual({ id: expect.any(String), ...payload });
});
it('creates a fully-populated sentry destination', async () => {
@ -354,8 +371,7 @@ describe('Log streaming in Public API', () => {
.send(payload);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toMatchObject(payload);
expect(response.body).toStrictEqual({ id: expect.any(String), ...payload });
});
it('rejects a malformed body with 400', async () => {
@ -399,6 +415,22 @@ describe('Log streaming in Public API', () => {
expect(listResponse.body.data[0].label).toBe('Renamed webhook');
});
it('accepts a GET response as the body and keeps the path id', async () => {
const created = await createDestination();
const read = await testServer
.publicApiAgentFor(owner)
.get(`/settings/log-streaming/destinations/${created.id}`);
const response = await testServer
.publicApiAgentFor(owner)
.put(`/settings/log-streaming/destinations/${created.id}`)
.send({ ...read.body, id: '99999999-9999-4999-8999-999999999999', label: 'Round trip' });
expect(response.status).toBe(200);
expect(response.body.id).toBe(created.id);
expect(response.body.label).toBe('Round trip');
});
it('returns 404 for an unknown destination id', async () => {
const response = await testServer
.publicApiAgentFor(owner)