OpenTelemetry Metrics API for JavaScript
This document explains how to use the OpenTelemetry Metrics JavaScript API for manual instrumentation. To learn how to install and configure the OpenTelemetry JavaScript SDK, see Getting started with OpenTelemetry Node.js and OpenTelemetry for Browsers.
If you are not familiar with metrics terminology like timeseries or additive/synchronous/asynchronous instruments, read the introduction to OpenTelemetry Metrics first.
Getting started
To get started with metrics, you need to create a Meter
using the OpenTelemetry JavaScript API:
const { metrics } = require('@opentelemetry/api')
// Get the global meter provider
const meterProvider = metrics.getMeterProvider()
// Create a meter
const meter = meterProvider.getMeter('my-app-name', '1.0.0')
Using the meter, you can create instruments to measure performance. The simplest Counter instrument looks like this:
// Create a counter
const httpRequestsCounter = meter.createCounter('http_requests_total', {
description: 'Total number of HTTP requests',
})
// Increment the counter
httpRequestsCounter.add(1, {
method: 'GET',
status: '200',
})
Counter
Counter is a synchronous instrument that measures additive non-decreasing values, such as requests served, tasks completed, or errors occurred.
const { metrics } = require('@opentelemetry/api')
const meter = metrics.getMeterProvider().getMeter('my-app', '1.0.0')
// Create a counter for HTTP requests
const httpRequestsCounter = meter.createCounter('http_requests_total', {
description: 'Total number of HTTP requests',
unit: '1',
})
// Usage in HTTP handler
function handleRequest(method, statusCode) {
try {
// Your business logic here
processRequest()
// Count successful requests
httpRequestsCounter.add(1, {
method: method,
status: statusCode.toString(),
})
} catch (error) {
// Count failed requests
httpRequestsCounter.add(1, {
method: method,
status: '500',
})
throw error
}
}
UpDownCounter
UpDownCounter is a synchronous instrument that measures values that can go up or down, such as active connections, queue size, or memory usage.
// Create an up-down counter for active connections
const activeConnectionsCounter = meter.createUpDownCounter(
'db_connections_active',
{
description: 'Number of active database connections',
unit: '1',
},
)
// Usage in connection management
class ConnectionPool {
getConnection() {
const connection = this.createConnection()
// Increment when connection is created
activeConnectionsCounter.add(1)
return connection
}
releaseConnection(connection) {
connection.close()
// Decrement when connection is released
activeConnectionsCounter.add(-1)
}
createConnection() {
// Create database connection logic
return { close: () => {} }
}
}
Histogram
Histogram is a synchronous instrument that records a distribution of values, perfect for measuring request durations, response sizes, or query execution times.
// Create a histogram for request duration
const requestDurationHistogram = meter.createHistogram(
'http_request_duration_seconds',
{
description: 'Duration of HTTP requests in seconds',
unit: 's',
},
)
// Usage with timing
function handleRequest(method, endpoint) {
const startTime = Date.now()
try {
// Your business logic here
processRequest()
} finally {
const endTime = Date.now()
const durationSeconds = (endTime - startTime) / 1000
// Record the duration
requestDurationHistogram.record(durationSeconds, {
method: method,
endpoint: endpoint,
})
}
}
// Alternative using performance.now() for higher precision
function handleRequestPrecise(method, endpoint) {
const startTime = performance.now()
try {
processRequest()
} finally {
const endTime = performance.now()
const durationSeconds = (endTime - startTime) / 1000
requestDurationHistogram.record(durationSeconds, {
method: method,
endpoint: endpoint,
})
}
}
Gauge
Note: Synchronous Gauge instruments are not available in the current OpenTelemetry JavaScript implementation. Use Observable Gauge for gauge metrics.
For gauge-like measurements where you need to record current values, use Observable Gauge (see Asynchronous Instruments section below).
Asynchronous Instruments
ObservableCounter
ObservableCounter is an asynchronous instrument that measures additive non-decreasing values collected via a callback.
// Create an observable counter for total processed orders
const totalOrdersCounter = meter.createObservableCounter('orders_total', {
description: 'Total number of processed orders',
unit: '1',
})
// Register callback
meter.addBatchObservableCallback(
(observableResult) => {
const totalOrders = getTotalOrdersFromDatabase()
observableResult.observe(totalOrdersCounter, totalOrders)
},
[totalOrdersCounter],
)
function getTotalOrdersFromDatabase() {
// Query your database to get the current total
return orderService.getTotalOrderCount()
}
ObservableUpDownCounter
ObservableUpDownCounter is an asynchronous instrument that measures additive values that can increase or decrease.
// Create observable up-down counter for queue size
const queueSizeCounter = meter.createObservableUpDownCounter('queue_size', {
description: 'Current number of messages in queue',
unit: '1',
})
// Register callback
meter.addBatchObservableCallback(
(observableResult) => {
const queueSize = messageQueue.size()
observableResult.observe(queueSizeCounter, queueSize)
},
[queueSizeCounter],
)
// For multiple queues with attributes
const multiQueueCounter = meter.createObservableUpDownCounter(
'queue_size_by_type',
{
description: 'Queue size by queue type',
unit: '1',
},
)
meter.addBatchObservableCallback(
(observableResult) => {
Object.entries(queues).forEach(([queueType, queue]) => {
observableResult.observe(multiQueueCounter, queue.size(), {
queue_type: queueType,
})
})
},
[multiQueueCounter],
)
ObservableGauge
ObservableGauge is an asynchronous instrument that measures non-additive values that can go up and down. This is the recommended way to implement gauge-like metrics in JavaScript.
// Create an observable gauge for memory usage (Node.js)
const memoryUsageGauge = meter.createObservableGauge(
'node_memory_usage_bytes',
{
description: 'Current Node.js memory usage',
unit: 'By',
},
)
// Register callback
meter.addBatchObservableCallback(
(observableResult) => {
const memoryUsage = process.memoryUsage()
observableResult.observe(memoryUsageGauge, memoryUsage.heapUsed, {
memory_type: 'heap_used',
})
observableResult.observe(memoryUsageGauge, memoryUsage.heapTotal, {
memory_type: 'heap_total',
})
observableResult.observe(memoryUsageGauge, memoryUsage.rss, {
memory_type: 'rss',
})
observableResult.observe(memoryUsageGauge, memoryUsage.external, {
memory_type: 'external',
})
},
[memoryUsageGauge],
)
// Browser example - performance metrics
const performanceGauge = meter.createObservableGauge('browser_performance', {
description: 'Browser performance metrics',
})
meter.addBatchObservableCallback(
(observableResult) => {
if (typeof window !== 'undefined' && window.performance) {
// Memory info (Chrome only)
if (performance.memory) {
observableResult.observe(
performanceGauge,
performance.memory.usedJSHeapSize,
{
metric_type: 'used_heap_size',
},
)
observableResult.observe(
performanceGauge,
performance.memory.totalJSHeapSize,
{
metric_type: 'total_heap_size',
},
)
}
}
},
[performanceGauge],
)
Complete example
const { metrics } = require('@opentelemetry/api')
class MetricsExample {
constructor() {
this.meter = metrics.getMeterProvider().getMeter('my-app', '1.0.0')
this.setupInstruments()
}
setupInstruments() {
// Synchronous instruments
this.requestCounter = this.meter.createCounter('http_requests_total', {
description: 'Total HTTP requests',
})
this.requestDuration = this.meter.createHistogram(
'http_request_duration_seconds',
{
description: 'HTTP request duration',
unit: 's',
},
)
this.activeConnections = this.meter.createUpDownCounter(
'active_connections',
{
description: 'Number of active connections',
unit: '1',
},
)
// Asynchronous instruments
this.systemMemoryGauge = this.meter.createObservableGauge(
'system_memory_bytes',
{
description: 'System memory usage',
unit: 'By',
},
)
// Register async callback
this.meter.addBatchObservableCallback(
(observableResult) => {
this.recordSystemMemory(observableResult)
},
[this.systemMemoryGauge],
)
}
handleHttpRequest(method, path) {
const startTime = performance.now()
try {
// Process request
this.processRequest()
// Record successful request
this.recordRequest(method, path, '200', startTime)
} catch (error) {
// Record failed request
this.recordRequest(method, path, '500', startTime)
throw error
}
}
recordRequest(method, path, status, startTime) {
const attributes = {
method: method,
path: path,
status: status,
}
// Increment counter
this.requestCounter.add(1, attributes)
// Record duration
const duration = (performance.now() - startTime) / 1000
this.requestDuration.record(duration, attributes)
}
recordSystemMemory(observableResult) {
if (typeof process !== 'undefined') {
// Node.js environment
const memoryUsage = process.memoryUsage()
observableResult.observe(this.systemMemoryGauge, memoryUsage.heapUsed, {
memory_type: 'heap_used',
})
observableResult.observe(this.systemMemoryGauge, memoryUsage.rss, {
memory_type: 'rss',
})
} else if (typeof window !== 'undefined' && window.performance?.memory) {
// Browser environment (Chrome)
observableResult.observe(
this.systemMemoryGauge,
performance.memory.usedJSHeapSize,
{
memory_type: 'used_heap_size',
},
)
}
}
addConnection() {
this.activeConnections.add(1)
}
removeConnection() {
this.activeConnections.add(-1)
}
processRequest() {
// Your business logic
}
}
// Usage
const metricsExample = new MetricsExample()
// Simulate handling requests
setInterval(() => {
metricsExample.handleHttpRequest('GET', '/api/users')
}, 1000)
Environment-specific examples
Node.js specific metrics
// CPU usage tracking (simplified approach)
const cpuUsageGauge = meter.createObservableGauge('node_cpu_usage_percent', {
description: 'Node.js CPU usage percentage',
unit: '%',
})
meter.addBatchObservableCallback(
(observableResult) => {
const cpuUsage = process.cpuUsage()
// Convert to percentage (simplified calculation)
const userPercent = (cpuUsage.user / 1000000) * 100 // Convert microseconds to percentage
const systemPercent = (cpuUsage.system / 1000000) * 100
observableResult.observe(cpuUsageGauge, userPercent, { cpu_type: 'user' })
observableResult.observe(cpuUsageGauge, systemPercent, {
cpu_type: 'system',
})
},
[cpuUsageGauge],
)
Browser specific metrics
// Page load performance
const pageLoadHistogram = meter.createHistogram('page_load_duration_seconds', {
description: 'Page load duration',
unit: 's',
})
// Measure page load time
window.addEventListener('load', () => {
if (performance.timing) {
const loadTime =
performance.timing.loadEventEnd - performance.timing.navigationStart
pageLoadHistogram.record(loadTime / 1000, {
page: window.location.pathname,
})
}
})
// User interaction metrics
const userInteractionCounter = meter.createCounter('user_interactions_total', {
description: 'Total user interactions',
})
document.addEventListener('click', (event) => {
userInteractionCounter.add(1, {
element_type: event.target.tagName.toLowerCase(),
page: window.location.pathname,
})
})
Querying metrics
Once your application is instrumented and sending metrics data to Uptrace, you can leverage the collected metrics for monitoring and alerting:
Metrics analysis
- Performance monitoring: Track application performance trends over time
- Resource utilization: Monitor CPU, memory, and connection usage
- Business metrics: Track user interactions, orders, and custom business KPIs
- Error rates: Monitor error rates and success ratios
Dashboard and alerting
- Custom dashboards: Create visualizations for your specific metrics
- Real-time monitoring: Monitor metrics in real-time with automatic updates
- Alerting rules: Set up alerts based on metric thresholds and anomalies
- Historical analysis: Analyze trends and patterns over different time periods
Best practices
Metric naming
- Use descriptive, hierarchical names:
http_requests_total
,db_connections_active
- Follow naming conventions consistent with your organization
- Avoid high-cardinality attributes (don't include IDs or timestamps)
Attribute guidelines
- Use meaningful attributes for filtering and grouping
- Keep attribute cardinality reasonable to avoid performance issues
- Consider the storage and query implications of your attribute choices
Performance considerations
- Use asynchronous instruments for expensive computations
- Be mindful of callback frequency for observable instruments
- Consider sampling for high-frequency metrics in production