Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(mock-server): make ui printer optional #165

Merged
merged 10 commits into from
May 7, 2024
2 changes: 1 addition & 1 deletion packages/mockotlpserver/db/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# UI traces folder

this folder is to place all the traces data we want to plt in the web ui.
this folder is to place all the traces data we want to plot in the web ui.
21 changes: 19 additions & 2 deletions packages/mockotlpserver/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -50,6 +50,8 @@ 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`
Expand Down Expand Up @@ -97,6 +99,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.`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: don't need backtick string here... though I guess lint doesn't check that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't

},
];

async function main() {
Expand All @@ -121,9 +128,15 @@ async function main() {
process.exit(0);
}

/** @type {Array<'http'|'grpc'|'ui'>} */
const services = ['http', 'grpc'];
if (opts.ui) {
services.push('ui');
}

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,
Expand Down Expand Up @@ -186,6 +199,10 @@ async function main() {
case 'logs-summary':
printers.push(new LogsSummaryPrinter(log));
break;

case 'trace-file':
printers.push(new FilePrinter(log));
break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the trace-file "printer" be added the printers automatically if --ui is specified?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One may want to open the UI just to inspect traces from past sessions but I think you question points to the case someone starting the UI but forgetting to put the printer in place, therefore not being able to se traces in the new traces UI. I think is as good point, I'll add it in the next commit

}
});
printers.forEach((p) => p.subscribe());
Expand Down
58 changes: 58 additions & 0 deletions packages/mockotlpserver/lib/printers.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
* printer.subscribe();
*/

const fs = require('fs');
const path = require('path');

const {
diagchSub,
CH_OTLP_V1_LOGS,
Expand Down Expand Up @@ -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 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note for reviewer: detaching the printer from the UI service makes sense. A possible scenario is to store traces for further analysis with other tools. Also changing the usage of jsonStringifyTrace makes the payload more compact so files are smaller in size

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 als spans from the same trace into an ndjson file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// Group als spans from the same trace into an ndjson file
// 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,
};
105 changes: 10 additions & 95 deletions packages/mockotlpserver/lib/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, import('./types').Span[]>} */
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
*/
Expand Down Expand Up @@ -152,7 +61,6 @@ class UiService extends Service {
super();
this._opts = opts;
this._server = null;
this._printer = new UiPrinter(opts.log);
}

async start() {
Expand All @@ -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/', '');
Expand Down Expand Up @@ -211,8 +128,6 @@ class UiService extends Service {
});
});

this._printer.subscribe();

return new Promise((resolve, reject) => {
this._server.listen(port, hostname, () => {
resolve();
Expand Down
Loading