Instrumentation is the act of adding observability code to an app yourself.
If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language. You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code. This will emit telemetry from your app, and any library you installed that also comes with instrumentation.
If you’re instrumenting a library, only install the OpenTelemetry API package for your language. Your library will not emit telemetry on its own. It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK. For more on instrumenting libraries, see Libraries.
For more information about the OpenTelemetry API and SDK, see the specification.
On this page you will learn how you can add traces, metrics and logs to your code manually. But, you are not limited to only use one kind of instrumentation: use automatic instrumentation to get started and then enrich your code with manual instrumentation as needed.
Also, for libraries your code depends on, you don’t have to write instrumentation code yourself, since they might come with OpenTelemetry built-in natively or you can make use of instrumentation libraries.
This page uses a modified version of the example app from Getting Started to help you learn about manual instrumentation.
You don’t have to use the example app: if you want to instrument your own app or library, follow the instructions here to adapt the process to your own code.
Create an empty NPM package.json
file in a new directory:
npm init -y
Next, install Express dependencies.
npm install typescript \
ts-node \
@types/node \
express \
@types/express
npm install express
To highlight the difference between instrumenting a library and a standalone app, split out the dice rolling into a library file, which then will be imported as a dependency by the app file.
Create the library file named dice.ts
(or dice.js
if you are not using
TypeScript) and add the following code to it:
/*dice.ts*/
function rollOnce(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
export function rollTheDice(rolls: number, min: number, max: number) {
const result: number[] = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
return result;
}
/*dice.js*/
function rollOnce(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function rollTheDice(rolls, min, max) {
const result = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
return result;
}
module.exports = { rollTheDice };
Create the app file named app.ts
(or app.js
if not using TypeScript) and
add the following code to it:
/*app.ts*/
import express, { Request, Express } from 'express';
import { rollTheDice } from './dice';
const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();
app.get('/rolldice', (req, res) => {
const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
if (isNaN(rolls)) {
res
.status(400)
.send("Request parameter 'rolls' is missing or not a number.");
return;
}
res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});
app.listen(PORT, () => {
console.log(`Listening for requests on http://localhost:${PORT}`);
});
/*app.js*/
const express = require('express');
const { rollTheDice } = require('./dice.js');
const PORT = parseInt(process.env.PORT || '8080');
const app = express();
app.get('/rolldice', (req, res) => {
const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
if (isNaN(rolls)) {
res
.status(400)
.send("Request parameter 'rolls' is missing or not a number.");
return;
}
res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});
app.listen(PORT, () => {
console.log(`Listening for requests on http://localhost:${PORT}`);
});
To ensure that it is working, run the application with the following command and open http://localhost:8080/rolldice?rolls=12 in your web browser.
$ npx ts-node app.ts
Listening for requests on http://localhost:8080
$ node app.js
Listening for requests on http://localhost:8080
Install OpenTelemetry API packages:
npm install @opentelemetry/api @opentelemetry/resources @opentelemetry/semantic-conventions
If you instrument a Node.js application install the OpenTelemetry SDK for Node.js:
npm install @opentelemetry/sdk-node
Before any other module in your application is loaded, you must initialize the SDK. If you fail to initialize the SDK or initialize it too late, no-op implementations will be provided to any library that acquires a tracer or meter from the API.
/*instrumentation.ts*/
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import {
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'yourServiceName',
[ATTR_SERVICE_VERSION]: '1.0',
}),
traceExporter: new ConsoleSpanExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
}),
});
sdk.start();
/*instrumentation.js*/
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
const {
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'dice-server',
[ATTR_SERVICE_VERSION]: '0.1.0',
}),
traceExporter: new ConsoleSpanExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
}),
});
sdk.start();
For debugging and local development purposes, the following example exports telemetry to the console. After you have finished setting up manual instrumentation, you need to configure an appropriate exporter to export the app’s telemetry data to one or more telemetry backends.
The example also sets up the mandatory SDK default attribute service.name
,
which holds the logical name of the service, and the optional (but highly
encouraged!) attribute service.version
, which holds the version of the service
API or implementation.
Alternative methods exist for setting up resource attributes. For more information, see Resources.
To verify your code, run the app by requiring the library:
npx ts-node --require ./instrumentation.ts app.ts
node --require ./instrumentation.js app.js
This basic setup has no effect on your app yet. You need to add code for traces, metrics, and/or logs.
You can register instrumentation libraries with the OpenTelemetry SDK for Node.js in order to generate telemetry data for your dependencies. For more information, see Libraries.
To enable tracing in your app, you’ll need to
have an initialized
TracerProvider
that will let
you create a Tracer
.
If a TracerProvider
is not created, the OpenTelemetry APIs for tracing will
use a no-op implementation and fail to generate data. As explained next, modify
the instrumentation.ts
(or instrumentation.js
) file to include all the SDK
initialization code in Node and the browser.
If you followed the instructions to initialize the SDK
above, you have a TracerProvider
setup for you already. You can continue with
acquiring a tracer.
First, ensure you’ve got the right packages:
npm install @opentelemetry/sdk-trace-web
Next, update instrumentation.ts
(or instrumentation.js
) to contain all the
SDK initialization code in it:
import { Resource } from '@opentelemetry/resources';
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import {
BatchSpanProcessor,
ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: 'service-name-here',
[ATTR_SERVICE_VERSION]: '0.1.0',
}),
);
const provider = new WebTracerProvider({
resource: resource,
});
const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);
provider.addSpanProcessor(processor);
provider.register();
const opentelemetry = require('@opentelemetry/api');
const { Resource } = require('@opentelemetry/resources');
const {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');
const { WebTracerProvider } = require('@opentelemetry/sdk-trace-web');
const {
ConsoleSpanExporter,
BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: 'service-name-here',
[ATTR_SERVICE_VERSION]: '0.1.0',
}),
);
const provider = new WebTracerProvider({
resource: resource,
});
const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);
provider.addSpanProcessor(processor);
provider.register();
You’ll need to bundle this file with your web application to be able to use tracing throughout the rest of your web application.
This will have no effect on your app yet: you need to create spans to have telemetry emitted by your app.
By default, the Node SDK uses the BatchSpanProcessor
, and this span processor
is also chosen in the Web SDK example. The BatchSpanProcessor
processes spans
in batches before they are exported. This is usually the right processor to use
for an application.
In contrast, the SimpleSpanProcessor
processes spans as they are created. This
means that if you create 5 spans, each will be processed and exported before the
next span is created in code. This can be helpful in scenarios where you do not
want to risk losing a batch, or if you’re experimenting with OpenTelemetry in
development. However, it also comes with potentially significant overhead,
especially if spans are being exported over a network - each time a call to
create a span is made, it would be processed and sent over a network before your
app’s execution could continue.
In most cases, stick with BatchSpanProcessor
over SimpleSpanProcessor
.
Anywhere in your application where you write manual tracing code should call
getTracer
to acquire a tracer. For example:
import opentelemetry from '@opentelemetry/api';
//...
const tracer = opentelemetry.trace.getTracer(
'instrumentation-scope-name',
'instrumentation-scope-version',
);
// You can now use a 'tracer' to do tracing!
const opentelemetry = require('@opentelemetry/api');
//...
const tracer = opentelemetry.trace.getTracer(
'instrumentation-scope-name',
'instrumentation-scope-version',
);
// You can now use a 'tracer' to do tracing!
The values of instrumentation-scope-name
and instrumentation-scope-version
should uniquely identify the
Instrumentation Scope, such as the
package, module or class name. While the name is required, the version is still
recommended despite being optional.
It’s generally recommended to call getTracer
in your app when you need it
rather than exporting the tracer
instance to the rest of your app. This helps
avoid trickier application load issues when other required dependencies are
involved.
In the case of the example app, there are two places where a tracer may be acquired with an appropriate Instrumentation Scope:
First, in the application file app.ts
(or app.js
):
/*app.ts*/
import { trace } from '@opentelemetry/api';
import express, { Express } from 'express';
import { rollTheDice } from './dice';
const tracer = trace.getTracer('dice-server', '0.1.0');
const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();
app.get('/rolldice', (req, res) => {
const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
if (isNaN(rolls)) {
res
.status(400)
.send("Request parameter 'rolls' is missing or not a number.");
return;
}
res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});
app.listen(PORT, () => {
console.log(`Listening for requests on http://localhost:${PORT}`);
});
/*app.js*/
const { trace } = require('@opentelemetry/api');
const express = require('express');
const { rollTheDice } = require('./dice.js');
const tracer = trace.getTracer('dice-server', '0.1.0');
const PORT = parseInt(process.env.PORT || '8080');
const app = express();
app.get('/rolldice', (req, res) => {
const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
if (isNaN(rolls)) {
res
.status(400)
.send("Request parameter 'rolls' is missing or not a number.");
return;
}
res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});
app.listen(PORT, () => {
console.log(`Listening for requests on http://localhost:${PORT}`);
});
And second, in the library file dice.ts
(or dice.js
):
/*dice.ts*/
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('dice-lib');
function rollOnce(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
export function rollTheDice(rolls: number, min: number, max: number) {
const result: number[] = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
return result;
}
/*dice.js*/
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('dice-lib');
function rollOnce(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function rollTheDice(rolls, min, max) {
const result = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
return result;
}
module.exports = { rollTheDice };
Now that you have tracers initialized, you can create spans.
The API of OpenTelemetry JavaScript exposes two methods that allow you to create spans:
tracer.startSpan
:
Starts a new span without setting it on context.tracer.startActiveSpan
:
Starts a new span and calls the given callback function passing it the created
span as first argument. The new span gets set in context and this context is
activated for the duration of the function call.In most cases you want to use the latter (tracer.startActiveSpan
), as it takes
care of setting the span and its context active.
The code below illustrates how to create an active span.
import { trace, Span } from '@opentelemetry/api';
/* ... */
export function rollTheDice(rolls: number, min: number, max: number) {
// Create a span. A span must be closed.
return tracer.startActiveSpan('rollTheDice', (span: Span) => {
const result: number[] = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
// Be sure to end the span!
span.end();
return result;
});
}
function rollTheDice(rolls, min, max) {
// Create a span. A span must be closed.
return tracer.startActiveSpan('rollTheDice', (span) => {
const result = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
// Be sure to end the span!
span.end();
return result;
});
}
If you followed the instructions using the example app up to
this point, you can copy the code above in your library file dice.ts
(or
dice.js
). You should now be able to see spans emitted from your app.
Start your app as follows, and then send it requests by visiting
http://localhost:8080/rolldice?rolls=12 with your browser or curl
.
ts-node --require ./instrumentation.ts app.ts
node --require ./instrumentation.js app.js
After a while, you should see the spans printed in the console by the
ConsoleSpanExporter
, something like this:
{
"traceId": "6cc927a05e7f573e63f806a2e9bb7da8",
"parentId": undefined,
"name": "rollTheDice",
"id": "117d98e8add5dc80",
"kind": 0,
"timestamp": 1688386291908349,
"duration": 501,
"attributes": {},
"status": { "code": 0 },
"events": [],
"links": []
}
Nested spans let you track work that’s
nested in nature. For example, the rollOnce()
function below represents a
nested operation. The following sample creates a nested span that tracks
rollOnce()
:
function rollOnce(i: number, min: number, max: number) {
return tracer.startActiveSpan(`rollOnce:${i}`, (span: Span) => {
const result = Math.floor(Math.random() * (max - min + 1) + min);
span.end();
return result;
});
}
export function rollTheDice(rolls: number, min: number, max: number) {
// Create a span. A span must be closed.
return tracer.startActiveSpan('rollTheDice', (parentSpan: Span) => {
const result: number[] = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(i, min, max));
}
// Be sure to end the span!
parentSpan.end();
return result;
});
}
function rollOnce(i, min, max) {
return tracer.startActiveSpan(`rollOnce:${i}`, (span) => {
const result = Math.floor(Math.random() * (max - min + 1) + min);
span.end();
return result;
});
}
function rollTheDice(rolls, min, max) {
// Create a span. A span must be closed.
return tracer.startActiveSpan('rollTheDice', (parentSpan) => {
const result = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(i, min, max));
}
// Be sure to end the span!
parentSpan.end();
return result;
});
}
This code creates a child span for each roll that has parentSpan
’s ID as
their parent ID:
{
"traceId": "ff1d39e648a3dc53ba710e1bf1b86e06",
"parentId": "9214ff209e6a8267",
"name": "rollOnce:4",
"id": "7eccf70703e2bccd",
"kind": 0,
"timestamp": 1688387049511591,
"duration": 22,
"attributes": {},
"status": { "code": 0 },
"events": [],
"links": []
}
{
"traceId": "ff1d39e648a3dc53ba710e1bf1b86e06",
"parentId": undefined,
"name": "rollTheDice",
"id": "9214ff209e6a8267",
"kind": 0,
"timestamp": 1688387049510303,
"duration": 1314,
"attributes": {},
"status": { "code": 0 },
"events": [],
"links": []
}
The previous examples showed how to create an active span. In some cases, you’ll want to create inactive spans that are siblings of one another rather than being nested.
const doWork = () => {
const span1 = tracer.startSpan('work-1');
// do some work
const span2 = tracer.startSpan('work-2');
// do some more work
const span3 = tracer.startSpan('work-3');
// do even more work
span1.end();
span2.end();
span3.end();
};
In this example, span1
, span2
, and span3
are sibling spans and none of
them are considered the currently active span. They share the same parent rather
than being nested under one another.
This arrangement can be helpful if you have units of work that are grouped together but are conceptually independent from one another.
Sometimes it’s helpful to do something with the current/active span at a particular point in program execution.
const activeSpan = opentelemetry.trace.getActiveSpan();
// do something with the active span, optionally ending it if that is appropriate for your use case.
It can also be helpful to get the span from a given context that isn’t necessarily the active span.
const ctx = getContextFromSomewhere();
const span = opentelemetry.trace.getSpan(ctx);
// do something with the acquired span, optionally ending it if that is appropriate for your use case.
Attributes let you attach key/value
pairs to a Span
so it carries more
information about the current operation that it’s tracking.
function rollOnce(i: number, min: number, max: number) {
return tracer.startActiveSpan(`rollOnce:${i}`, (span: Span) => {
const result = Math.floor(Math.random() * (max - min + 1) + min);
// Add an attribute to the span
span.setAttribute('dicelib.rolled', result.toString());
span.end();
return result;
});
}
function rollOnce(i, min, max) {
return tracer.startActiveSpan(`rollOnce:${i}`, (span) => {
const result = Math.floor(Math.random() * (max - min + 1) + min);
// Add an attribute to the span
span.setAttribute('dicelib.rolled', result.toString());
span.end();
return result;
});
}
You can also add attributes to a span as it’s created:
tracer.startActiveSpan(
'app.new-span',
{ attributes: { attribute1: 'value1' } },
(span) => {
// do some work...
span.end();
},
);
function rollTheDice(rolls: number, min: number, max: number) {
return tracer.startActiveSpan(
'rollTheDice',
{ attributes: { 'dicelib.rolls': rolls.toString() } },
(span: Span) => {
/* ... */
},
);
}
function rollTheDice(rolls, min, max) {
return tracer.startActiveSpan(
'rollTheDice',
{ attributes: { 'dicelib.rolls': rolls.toString() } },
(span) => {
/* ... */
},
);
}
There are semantic conventions for spans representing operations in well-known protocols like HTTP or database calls. Semantic conventions for these spans are defined in the specification at Trace Semantic Conventions. In the simple example of this guide the source code attributes can be used.
First add the semantic conventions as a dependency to your application:
npm install --save @opentelemetry/semantic-conventions
Add the following to the top of your application file:
import {
SEMATTRS_CODE_FUNCTION,
SEMATTRS_CODE_FILEPATH,
} from '@opentelemetry/semantic-conventions';
const {
SEMATTRS_CODE_FUNCTION,
SEMATTRS_CODE_FILEPATH,
} = require('@opentelemetry/semantic-conventions');
Finally, you can update your file to include semantic attributes:
const doWork = () => {
tracer.startActiveSpan('app.doWork', (span) => {
span.setAttribute(SEMATTRS_CODE_FUNCTION, 'doWork');
span.setAttribute(SEMATTRS_CODE_FILEPATH, __filename);
// Do some work...
span.end();
});
};
A Span Event is a human-readable
message on an Span
that represents a
discrete event with no duration that can be tracked by a single timestamp. You
can think of it like a primitive log.
span.addEvent('Doing something');
const result = doWork();
You can also create Span Events with additional Attributes:
span.addEvent('some log', {
'log.severity': 'error',
'log.message': 'Data not found',
'request.id': requestId,
});
Span
s can be created with zero or more
Link
s to other Spans that are
causally related. A common scenario is to correlate one or more traces with the
current span.
const someFunction = (spanToLinkFrom) => {
const options = {
links: [
{
context: spanToLinkFrom.spanContext(),
},
],
};
tracer.startActiveSpan('app.someFunction', options, (span) => {
// Do some work...
span.end();
});
};
A Status can be set on a
Span, typically used to specify that a
Span has not completed successfully - Error
. By default, all spans are
Unset
, which means a span completed without error. The Ok
status is reserved
for when you need to explicitly mark a span as successful rather than stick with
the default of Unset
(i.e., “without error”).
The status can be set at any time before the span is finished.
import opentelemetry, { SpanStatusCode } from '@opentelemetry/api';
// ...
tracer.startActiveSpan('app.doWork', (span) => {
for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
if (i > 10000) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: 'Error',
});
}
}
span.end();
});
const opentelemetry = require('@opentelemetry/api');
// ...
tracer.startActiveSpan('app.doWork', (span) => {
for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
if (i > 10000) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: 'Error',
});
}
}
span.end();
});
It can be a good idea to record exceptions when they happen. It’s recommended to do this in conjunction with setting span status.
import opentelemetry, { SpanStatusCode } from '@opentelemetry/api';
// ...
try {
doWork();
} catch (ex) {
span.recordException(ex);
span.setStatus({ code: SpanStatusCode.ERROR });
}
const opentelemetry = require('@opentelemetry/api');
// ...
try {
doWork();
} catch (ex) {
span.recordException(ex);
span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR });
}
sdk-trace-base
and manually propagating span contextIn some cases, you may not be able to use either the Node.js SDK nor the Web SDK. The biggest difference, aside from initialization code, is that you’ll have to manually set spans as active in the current context to be able to create nested spans.
sdk-trace-base
Initializing tracing is similar to how you’d do it with Node.js or the Web SDK.
import opentelemetry from '@opentelemetry/api';
import {
BasicTracerProvider,
BatchSpanProcessor,
ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';
const provider = new BasicTracerProvider();
// Configure span processor to send spans to the exporter
provider.addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter()));
provider.register();
// This is what we'll access in all instrumentation code
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');
const opentelemetry = require('@opentelemetry/api');
const {
BasicTracerProvider,
ConsoleSpanExporter,
BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');
const provider = new BasicTracerProvider();
// Configure span processor to send spans to the exporter
provider.addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter()));
provider.register();
// This is what we'll access in all instrumentation code
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');
Like the other examples in this document, this exports a tracer you can use throughout the app.
sdk-trace-base
To create nested spans, you need to set whatever the currently-created span is
as the active span in the current context. Don’t bother using startActiveSpan
because it won’t do this for you.
const mainWork = () => {
const parentSpan = tracer.startSpan('main');
for (let i = 0; i < 3; i += 1) {
doWork(parentSpan, i);
}
// Be sure to end the parent span!
parentSpan.end();
};
const doWork = (parent, i) => {
// To create a child span, we need to mark the current (parent) span as the active span
// in the context, then use the resulting context to create a child span.
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
parent,
);
const span = tracer.startSpan(`doWork:${i}`, undefined, ctx);
// simulate some random work.
for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
// empty
}
// Make sure to end this child span! If you don't,
// it will continue to track work beyond 'doWork'!
span.end();
};
All other APIs behave the same when you use sdk-trace-base
compared with the
Node.js or Web SDKs.
Metrics combine individual measurements into aggregates, and produce data which is constant as a function of system load. Aggregates lack details required to diagnose low level issues, but complement spans by helping to identify trends and providing application runtime telemetry.
To enable metrics in your app, you’ll need to
have an initialized
MeterProvider
that will let
you create a Meter
.
If a MeterProvider
is not created, the OpenTelemetry APIs for metrics will use
a no-op implementation and fail to generate data. As explained next, modify the
instrumentation.ts
(or instrumentation.js
) file to include all the SDK
initialization code in Node and the browser.
If you followed the instructions to initialize the SDK
above, you have a MeterProvider
setup for you already. You can continue with
acquiring a meter.
sdk-metrics
In some cases you may not be able or may not want to use the full OpenTelemetry SDK for Node.js. This is also true if you want to use OpenTelemetry JavaScript in the browser.
If so, you can initialize metrics with the @opentelemetry/sdk-metrics
package:
npm install @opentelemetry/sdk-metrics
If you have not created it for tracing already, create a separate
instrumentation.ts
(or instrumentation.js
) file that has all the SDK
initialization code in it:
import opentelemetry from '@opentelemetry/api';
import {
ConsoleMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: 'dice-server',
[ATTR_SERVICE_VERSION]: '0.1.0',
}),
);
const metricReader = new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
// Default is 60000ms (60 seconds). Set to 10 seconds for demonstrative purposes only.
exportIntervalMillis: 10000,
});
const myServiceMeterProvider = new MeterProvider({
resource: resource,
readers: [metricReader],
});
// Set this MeterProvider to be global to the app being instrumented.
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);
const opentelemetry = require('@opentelemetry/api');
const {
MeterProvider,
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: 'service-name-here',
[ATTR_SERVICE_VERSION]: '0.1.0',
}),
);
const metricReader = new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
// Default is 60000ms (60 seconds). Set to 10 seconds for demonstrative purposes only.
exportIntervalMillis: 10000,
});
const myServiceMeterProvider = new MeterProvider({
resource: resource,
readers: [metricReader],
});
// Set this MeterProvider to be global to the app being instrumented.
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);
You’ll need to --require
this file when you run your app, such as:
ts-node --require ./instrumentation.ts app.ts
node --require ./instrumentation.js app.js
Now that a MeterProvider
is configured, you can acquire a Meter
.
Anywhere in your application where you have manually instrumented code you can
call getMeter
to acquire a meter. For example:
import opentelemetry from '@opentelemetry/api';
const myMeter = opentelemetry.metrics.getMeter(
'instrumentation-scope-name',
'instrumentation-scope-version',
);
// You can now use a 'meter' to create instruments!
const opentelemetry = require('@opentelemetry/api');
const myMeter = opentelemetry.metrics.getMeter(
'instrumentation-scope-name',
'instrumentation-scope-version',
);
// You can now use a 'meter' to create instruments!
The values of instrumentation-scope-name
and instrumentation-scope-version
should uniquely identify the
Instrumentation Scope, such as the
package, module or class name. While the name is required, the version is still
recommended despite being optional.
It’s generally recommended to call getMeter
in your app when you need it
rather than exporting the meter instance to the rest of your app. This helps
avoid trickier application load issues when other required dependencies are
involved.
In the case of the example app, there are two places where a tracer may be acquired with an appropriate Instrumentation Scope:
First, in the application file app.ts
(or app.js
):
/*app.ts*/
import { metrics, trace } from '@opentelemetry/api';
import express, { Express } from 'express';
import { rollTheDice } from './dice';
const tracer = trace.getTracer('dice-server', '0.1.0');
const meter = metrics.getMeter('dice-server', '0.1.0');
const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();
app.get('/rolldice', (req, res) => {
const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
if (isNaN(rolls)) {
res
.status(400)
.send("Request parameter 'rolls' is missing or not a number.");
return;
}
res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});
app.listen(PORT, () => {
console.log(`Listening for requests on http://localhost:${PORT}`);
});
/*app.js*/
const { trace, metrics } = require('@opentelemetry/api');
const express = require('express');
const { rollTheDice } = require('./dice.js');
const tracer = trace.getTracer('dice-server', '0.1.0');
const meter = metrics.getMeter('dice-server', '0.1.0');
const PORT = parseInt(process.env.PORT || '8080');
const app = express();
app.get('/rolldice', (req, res) => {
const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
if (isNaN(rolls)) {
res
.status(400)
.send("Request parameter 'rolls' is missing or not a number.");
return;
}
res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});
app.listen(PORT, () => {
console.log(`Listening for requests on http://localhost:${PORT}`);
});
And second, in the library file dice.ts
(or dice.js
):
/*dice.ts*/
import { trace, metrics } from '@opentelemetry/api';
const tracer = trace.getTracer('dice-lib');
const meter = metrics.getMeter('dice-lib');
function rollOnce(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
export function rollTheDice(rolls: number, min: number, max: number) {
const result: number[] = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
return result;
}
/*dice.js*/
const { trace, metrics } = require('@opentelemetry/api');
const tracer = trace.getTracer('dice-lib');
const meter = metrics.getMeter('dice-lib');
function rollOnce(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function rollTheDice(rolls, min, max) {
const result = [];
for (let i = 0; i < rolls; i++) {
result.push(rollOnce(min, max));
}
return result;
}
module.exports = { rollTheDice };
Now that you have meters initialized. you can create metric instruments.
Counters can be used to measure a non-negative, increasing value.
In the case of our example app we can use this to count how often the dice has been rolled:
/*dice.ts*/
const counter = meter.createCounter('dice-lib.rolls.counter');
function rollOnce(min: number, max: number) {
counter.add(1);
return Math.floor(Math.random() * (max - min + 1) + min);
}
/*dice.js*/
const counter = meter.createCounter('dice-lib.rolls.counter');
function rollOnce(min, max) {
counter.add(1);
return Math.floor(Math.random() * (max - min + 1) + min);
}
UpDown counters can increment and decrement, allowing you to observe a cumulative value that goes up or down.
const counter = myMeter.createUpDownCounter('events.counter');
//...
counter.add(1);
//...
counter.add(-1);
Histograms are used to measure a distribution of values over time.
For example, here’s how you report a distribution of response times for an API route with Express:
import express from 'express';
const app = express();
app.get('/', (_req, _res) => {
const histogram = myMeter.createHistogram('task.duration');
const startTime = new Date().getTime();
// do some work in an API call
const endTime = new Date().getTime();
const executionTime = endTime - startTime;
// Record the duration of the task operation
histogram.record(executionTime);
});
const express = require('express');
const app = express();
app.get('/', (_req, _res) => {
const histogram = myMeter.createHistogram('task.duration');
const startTime = new Date().getTime();
// do some work in an API call
const endTime = new Date().getTime();
const executionTime = endTime - startTime;
// Record the duration of the task operation
histogram.record(executionTime);
});
Observable counters can be used to measure an additive, non-negative, monotonically increasing value.
const events = [];
const addEvent = (name) => {
events.push(name);
};
const counter = myMeter.createObservableCounter('events.counter');
counter.addCallback((result) => {
result.observe(events.length);
});
//... calls to addEvent
Observable UpDown counters can increment and decrement, allowing you to measure an additive, non-negative, non-monotonically increasing cumulative value.
const events = [];
const addEvent = (name) => {
events.push(name);
};
const removeEvent = () => {
events.pop();
};
const counter = myMeter.createObservableUpDownCounter('events.counter');
counter.addCallback((result) => {
result.observe(events.length);
});
//... calls to addEvent and removeEvent
Observable Gauges should be used to measure non-additive values.
let temperature = 32;
const gauge = myMeter.createObservableGauge('temperature.gauge');
gauge.addCallback((result) => {
result.observe(temperature);
});
//... temperature variable is modified by a sensor
When you create instruments like counters, histograms, etc. you can give them a description.
const httpServerResponseDuration = myMeter.createHistogram(
'http.server.duration',
{
description: 'A distribution of the HTTP server response times',
unit: 'milliseconds',
valueType: ValueType.INT,
},
);
In JavaScript, each configuration type means the following:
description
- a human-readable description for the instrumentunit
- The description of the unit of measure that the value is intended to
represent. For example, milliseconds
to measure duration, or bytes
to
count number of bytes.valueType
- The kind of numeric value used in measurements.It’s generally recommended to describe each instrument you create.
You can add Attributes to metrics when they are generated.
const counter = myMeter.createCounter('my.counter');
counter.add(1, { 'some.optional.attribute': 'some value' });
A Metric View provides developers with the ability to customize metrics exposed by the Metrics SDK.
To instantiate a view, one must first select a target instrument. The following are valid selectors for metrics:
instrumentType
instrumentName
meterName
meterVersion
meterSchemaUrl
Selecting by instrumentName
(of type string) has support for wildcards, so you
can select all instruments using *
or select all instruments whose name starts
with http
by using http*
.
Filter attributes on all metric types:
const limitAttributesView = new View({
// only export the attribute 'environment'
attributeKeys: ['environment'],
// apply the view to all instruments
instrumentName: '*',
});
Drop all instruments with the meter name pubsub
:
const dropView = new View({
aggregation: new DropAggregation(),
meterName: 'pubsub',
});
Define explicit bucket sizes for the Histogram named http.server.duration
:
const histogramView = new View({
aggregation: new ExplicitBucketHistogramAggregation([
0, 1, 5, 10, 15, 20, 25, 30,
]),
instrumentName: 'http.server.duration',
instrumentType: InstrumentType.HISTOGRAM,
});
Once views have been configured, attach them to the corresponding meter provider:
const meterProvider = new MeterProvider({
views: [limitAttributesView, dropView, histogramView],
});
The logs API & SDK are currently under development.
You’ll also want to configure an appropriate exporter to export your telemetry data to one or more telemetry backends.
Was this page helpful?
Thank you. Your feedback is appreciated!
Please let us know how we can improve this page. Your feedback is appreciated!