UNPKG

@ogcio/o11y-sdk-node

Version:

Opentelemetry standard instrumentation SDK for NodeJS based project

150 lines (149 loc) 6.36 kB
import { OTLPExporterBase, } from "@opentelemetry/otlp-exporter-base"; import { redactors, } from "../internals/redaction/redactors/index.js"; import { _cleanStringPII, _recursiveObjectClean, BasicRedactor, } from "../../../sdk-core/index.js"; export class PIIExporterDecorator extends OTLPExporterBase { _exporter; _config; _redactors; constructor(exporter, config) { super( // @ts-expect-error we are intentionally overriding the protected _delegate property to inject PII redaction before exporting exporter._delegate["_delegate"]); this._exporter = exporter; this._config = config; // Build the enabled redactors once from config (defaults to all built-ins unless disabled). this._redactors = this._buildRedactors(config?.detection); } forceFlush() { return this._exporter.forceFlush(); } shutdown() { return this._exporter.shutdown(); } export(items, resultCallback) { // Redaction entrypoint: invoked by the SDK for every batch just before it leaves the process, // so every span/log/metric is scrubbed of PII before being handed to the real OTLP exporter. if (this._redactors.length === 0) { this._exporter.export(items, resultCallback); return; } if (Array.isArray(items)) { const redactedItem = items.map((item) => { if (this._isReadableSpan(item)) { this._redactSpan(item); } else if (this._isReadableLogRecord(item)) { item = this._redactLogRecord(item); } return item; }); this._exporter.export(redactedItem, resultCallback); return; } if (this._isResourceMetrics(items)) { this._redactResourceMetrics(items); this._exporter.export(items, resultCallback); } } _isReadableSpan(span) { return (typeof span === "object" && span !== null && "name" in span && "kind" in span && "spanContext" in span && "attributes" in span); } _isReadableLogRecord(span) { return (typeof span === "object" && span !== null && "body" in span && "attributes" in span && "severityText" in span && "severityNumber" in span); } _isResourceMetrics(obj) { return (typeof obj === "object" && obj !== null && !Array.isArray(obj) && "resource" in obj && "scopeMetrics" in obj); } // Scrubs a span in place: name, attributes, resource, and every link/event. _redactSpan(span) { Object.assign(span, { name: _cleanStringPII(span.name, "trace", this._redactors), attributes: span.attributes && _recursiveObjectClean(span.attributes, "trace", this._redactors), resource: { attributes: span?.resource?.attributes && _recursiveObjectClean(span.resource.attributes, "trace", this._redactors), }, links: span?.links?.map((link) => { Object.assign(link, { attributes: link?.attributes && _recursiveObjectClean(link.attributes, "trace", this._redactors), }); return link; }), events: span?.events?.map((event) => { Object.assign(event, { name: _cleanStringPII(event.name, "trace", this._redactors), attributes: event?.attributes && _recursiveObjectClean(event.attributes, "trace", this._redactors), }); return event; }), }); } // Returns a redacted copy of a log record: body, attributes, and resource. _redactLogRecord(log) { return { ...log, body: _recursiveObjectClean(log.body, "log", this._redactors), attributes: log.attributes && _recursiveObjectClean(log.attributes, "log", this._redactors), resource: log.resource && { ...log.resource, attributes: _recursiveObjectClean(log.resource.attributes, "log", this._redactors), }, }; } // Scrubs metrics in place: resource attributes plus every data point's label attributes. _redactResourceMetrics(metric) { Object.assign(metric, { resource: { attributes: metric?.resource?.attributes && _recursiveObjectClean(metric.resource.attributes, "metric", this._redactors), }, }); // Redact PII from every data point's attributes (metric labels/dimensions). // These are distinct from resource attributes and are a common place for // request-derived values (e.g. IPs) to leak into exported metrics. // Most of the properties of the metric interface are readonly primitives, // we need to navigate down to the datapoint attributes themselves before being able to apply Object assign for (const scopeMetric of metric?.scopeMetrics ?? []) { for (const metricData of scopeMetric?.metrics ?? []) { for (const dataPoint of metricData?.dataPoints ?? []) { if (!dataPoint?.attributes) { continue; } Object.assign(dataPoint, { attributes: _recursiveObjectClean(dataPoint.attributes, "metric", this._redactors), }); } } } } // Default opt-in every redactor available, excluding only those explicitly configured to false _buildRedactors(redactorsConfig = {}) { const defaultRedactors = Object.entries(redactors) .filter(([key]) => { return redactorsConfig[key] !== false; }) .map(([_, value]) => value); const customRedactors = redactorsConfig.custom?.length ? redactorsConfig.custom.filter((redactor) => redactor instanceof BasicRedactor) : []; return [...defaultRedactors, ...customRedactors]; } }