@mwcp/otel
Version:
midway component for open telemetry
294 lines • 10.6 kB
JavaScript
import assert from 'node:assert';
import { makeHttpRequest } from '@midwayjs/core';
import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_SCHEME, SEMATTRS_HTTP_SERVER_NAME, SEMATTRS_NET_HOST_NAME, } from '@opentelemetry/semantic-conventions';
import { sleep } from '@waiting/shared-core';
import { AttrNames } from '../lib/attrnames.types.js';
import { exporterEndpoint } from '../lib/config.js';
// #region retrieveTraceInfoFromRemote
const agent = exporterEndpoint.replace(/:\d+$/u, '');
export async function retrieveTraceInfoFromRemote(traceId, expectSpanNumber) {
assert(agent, 'OTEL_EXPORTER_OTLP_ENDPOINT not set');
let id = traceId;
if (traceId.includes('-')) {
const txt = traceId.split('-').at(1);
assert(txt);
id = txt;
}
await sleep(1000);
// console.log('retrieveTraceInfoFromRemote: ', { traceId })
const tracePath = `${agent}:16686/api/traces/${id}?prettyPrint=true`;
let resp = await makeHttpRequest(tracePath, {
method: 'GET',
dataType: 'json',
})
/* c8 ignore next 4 */
.catch((err) => {
console.error(`retrieve trace info failed, check agent "${agent}" valid or OTEL_EXPORTER_OTLP_ENDPOINT is correct.`);
throw err;
});
for (let i = 0; i < 30; i += 1) {
assert(resp.status !== 401, `Expect not 401, trace: "${tracePath}"`);
assert(resp.status !== 404, `Expect not 404, trace: "${tracePath}"`);
assert(resp.status !== 500, `Expect not 500, trace: "${tracePath}"`);
if (resp.status === 200 && resp.data) {
break;
}
/* c8 ignore start */
const { data } = resp.data;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (data.length > 0) {
break;
}
// console.log('retry traceId...')
await sleep(2000);
resp = await makeHttpRequest(tracePath, {
method: 'GET',
dataType: 'json',
});
/* c8 ignore stop */
}
const { data } = resp.data;
/* c8 ignore next 3 */
if (!expectSpanNumber) {
return data;
}
let { spans } = data[0];
if (spans.length >= expectSpanNumber) {
return data;
}
/* c8 ignore start */
for (let i = 0; i < 30; i += 1) {
// console.info('retry span... idx:', i)
await sleep(2000);
const resp2 = await retrieveTraceInfoFromRemote(traceId);
spans = resp2[0].spans;
// console.log('spans.length:', spans.length)
if (spans.length >= expectSpanNumber) {
return resp2;
}
}
assert(false, `spans.length: ${spans.length}, expectSpanNumber: ${expectSpanNumber}`);
/* c8 ignore stop */
}
// #region sortSpans
// 对于 JaegerTraceInfo.spans 入参,对于 JaegerTraceInfo.spans 根据 span.reference 的 spanID 依赖关系进行排序
// 以确保 root span -> span0 -> span1 -> span2 的顺序
export function sortSpans(spans) {
assert(spans.length > 0, 'sortSpans() spans.length === 0');
const map = new Map();
spans.forEach((span) => {
map.set(span.spanID, span);
});
const ret = [];
// find root
// spans.forEach((span) => {
for (const span of spans) {
const parentSpan = getParentSpan(span, map);
if (!parentSpan) { // root span
ret.push(span);
break;
}
}
const parentSpan = ret.at(-1);
assert(parentSpan);
mergeSpans(parentSpan, spans, ret);
return ret;
}
function mergeSpans(parentSpan, srcSpans, result) {
assert(parentSpan);
const pid = parentSpan.spanID;
const childSpans = getChildren(pid, srcSpans);
if (childSpans.length) { // has child node
if (parentSpan !== result.at(-1)) {
result.push(parentSpan);
}
childSpans.forEach((child) => {
mergeSpans(child, srcSpans, result);
});
}
else if (parentSpan !== result.at(-1)) { // leaf node
result.push(parentSpan);
}
}
function getParentSpan(span, map) {
if (!span.references.length) {
return;
}
const ref = span.references[0];
/* c8 ignore next */
if (!ref) {
return;
}
return map.get(ref.spanID);
}
function getChildren(parentSpanId, spans) {
const ret = [];
assert(parentSpanId);
spans.forEach((span) => {
const ref = span.references[0];
if (ref && ref.spanID === parentSpanId) {
ret.push(span);
}
});
if (ret.length > 1) {
sortSpansByStartTime(ret);
}
return ret;
}
function sortSpansByStartTime(spans) {
return spans.sort((p1, p2) => {
return p1.startTime - p2.startTime;
});
}
export function assertRootSpan(options) {
const { path, span, tags } = options;
const scheme = options.scheme ?? 'http';
const operationName = options.operationName ?? `HTTP GET ${path}`;
const httpMethod = scheme === 'http'
? operationName.includes('GET') ? 'GET' : 'POST'
: 'RPC';
const expectLogs = options.logs ?? [];
const mergeDefaultTags = options.mergeDefaultTags ?? true;
const mergeDefaultLogs = options.mergeDefaultLogs ?? true;
const tags2 = mergeDefaultTags
? Object.assign({
[SEMATTRS_HTTP_METHOD]: httpMethod,
[SEMATTRS_HTTP_SCHEME]: scheme,
[SEMATTRS_HTTP_SERVER_NAME]: 'base-app',
[SEMATTRS_NET_HOST_NAME]: '127.0.0.1',
[AttrNames.ServiceName]: 'base-app',
[AttrNames.ServiceVersion]: '1.0.0',
'span.kind': 'server',
}, tags)
: Object.assign({}, tags);
const logBase = [
{ event: AttrNames.RequestBegin },
{ event: AttrNames.Incoming_Request_data },
{ event: AttrNames.PreProcessFinish },
{ event: AttrNames.PostProcessBegin },
{ event: AttrNames.Outgoing_Response_data, [AttrNames.Http_Response_Code]: 200 },
{ event: AttrNames.RequestEnd },
];
const logs = [];
if (mergeDefaultLogs) {
for (let idx = 0; idx < Math.max(logBase.length, expectLogs.length, options.logs?.length ?? 0); idx += 1) {
const log = logBase[idx] ?? {};
const expectRow = expectLogs[idx];
if (expectRow === false) {
continue;
}
if (expectRow && Object.keys(expectRow).length) {
const row = Object.assign(log, expectRow);
logs.push(row);
continue;
}
logs.push(log);
}
}
else {
options.logs?.forEach((log) => {
if (log) {
logs.push(log);
}
});
}
const opt = {
traceId: options.traceId,
operationName,
tags: tags2,
logs,
};
assertsSpan(span, opt);
}
export function assertsSpan(span, options) {
assert(span, 'span is null');
assert(options, 'options is null');
assert(span.traceID === options.traceId);
assert(span.operationName === options.operationName, `operationName: ${span.operationName} !== (expect) ${options.operationName}`);
Object.entries(options.tags ?? {}).forEach(([key, expectValue]) => {
const flag = span.tags.some((tag) => {
if (tag['key'] !== key) {
return false;
}
assertJaegerTagItem(tag, key, expectValue);
return true;
});
assert(flag, `${key}: ${expectValue.toString()} not found`);
});
if (options.logs?.length) {
options.logs.forEach((expectLog, idx) => {
/* c8 ignore next 2 */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!expectLog) {
return;
}
const log = span.logs[idx];
assert(log, `log[${idx}] is null`);
Object.entries(expectLog).forEach(([key, expectValue]) => {
const flag = log.fields.some((field) => {
if (field.key !== key) {
return false;
}
assertJaegerLogField(field, key, expectValue);
return true;
});
assert(flag, `(${idx}) ${key}: ${expectValue.toString()} not found`);
});
});
}
}
export function assertJaegerTagItem(tag, key, expectValue) {
const tagVal = tag['value'];
assert(typeof tagVal !== 'undefined', `${key}: tagVal from span is null`);
if (expectValue instanceof RegExp) {
assert(expectValue.test(tagVal.toString()), `${key}: ${tagVal.toString()} !== (expect) ${expectValue.toString()}`);
}
else {
const res = tagVal === expectValue;
assert(res, `${key}: ${tagVal.toString()} !== (expect) ${expectValue.toString()}`);
}
}
export function assertJaegerLogField(field, key, expectValue) {
const fieldVal = field.value;
assert(typeof fieldVal !== 'undefined', `${key}: fieldVal from span is null`);
if (expectValue instanceof RegExp) {
assert(expectValue.test(fieldVal.toString()), `${key}: ${expectValue.toString()} !== (expect) ${fieldVal.toString()}`);
}
else {
const res = fieldVal === expectValue;
assert(res, `${key}: ${fieldVal.toString()} !== (expect) ${expectValue.toString()}`);
}
}
export function retrieveTraceparentFromHeader(headers) {
assert(headers, 'headers is null');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const traceparent = typeof headers.get === 'function'
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
? headers.get('traceparent')
// @ts-ignore
: headers['traceparent'];
if (!traceparent) {
return;
}
const [version, traceId, parentId, traceFlags] = traceparent.split('-');
return {
version,
traceId,
parentId,
traceFlags,
};
}
export function assertJaegerParentSpanArray(input) {
input.forEach((row) => {
assertJaegerParentSpan(row.parentSpan, row.childSpan);
});
}
// #region assertJaegerParentSpan
export function assertJaegerParentSpan(parentSpan, childSpan) {
assert(parentSpan, 'parentSpan is null');
assert(childSpan, 'childSpan is null');
assert(parentSpan.spanID === childSpan.references[0]?.spanID, `parentSpan.spanID: ${parentSpan.spanID},
childSpan: ${childSpan.spanID},
childSpan.parent spanID: ${childSpan.references[0]?.spanID}`);
}
//# sourceMappingURL=common.js.map