Custom Observability Destinations
on this page
Implement ObservabilityDestination when a backend is not covered by Loki or OTLP.
interface ObservabilityDestination {
readonly name: string;
readonly checkpointKey?: string;
readonly create: (
context: DestinationContext,
) => Result<DestinationExporters, ExportFailure>;
}A destination is inert configuration. create() receives immutable context.resourceAttributes and builds local exporter state. The context contains the caller’s optional flat resource record plus the runtime defaults for service.name and service.instance.id. It must not make network requests. Network work belongs in an export call or shutdown().
Exporters
create() returns any supported signal exporters:
interface DestinationExporters {
readonly logs?: LogExporter;
readonly metrics?: MetricExporter;
readonly shutdown?: (
context: ExportContext,
) => ResultAsync<void, ExportFailure>;
}
interface LogExporter {
export(
records: readonly LogEntry[],
context: LogExportContext,
): ResultAsync<ExportResult, ExportFailure>;
exportCanonical?(
batch: CanonicalLogBatch,
context: ExportContext,
): ResultAsync<ExportResult, ExportFailure>;
}
interface CanonicalLogBatch {
readonly records: readonly NormalizedLogRecord[];
readonly resourceAttributes: ResourceAttributes;
}
interface LogExportContext extends ExportContext {
readonly resourceAttributes?: ResourceAttributes;
}
interface MetricExporter {
export(
batch: {
timestamp: Date;
observations: readonly MetricObservation[];
},
context: ExportContext,
): ResultAsync<ExportResult, ExportFailure>;
}The AbortSignal in each ExportContext is cancelled when the configured delivery deadline expires. Pass it to fetch and any other cancellable I/O.
Return { rejectedRecords: 0 } after accepting an entire batch. A nonzero count is a terminal result for the whole batch: Launchpad counts accepted and rejected records, then acknowledges the batch. The protocol does not identify individual rejected records, so Launchpad does not retry them separately.
Map failures to ExportFailure:
type ExportFailure = Error & {
readonly retryable?: boolean;
readonly retryAfterMs?: number;
};Set retryable: false for a permanent failure. A retryable backend can provide retryAfterMs as a hint. Do not throw from create(), export(), or shutdown(); return Result or ResultAsync.
File-delivery support
File delivery is the default when logStorage is omitted, as well as when { type: 'file' } is explicit. A custom destination with a log exporter must provide both:
- a stable, credential-free
checkpointKeyon the destination; and - an
exportCanonical(batch, context)method on its log exporter.
The checkpoint identity combines the destination name, checkpoint key, and controller log source. Base the key on the logical target, such as a normalized endpoint. Do not include tokens, passwords, authorization headers, or the destination’s complete configuration. Keep it stable across ordinary credential rotation. Change the destination name when two accounts share one endpoint but require separate delivery histories.
During replay, Launchpad calls exportCanonical() with normalized records and their historical resource snapshot in batch.resourceAttributes. Use that snapshot instead of the setup-time resource from DestinationContext. This prevents a restarted process from relabeling historical logs with its new runtime identity. Built-in Loki and OTLP destinations implement both requirements.
CanonicalLogBatch.records contains validated NormalizedLogRecord values from the central source. They have already been normalized and redacted; preserve their resource and nested metadata without normalizing them again. Raw entries belong in export(), whose optional resource override does not make them canonical. Keeping these entry points separate lets TypeScript check the canonical record shape rather than relying on a context flag to skip redaction.
Custom log exporters without exportCanonical() must explicitly configure logStorage: { type: 'memory' } or implement replay support before upgrading. With explicit memory delivery, checkpointKey, exportCanonical(), and the per-call resource remain optional. Metrics-only exporters do not need log replay support.
The default file source contains the controller’s canonical records, not every raw custom bus event. Use explicit memory delivery for custom live-bus event capture, test contexts without a canonical source, or ephemeral delivery. There is no automatic memory fallback when the file source is unavailable.
Design limits
- Keep names, metric attributes, and resource attributes bounded.
- Treat resource attributes and metric-point attributes as separate scopes. Do not copy resource values into every point unless your destination protocol explicitly requires that representation.
- Preserve all resource attributes in destination formats that have a resource scope.
- Export only finite gauge values.
- Do not retain unbounded batches in destination state; Launchpad’s own queue is bounded, but it cannot bound a destination’s private memory.
- Treat shutdown as best effort and honor its abort signal.
- Avoid adding high-cardinality values such as timestamps, request IDs, file paths, or arbitrary state as metric attributes.