Tracing the easy way using JSON.
JSON tracer that mildly emulates OpenTelemetry semantics and behavior. Built as a ligher-weight way to handle spans in technical contexts like AWS Lambda to Honeycomb.
My rationale to build and distribute this is because setting up OpenTelemetry (OTel) was harder than I would have wanted and expected. Also, while some of the semantics are nice, my feeling was that generally it was not the DX that I was hoping for. I can see a lot of developers fall through on their tracing journey going this route... MikroTrace attempts to simplify and minimize how traces are created. It is specially built for those cases in which you can use JSON logs and have your observability tool translate these into traces in that system.
So what do you get with MikroTrace?
- Tracing that just works!
- Familiar OTel-type semantics
- Works perfectly with AWS and Honeycomb
- It removes the need to pass in complete instances into the span functions, instead use plain strings to refer to spans
- Uses
process.stdout.write()
rather thanconsole.log()
so you can safely use it in Lambda - Tiny (~2.6 KB gzipped)
- Has only one dependency,
aws-metadata-utils
(for picking out metadata) - Has 100% test coverage
When you run MikroTrace.start({ ...your input })
with input, this will override any previously set configuration. When run without input, i.e. MikroTrace.start()
, you can reuse the same instance again, making it easier to use across an application without having to pass the instance around. Make sure to end any spans before doing this.
MikroTrace will set the parent context automatically but you can override this. See the below Usage
section for more on this.
MikroTrace only supports a single tracing context. Use the MikroTrace.start()
functionality to get a new clean slate if needed.
// ES5 format
const { MikroTrace } = require('mikrotrace');
// ES6 format
import { MikroTrace } from 'mikrotrace';
const tracer = MikroTrace.start({ serviceName: 'My service' });
import { MikroTrace } from 'mikrotrace';
const tracer = MikroTrace.continue();
import { MikroTrace } from 'mikrotrace';
// Add AWS event and context objects
const tracer = MikroTrace.start({ serviceName: 'My service', event, context });
/*
EXAMPLE OUTPUT BELOW
{
accountId: '123412341234',
correlationId: '',
functionMemorySize: '1024',
functionName: 'somestack-FunctionName',
functionVersion: '$LATEST',
region: 'eu-north-1',
resource: '/functionName',
runtime: 'AWS_Lambda_nodejs20.x',
stage: 'shared',
timestampRequest: '1657389598171',
user: 'some user',
viewerCountry: 'SE',
timestamp: '2022-11-14T12:28:46.842Z',
timestampEpoch: '1668428926842',
startTime: '1668428926842',
durationMs: 0,
spanName: 'My span',
spanParent: undefined,
spanParentId: '',
spanId: '3e9de004-c484-4776-8a97-a9267133194e',
traceId: '07c30259-d6c3-4269-b615-d176eff86a3f',
attributes: {},
service: 'My service',
isEnded: false
}
*/
import { MikroTrace } from 'mikrotrace';
// Add custom metadata
const metadataConfig = {
version: 1,
hostPlatform: 'aws',
owner: 'MyCompany',
domain: 'MyDomain',
system: 'MySystem',
team: 'MyTeam',
tags: ['backend', 'typescript', 'api', 'serverless', 'my-service'],
dataSensitivity: 'proprietary'
};
const tracer = MikroTrace.start({ serviceName: 'My service', metadataConfig });
const tracer = MikroTrace.start({ serviceName: 'My service' });
const span = tracer.start('My span');
const innerSpan = tracer.start('My inner span');
innerSpan.end();
span.end();
const tracer = MikroTrace.start({ serviceName: 'My service' });
tracer.start('My span');
tracer.start('My extra span');
tracer.endAll();
const tracer = MikroTrace.start({ serviceName: 'My service' });
const span = tracer.start('My span');
span.setAttribute('key', 'some value');
const tracer = MikroTrace.start({ serviceName: 'My service' });
const span = tracer.start('My span');
span.setAttributes({ something: 'my thing here', abc: 123 });
const tracer = MikroTrace.start({ serviceName: 'My service' });
const span = tracer.start('My span');
const config = span.getConfiguration();
const tracer = MikroTrace.start({ correlationId: 'abc-123', serviceName: 'My service' });
const tracer = MikroTrace.start({ serviceName: 'My service' });
tracer.setCorrelationId('abc-123');
const tracer = MikroTrace.reset();
const tracer = MikroTrace.start({ serviceName: 'My service' });
const span = tracer.start('My span');
tracer.setParentContext('Some other context');
span.end();
Enrich the tracer with correlation ID, service name, or parent context without creating a new instance
const tracer = MikroTrace.enrich({
serviceName: 'My new service',
correlationId: 'qwerty-137',
parentContext: 'some-other-parent'
});
const tracer = MikroTrace.start({ serviceName: 'My service' });
const tracerConfig = tracer.getConfiguration();
const tracer = MikroTrace.start({ serviceName: 'My service' });
const span = tracer.start('My span');
const header = tracer.getTraceHeader(span.getConfiguration());
span.end();
You can set the sampling rate either manually or using an environment variable.
A "sampled" trace means it is a span that gets stored in memory and written. An "unsampled" trace is therefore one that is not written.
The sample rate uses the 0-100
scale. The default value is 100
, meaning you get all traces if you don't set this to something else.
You may use integers or floating point numbers.
Set MIKROTRACE_SAMPLE_RATE
to a numeric or numerically-convertible value and it will be set when initializing MikroTrace.
You can also call MikroTrace manually like so:
const logger = MikroTrace.start();
logger.setSamplingRate(0.5); // 0.5% of all traces will now be sampled.
logger.setSamplingRate(25); // 25% of all traces will now be sampled.
You can check if the last trace was sampled.
The true value of this will only exist after having used the start()
method, as it gets recalculated every time that the method is run.
tracer.isTraceSampled();
If you want to "persist" the decision you can handle this manually after the first DEBUG
log call:
// If we get 'TRUE' here we can crank the sampling rate all the way up, else turn it off completely
tracer.isTraceSampled(); ? tracer.setSamplingRate(100) : tracer.setSamplingRate(0);
This is useful if you want to do more complex, cross-boundary debug logging on a call chain. In MikroTrace, the easiest way to pass this decision is simply by using the getTraceHeader()
method—as it will produce the content of a W3C traceheader
that will store the sampling decision in the last part of the string—and sending this in your call to the next service.
// Import
import { MikroTrace } from './src/entities/MikroTrace';
/**
* Ideally have a configuration object or file that contains all
* needed span names to make it easier to reference them.
*/
const spanNames = {
outerSpan: 'I am the outer span',
innerSpan: 'I am inside of the outer span',
nestedInnerSpan: 'I am hidden deep inside'
};
/**
* The tracer instance that you will reuse in your application.
* The correlation ID is not required but is _highly_ recommended.
*/
const tracer = MikroTrace.start({ serviceName: 'MyService', correlationId: 'your-correlation-id' });
/**
* You may also set the correlation ID after instantiation:
* @example tracer.setCorrelationId('your-correlation-id');
*/
/**
* Each time you call `tracer.start()` a new span is created.
* All it takes is a unique span name.
*/
const span = tracer.start(spanNames['outerSpan']);
/**
* Here we are creating an inner span. No magic: The tracer will
* assume that the first trace is the parent.
*/
const innerSpan = tracer.start(spanNames['innerSpan']);
/**
* If this is not the case and you need to assign another parent span,
* they you can do it by adding the parent span in the second argument:
*/
const innermostSpan = tracer.start(spanNames['nestedInnerSpan'], spanNames['innerSpan']);
/**
* We call `span.end()` to close the span(s) and print the log(s) for each trace.
*/
innermostSpan.end();
innerSpan.end();
span.end();
/*
EXAMPLE OUTPUT BELOW
const outputInnerSpan = {
attributes: {},
correlationId: 'your-correlation-id',
durationMs: 0,
isEnded: true,
service: 'MyService',
spanId: 'd3a06fab-8f81-45c9-bcd8-e458e62a3ef9',
spanName: 'Call the User service and fetch a response',
spanParentId: '5dec9b5a-acda-4cdf-924f-f8cf6df236c2',
timestamp: '2022-06-26T16:11:41.977Z',
timestampEpoch: '1656252701000',
traceId: 'db62951b-d9a5-4fb6-adf0-10c360e6535f'
};
const outputOuterSpan = {
attributes: {},
correlationId: 'your-correlation-id',
durationMs: 1,
isEnded: true,
service: 'MyService',
spanId: '5dec9b5a-acda-4cdf-924f-f8cf6df236c2',
spanName: 'Greet a user',
timestamp: '2022-06-26T16:11:41.977Z',
timestampEpoch: '1656252701000',
traceId: 'db62951b-d9a5-4fb6-adf0-10c360e6535f'
};
*/
- The handling of child spans and actual references ("IDs") isn't very smart right now. Expect that cases where you use a single parent span will work just fine, but that nested parents probably work perfectly just yet.