diff --git a/packages/mockotlpserver/db/README.md b/packages/mockotlpserver/db/README.md index 66ad2fb2..1d8cd94c 100644 --- a/packages/mockotlpserver/db/README.md +++ b/packages/mockotlpserver/db/README.md @@ -1,3 +1,3 @@ # UI traces folder -this folder is to place all the traces data we want to plt in the web ui. \ No newline at end of file +this folder is to place all the traces data we want to plot in the web ui. \ No newline at end of file diff --git a/packages/mockotlpserver/lib/cli.js b/packages/mockotlpserver/lib/cli.js index a0523d89..0b2ed54b 100755 --- a/packages/mockotlpserver/lib/cli.js +++ b/packages/mockotlpserver/lib/cli.js @@ -24,7 +24,7 @@ const dashdash = require('dashdash'); const luggite = require('./luggite'); -const {JSONPrinter, InspectPrinter} = require('./printers'); +const {JSONPrinter, InspectPrinter, FilePrinter} = require('./printers'); const {TraceWaterfallPrinter} = require('./waterfall'); const {MetricsSummaryPrinter} = require('./metrics-summary'); const {LogsSummaryPrinter} = require('./logs-summary'); @@ -50,10 +50,19 @@ const PRINTER_NAMES = [ 'metrics-summary', 'logs-summary', 'summary', + + 'trace-file', // saving into fs for UI and other processing ]; -// This adds a custom cli option type to dashdash, to support `-o json,waterfall` -// options for specifying multiple printers (aka output modes). +/** + * This adds a custom cli option type to dashdash, to support `-o json,waterfall` + * options for specifying multiple printers (aka output modes). + * + * @param {any} option + * @param {string} optstr + * @param {string} arg + * @returns {Array} + */ function parseCommaSepPrinters(option, optstr, arg) { const printers = arg .trim() @@ -97,6 +106,11 @@ const OPTIONS = [ type: 'string', help: `The hostname on which servers should listen, by default this is "${DEFAULT_HOSTNAME}".`, }, + { + names: ['ui'], + type: 'bool', + help: 'Start a web server to inspect traces with some charts.', + }, ]; async function main() { @@ -121,17 +135,30 @@ async function main() { process.exit(0); } + /** @type {Array<'http'|'grpc'|'ui'>} */ + const services = ['http', 'grpc']; + /** @type {Array} */ + const outputs = opts.o; + + if (opts.ui) { + services.push('ui'); + outputs.push('trace-file'); + } + const otlpServer = new MockOtlpServer({ log, - services: ['http', 'grpc', 'ui'], + services, grpcHostname: opts.hostname || DEFAULT_HOSTNAME, httpHostname: opts.hostname || DEFAULT_HOSTNAME, uiHostname: opts.hostname || DEFAULT_HOSTNAME, }); await otlpServer.start(); + // Avoid duplication of printers + const printersSet = new Set(outputs); const printers = []; - opts.o.forEach((printerName) => { + + printersSet.forEach((printerName) => { switch (printerName) { case 'trace-inspect': printers.push(new InspectPrinter(log, ['trace'])); @@ -186,6 +213,10 @@ async function main() { case 'logs-summary': printers.push(new LogsSummaryPrinter(log)); break; + + case 'trace-file': + printers.push(new FilePrinter(log)); + break; } }); printers.forEach((p) => p.subscribe()); diff --git a/packages/mockotlpserver/lib/printers.js b/packages/mockotlpserver/lib/printers.js index 46e6bb31..25048b12 100644 --- a/packages/mockotlpserver/lib/printers.js +++ b/packages/mockotlpserver/lib/printers.js @@ -26,6 +26,9 @@ * printer.subscribe(); */ +const fs = require('fs'); +const path = require('path'); + const { diagchSub, CH_OTLP_V1_LOGS, @@ -153,8 +156,63 @@ class JSONPrinter extends Printer { } } +/** + * This printer converts to a possible JSON representation of each service + * request and saves it to a file. **Warning**: Converting OTLP service requests to JSON is fraught. + */ +class FilePrinter extends Printer { + constructor( + log, + indent, + signals = ['trace'], + dbDir = path.resolve(__dirname, '../db') + ) { + super(log); + this._indent = indent || 0; + this._signals = signals; + this._dbDir = dbDir; + } + printTrace(trace) { + if (!this._signals.includes('trace')) return; + const str = jsonStringifyTrace(trace, { + indent: this._indent, + normAttributes: true, + }); + const normTrace = JSON.parse(str); + const tracesMap = new Map(); + normTrace.resourceSpans.forEach((resSpan) => { + resSpan.scopeSpans.forEach((scopeSpan) => { + scopeSpan.spans.forEach((span) => { + let traceSpans = tracesMap.get(span.traceId); + + if (!traceSpans) { + traceSpans = []; + tracesMap.set(span.traceId, traceSpans); + } + traceSpans.push(span); + }); + }); + }); + + // Group all spans from the same trace into an ndjson file. + for (const [traceId, traceSpans] of tracesMap.entries()) { + const filePath = path.join(this._dbDir, `trace-${traceId}.ndjson`); + const stream = fs.createWriteStream(filePath, { + flags: 'a', + encoding: 'utf-8', + }); + + for (const span of traceSpans) { + stream.write(JSON.stringify(span) + '\n'); + } + stream.close(); + } + } +} + module.exports = { Printer, JSONPrinter, InspectPrinter, + FilePrinter, }; diff --git a/packages/mockotlpserver/lib/ui.js b/packages/mockotlpserver/lib/ui.js index f3ab29cd..41eedca7 100644 --- a/packages/mockotlpserver/lib/ui.js +++ b/packages/mockotlpserver/lib/ui.js @@ -21,101 +21,10 @@ const fs = require('fs'); const path = require('path'); const http = require('http'); -const Long = require('long'); - -const {Printer} = require('./printers'); const {Service} = require('./service'); // helper functions -/** - * @typedef {Object} SpanTree - * @property {import('./types').Span} span - * @property {import('./types').Span[]} children - */ - -/** - * @typedef {Object} TraceTree - * @property {string} id - * @property {SpanTree[]} children - */ - -class UiPrinter extends Printer { - constructor(log) { - super(log); - this._dbDir = path.resolve(__dirname, '../db'); - } - - /** - * Prints into files the spns belonging to a trace - * @param {import('./types').ExportTraceServiceRequest} traceReq - */ - printTrace(traceReq) { - /** @type {Map} */ - const tracesMap = new Map(); - - // Group all spans by trace - traceReq.resourceSpans.forEach((resSpan) => { - resSpan.scopeSpans.forEach((scopeSpan) => { - scopeSpan.spans.forEach((span) => { - const traceId = span.traceId.toString('hex'); - let traceSpans = tracesMap.get(traceId); - - if (!traceSpans) { - traceSpans = []; - tracesMap.set(traceId, traceSpans); - } - traceSpans.push(span); - }); - }); - }); - - // Write into a file - // TODO: manage lifetime of old trace ndjson files. - for (const [traceId, traceSpans] of tracesMap.entries()) { - const filePath = path.join(this._dbDir, `trace-${traceId}.ndjson`); - const stream = fs.createWriteStream(filePath, { - flags: 'a', - encoding: 'utf-8', - }); - - for (const span of traceSpans) { - const formatted = this._formatSpan(span); - stream.write( - JSON.stringify(Object.assign({}, span, formatted)) + '\n' - ); - } - stream.close(); - } - } - - /** - * @param {import('./types').Span} span - */ - _formatSpan(span) { - const traceId = span.traceId.toString('hex'); - const spanId = span.spanId.toString('hex'); - const parentSpanId = span.parentSpanId?.toString('hex'); - const formatted = {traceId, spanId}; - - if (parentSpanId) { - formatted.parentSpanId = parentSpanId; - } - formatted.startTimeUnixNano = new Long( - span.startTimeUnixNano.low, - span.startTimeUnixNano.high, - span.startTimeUnixNano.unsigned - ).toString(); - formatted.endTimeUnixNano = new Long( - span.endTimeUnixNano.low, - span.endTimeUnixNano.high, - span.endTimeUnixNano.unsigned - ).toString(); - - return formatted; - } -} - /** * @param {http.ServerResponse} res */ @@ -152,7 +61,6 @@ class UiService extends Service { super(); this._opts = opts; this._server = null; - this._printer = new UiPrinter(opts.log); } async start() { @@ -170,9 +78,18 @@ class UiService extends Service { const traceFiles = fs.readdirSync(dataPath).filter((f) => { return f.startsWith('trace-'); }); + const sortedFiles = traceFiles.sort((fileA, fileB) => { + const statA = fs.statSync(`${dataPath}/${fileA}`); + const statB = fs.statSync(`${dataPath}/${fileB}`); + + return ( + new Date(statB.birthtime).getTime() - + new Date(statA.birthtime).getTime() + ); + }); res.writeHead(200, {'Content-Type': 'application/json'}); - res.end(JSON.stringify(traceFiles)); + res.end(JSON.stringify(sortedFiles)); return; } else if (req.url.startsWith('/api/traces/')) { const traceId = req.url.replace('/api/traces/', ''); @@ -211,8 +128,6 @@ class UiService extends Service { }); }); - this._printer.subscribe(); - return new Promise((resolve, reject) => { this._server.listen(port, hostname, () => { resolve();