@comprehend/telemetry-browser
Version:
Integration of comprehend.dev with OpenTelemetry in browser environments.
191 lines (190 loc) • 7.7 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComprehendDevSpanProcessor = void 0;
const sha2_1 = require("@noble/hashes/sha2");
const utils_1 = require("@noble/hashes/utils");
const WebSocketConnection_1 = require("./WebSocketConnection");
class ComprehendDevSpanProcessor {
connection;
observedServices = [];
observedHttpServices = [];
observedInteractions = new Map();
observationsSeq = 1;
constructor(options) {
this.connection = new WebSocketConnection_1.WebSocketConnection(options.organization, options.token, options.debug === true ? console.log : options.debug === false ? undefined : options.debug);
}
onStart(span, parentContext) {
}
onEnd(span) {
const currentService = this.discoverService(span);
if (!currentService)
return;
const attrs = span.attributes;
if (span.kind === 2) { // CLIENT span kind
if (attrs['http.url']) {
this.processHttpRequest(currentService, attrs['http.url'], span);
}
}
}
discoverService(span) {
// Look for an existing matching entry.
const resAttrs = span.resource.attributes;
const name = resAttrs['service.name'];
if (!name)
return;
const namespace = resAttrs['service.namespace'];
const environment = resAttrs['deployment.environment'];
const existing = this.observedServices.find(s => s.name === name &&
s.namespace === namespace &&
s.environment === environment);
if (existing)
return existing;
// New; hash it and add it to the observed services.
const idString = `service:${name}:${namespace ?? ''}:${environment ?? ''}`;
const hash = hashIdString(idString);
const newService = {
name, namespace, environment, hash
};
this.observedServices.push(newService);
// Ingest its existence.
const message = {
event: "new-entity",
type: "service",
hash,
name,
...(namespace ? { namespace } : {}),
...(environment ? { environment } : {})
};
this.ingestMessage(message);
return newService;
}
processHttpRequest(currentService, url, span) {
// Build identity based upon protocol, host, and port.
const parsed = new URL(url);
const protocol = parsed.protocol.replace(':', ''); // Remove trailing colon
const host = parsed.hostname;
const port = parsed.port ? parseInt(parsed.port) : (protocol === 'https' ? 443 : 80);
const idString = `http-service:${protocol}:${host}:${port}`;
const hash = hashIdString(idString);
// Ingest it if it's not already observed.
let observedHttpService = this.observedHttpServices.find(s => s.protocol === protocol && s.host === host && s.port === port);
if (!observedHttpService) {
observedHttpService = { protocol, host, port, hash };
this.observedHttpServices.push(observedHttpService);
// The existence of the service.
const message = {
event: "new-entity",
type: "http-service",
hash,
protocol,
host,
port
};
this.ingestMessage(message);
}
// Ingest the interaction if first observed.
const interactions = this.getInteractions(currentService.hash, observedHttpService.hash);
if (!interactions.httpRequest) {
const idString = `http-request:${currentService.hash}:${observedHttpService.hash}`;
const hash = hashIdString(idString);
interactions.httpRequest = { hash };
const message = {
event: "new-interaction",
type: "http-request",
hash,
from: currentService.hash,
to: observedHttpService.hash
};
this.ingestMessage(message);
}
// Build and ingest observation.
const attrs = span.attributes;
const path = parsed.pathname || '/';
const method = span.attributes['http.method'];
if (!method) // Really should always be there
return;
const status = attrs['http.status_code'];
const duration = span.duration;
const httpVersion = span.attributes['http.flavor'];
const requestBytes = attrs['http.request_content_length'];
const responseBytes = attrs['http.response_content_length'];
const { message: errorMessage, type: errorType, stack } = extractErrorInfo(span);
const observation = {
type: "http-client",
subject: interactions.httpRequest.hash,
timestamp: span.startTime,
path,
method,
...(status !== undefined ? { status } : {}),
duration,
...(httpVersion !== undefined ? { httpVersion } : {}),
...(requestBytes !== undefined ? { requestBytes } : {}),
...(responseBytes !== undefined ? { responseBytes } : {}),
...(errorMessage ? { errorMessage } : {}),
...(errorType ? { errorType } : {}),
...(stack ? { stack } : {})
};
this.ingestMessage({
event: "observations",
seq: this.observationsSeq++,
observations: [observation]
});
}
getInteractions(from, to) {
let fromMap = this.observedInteractions.get(from);
if (!fromMap) {
fromMap = new Map();
this.observedInteractions.set(from, fromMap);
}
let interactions = fromMap.get(to);
if (!interactions) {
interactions = { httpRequest: undefined };
fromMap.set(to, interactions);
}
return interactions;
}
ingestMessage(message) {
this.connection.sendMessage(message);
}
async forceFlush() {
}
async shutdown() {
this.connection.close();
}
}
exports.ComprehendDevSpanProcessor = ComprehendDevSpanProcessor;
function hashIdString(idString) {
return Array.from((0, sha2_1.sha256)((0, utils_1.utf8ToBytes)(idString)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/** Try pretty hard to get error info out of a span, if it has any. Handles it being there as an event,
* directly on the span with error semantics, and some other more ad-hoc cases. */
function extractErrorInfo(span) {
const attrs = span.attributes;
// Try to extract from a structured 'exception' event, as it should have more detail
const exceptionEvent = span.events.find(e => e.name === 'exception');
if (exceptionEvent?.attributes) {
const message = exceptionEvent.attributes['exception.message'];
const type = exceptionEvent.attributes['exception.type'];
const stack = exceptionEvent.attributes['exception.stacktrace'];
if (message || type || stack) {
return { message, type, stack };
}
}
// Fallback to attributes directly on the span.
const isError = span.status.code === 2;
const message = attrs['exception.message'] ??
attrs['http.error_message'] ??
(isError ? attrs['otel.status_description'] : undefined) ??
(isError ? span.status.message : undefined);
const type = attrs['exception.type'] ??
attrs['error.type'] ??
attrs['http.error_name'];
const stack = attrs['exception.stacktrace'];
return {
message,
type,
stack
};
}
;