Skip to content

Commit

Permalink
feat: add a meter provider, enable host-metrics; starter metrics impr…
Browse files Browse the repository at this point in the history
…ovements for mockotlpserver (#25)

- This adds some starter improvements to mockotlpserver for handling incoming metrics OTLP requests. The `inspect` printer would already do fine. The `json` printer is slightly improved with some normalization -- though there are still unconverted Long instances that aren't super nice. There is a *start* at `metrics-summary` printer, but it is far from sufficient yet.
- The SDK now adds a meter provider (via a periodic exporter via OTLP/proto). By default every 30s.  
- There is a *lot* of data here from HostMetrics, so we may want to consider limiting that.  Note that this is using `@opentelemetry/[email protected]` which doesn't yet have your latest changes, David. IIRC your change will `fix process.cpu.* metrics`, but not reduce the large amount of data (each CPU and each state) coming through.
- Another note is that the `http.client.duration` metric has changing values when I send zero requests to the "http-server.js" example script (see below), so I'm not sure if we'll need to sanity check that data.
- I've started a couple hack/internal `ETEL_*` :)  envvars for configuring things.
- See #25 (comment) for details on why we are using `tsc --skipLibCheck` now.
  • Loading branch information
trentm authored Jan 24, 2024
1 parent 441d67f commit df6c0ab
Show file tree
Hide file tree
Showing 10 changed files with 3,430 additions and 6,595 deletions.
22 changes: 22 additions & 0 deletions examples/http-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Usage:
// node -r @elastic/opentelemetry-node/start.js http-server.js
// curl -i http://127.0.0.1:3000/ping

const http = require('http');

const server = http.createServer(function onRequest(req, res) {
console.log('incoming request: %s %s %s', req.method, req.url, req.headers);
req.resume();
req.on('end', function () {
const body = 'pong';
res.writeHead(200, {
'content-type': 'text/plain',
'content-length': Buffer.byteLength(body),
});
res.end(body);
});
});

server.listen(3000, '127.0.0.1', function () {
console.log('listening at', server.address());
});
9,720 changes: 3,168 additions & 6,552 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/mockotlpserver/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @elastic/mockotlpserver Changelog

## untagged

- Added a start at better metrics support:
- The `jsonN` output modes will some a somewhat normalized JSON
representation (similar to what is done to normalize trace request data).
- There is an experimental start at a `metrics-summary` printer:
`node lib/cli.js -o waterfall,metrics-summary`.

## v0.2.0

- Added the ability to use the mock OTLP server as a module, so it can
Expand Down
12 changes: 11 additions & 1 deletion packages/mockotlpserver/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ const dashdash = require('dashdash');
const luggite = require('./luggite');
const {JSONPrinter, InspectPrinter} = require('./printers');
const {TraceWaterfallPrinter} = require('./waterfall');
const {MetricsSummaryPrinter} = require('./metrics-summary');
const {MockOtlpServer} = require('./mockotlpserver');

const PRINTER_NAMES = ['inspect', 'json', 'json2', 'waterfall'];
const PRINTER_NAMES = [
'inspect',
'json',
'json2',
'waterfall',
'metrics-summary',
];

// This adds a custom cli option type to dashdash, to support `-o json,waterfall`
// options for specifying multiple printers (aka output modes).
Expand Down Expand Up @@ -96,6 +103,9 @@ async function main() {
case 'waterfall':
printers.push(new TraceWaterfallPrinter(log));
break;
case 'metrics-summary':
printers.push(new MetricsSummaryPrinter(log));
break;
}
});
printers.forEach((p) => p.subscribe());
Expand Down
57 changes: 57 additions & 0 deletions packages/mockotlpserver/lib/metrics-summary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* A "Printer" of metrics data that attempts a reasonable short summary.
*
* Dev Notes / Ideas:
*/

const {Printer} = require('./printers');
const {normalizeMetrics} = require('./normalize');

class MetricsSummaryPrinter extends Printer {
printMetrics(rawMetrics) {
const metrics = normalizeMetrics(rawMetrics);

const rendering = [];
// TODO add size of request in bytes, perhaps useful for debugging
// TODO add a summary of service.names (from resource) and scopes to the title line
rendering.push(`------ metrics ------`);
const scopes = [];
for (let resourceMetric of metrics.resourceMetrics) {
for (let scopeMetric of resourceMetric.scopeMetrics || []) {
const scope = `${scopeMetric.scope.name}@${scopeMetric.scope.version}`;
scopes.push(scope);
for (let metric of scopeMetric.metrics) {
if (metric.histogram) {
// TODO do we want to attempt a short summary of histogram buckets?
// TODO handle multiple datapoints, dp per normalized attribute set. Highest prio. Run `node -r @elastic/opentelemetry-node/start.js http-server.js` for example data.
if (metric.histogram.dataPoints.length !== 1) {
this._log.warn(
{metric},
'metric has other than 1 dataPoint'
);
rendering.push(` ${metric.name} (histogram)`);
} else {
const dp = metric.histogram.dataPoints[0];
rendering.push(
` ${metric.name} (histogram, ${
metric.unit
}, ${
Object.keys(dp.attributes).length
} attrs): min=${dp.min}, max=${dp.max}`
);
}
} else {
// TODO handle other metric types better
rendering.push(` ${metric.name} (type=???)`);
}
}
}
}

console.log(rendering.join('\n'));
}
}

module.exports = {
MetricsSummaryPrinter,
};
151 changes: 118 additions & 33 deletions packages/mockotlpserver/lib/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,37 @@ const statusCodeEnumFromVal = {
[StatusCode.STATUS_CODE_ERROR]: 'STATUS_CODE_ERROR',
};

/**
* Normalize an 'attributes' value, for example in:
* [ { key: 'telemetry.sdk.version', value: { stringValue: '1.19.0' } },
* { key: 'process.pid', value: { intValue: '19667' } } ]
* to a value for converting 'attributes' to a simpler object, e.g.:
* { 'telemetry.sdk.version': '1.19.0',
* 'process.pid': 19667 }
*/
function normAttrValue(v) {
if ('stringValue' in v) {
return v.stringValue;
} else if ('arrayValue' in v) {
return v.arrayValue.values.map(normAttrValue);
} else if ('intValue' in v) {
// The OTLP/json serialization uses JS Number for these, so we'll
// do the same. TODO: Is there not a concern with a 64-bit value?
if (typeof v.intValue === 'number') {
return v.intValue;
} else if (typeof v.intValue === 'string') {
return Number(v.intValue);
} else if (typeof v.intValue === 'object' && 'low' in v.intValue) {
return new Long(
v.intValue.low,
v.intValue.high,
v.intValue.unsigned
).toString();
}
}
throw new Error(`unexpected type of attributes value: ${v}`);
}

/**
* JSON stringify an OTLP trace service request to one *possible* representation.
*
Expand Down Expand Up @@ -83,37 +114,6 @@ const statusCodeEnumFromVal = {
* @param {boolean} [opts.stripScope] - exclude 'scope' property, for brevity.
*/
function jsonStringifyTrace(trace, opts) {
/**
* Normalize an 'attributes' value, for example in:
* [ { key: 'telemetry.sdk.version', value: { stringValue: '1.19.0' } },
* { key: 'process.pid', value: { intValue: '19667' } } ]
* to a value for converting 'attributes' to a simpler object, e.g.:
* { 'telemetry.sdk.version', '1.19.0',
* 'process.pid', 19667 }
*/
const normAttrValue = (v) => {
if ('stringValue' in v) {
return v.stringValue;
} else if ('arrayValue' in v) {
return v.arrayValue.values.map(normAttrValue);
} else if ('intValue' in v) {
// The OTLP/json serialization uses JS Number for these, so we'll
// do the same. TODO: Is there not a concern with a 64-bit value?
if (typeof v.intValue === 'number') {
return v.intValue;
} else if (typeof v.intValue === 'string') {
return Number(v.intValue);
} else if (typeof v.intValue === 'object' && 'low' in v.intValue) {
return new Long(
v.intValue.low,
v.intValue.high,
v.intValue.unsigned
).toString();
}
}
throw new Error(`unexpected type of attributes value: ${v}`);
};

const replacer = (k, v) => {
let rv = v;
switch (k) {
Expand Down Expand Up @@ -195,8 +195,6 @@ function jsonStringifyTrace(trace, opts) {
* - converts `span.kind` and `span.status.code` to their enum string value
* - converts longs to string
*
* TODO probably should live elsewhere
*
* See `jsonStringifyTrace()` in for full notes.
*/
function normalizeTrace(rawTrace) {
Expand All @@ -206,7 +204,94 @@ function normalizeTrace(rawTrace) {
return JSON.parse(str);
}

/**
* JSON stringify an OTLP metrics service request to one *possible* representation.
*
* This implementation:
* - converts `startTimeUnixNano` and `timeUnixNano` longs to string
*
* Limitations:
* - We are using `json: true` for protobufjs conversion, which isn't applied
* for the other flavours.
* - TODO: convert aggregationTemporality to enum string?
*
* @param {any} metrics
* @param {object} opts
* @param {number} [opts.indent] - indent option to pass to `JSON.stringify()`.
* @param {boolean} [opts.normAttributes] - whether to convert 'attributes' to
* an object (rather than the native array of {key, value} objects).
* @param {boolean} [opts.stripResource] - exclude the 'resource' property, for brevity.
* @param {boolean} [opts.stripAttributes] - exclude 'attributes' properties, for brevity.
* @param {boolean} [opts.stripScope] - exclude 'scope' property, for brevity.
*/
function jsonStringifyMetrics(metrics, opts) {
const replacer = (k, v) => {
let rv = v;
switch (k) {
case 'resource':
if (opts.stripResource) {
rv = undefined;
}
break;
case 'attributes':
if (opts.stripAttributes) {
rv = undefined;
} else if (opts.normAttributes) {
rv = {};
for (let i = 0; i < v.length; i++) {
const attr = v[i];
rv[attr.key] = normAttrValue(attr.value);
}
}
break;
case 'scope':
if (opts.stripScope) {
rv = undefined;
}
break;
case 'startTimeUnixNano':
case 'endTimeUnixNano':
// OTLP/gRPC time fields are `Long` (https://github.com/dcodeIO/Long.js),
// converted to a plain object. Convert them to a string to
// match the other flavour.
if (typeof v === 'object' && 'low' in v) {
rv = new Long(v.low, v.high, v.unsigned).toString();
}
break;
}
return rv;
};

let norm;
if (typeof metrics.constructor.toObject === 'function') {
// Normalize `ExportMetricsServiceRequest` from OTLP/proto request
// with our custom options.
norm = metrics.constructor.toObject(metrics, {
longs: String,
json: true, // TODO not sure about using this, b/c it differs from other flavours
});
} else {
norm = metrics;
}

return JSON.stringify(norm, replacer, opts.indent || 0);
}

/**
* Normalize the given raw MetricesServiceRequest.
*
* See `jsonStringifyTrace()` in for full notes.
*/
function normalizeMetrics(rawMetrics) {
const str = jsonStringifyMetrics(rawMetrics, {
normAttributes: true,
});
return JSON.parse(str);
}

module.exports = {
jsonStringifyTrace,
normalizeTrace,
jsonStringifyMetrics,
normalizeMetrics,
};
11 changes: 7 additions & 4 deletions packages/mockotlpserver/lib/printers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const {
CH_OTLP_V1_METRICS,
CH_OTLP_V1_TRACE,
} = require('./diagch');
const {jsonStringifyTrace} = require('./normalize');
const {jsonStringifyMetrics, jsonStringifyTrace} = require('./normalize');

/**
* Abstract printer class.
Expand Down Expand Up @@ -75,7 +75,7 @@ class InspectPrinter extends Printer {
super(log);
/** @private */
this._inspectOpts = {
depth: 10,
depth: 13, // Need 13 to get full metrics data structure.
breakLength: process.stdout.columns || 120,
};
}
Expand Down Expand Up @@ -107,8 +107,11 @@ class JSONPrinter extends Printer {
console.log(str);
}
printMetrics(metrics) {
// TODO: cope with similar conversion issues as for trace above
console.log(JSON.stringify(metrics, null, this._indent));
const str = jsonStringifyMetrics(metrics, {
indent: this._indent,
normAttributes: true,
});
console.log(str);
}
printLogs(logs) {
// TODO: cope with similar conversion issues as for trace above
Expand Down
1 change: 0 additions & 1 deletion packages/mockotlpserver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
"safe-stable-stringify": "^2.4.3"
},
"devDependencies": {
"@opentelemetry/auto-instrumentations-node": "^0.40.3",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.47.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.47.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.47.0",
Expand Down
Loading

0 comments on commit df6c0ab

Please sign in to comment.