@ogcio/o11y-sdk-node
Version:
Opentelemetry standard instrumentation SDK for NodeJS based project
150 lines (149 loc) • 6.36 kB
JavaScript
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];
}
}