Insights API
Overview
Available since io.Connect Desktop 10.1
⚠️ Note that the Insights API is currently available only in io.Connect Desktop.
The @interopio/otel library provides observability capabilities for io.Connect apps using OpenTelemetry. It exposes three API modules - Metrics, Traces and Logs - accessible through the io.insights object after initializing the @interopio/desktop library.
The library publishes OpenTelemetry metrics, traces and logs, and allows client apps to define and publish custom telemetry that integrates seamlessly with the predefined platform instrumentation.
If the library or any of its modules is disabled, the API is still usable, but all methods become no-ops - no defensive code is needed to protect against null values.
The Insights API is accessible via the io.insights object.
Initialization
The recommended way to use @interopio/otel is through the @interopio/desktop library, which wraps and initializes it internally. Pass the io.Insights configuration in the constructor:
const config = {
insights: {
enabled: true,
metrics: {
enabled: true,
url: "http://localhost:4318/v1/metrics"
},
traces: {
enabled: true,
url: "http://localhost:4318/v1/traces"
},
logs: {
enabled: true,
url: "http://localhost:4318/v1/logs"
}
}
};
const io = await IODesktop(config);
// Use io.insights.metrics, io.insights.traces, io.insights.logs.When running in io.Connect Desktop, the configuration passed to the constructor will be merged with the "otel" property from system.json, so you only need to specify overrides, or omit the insights property entirely if there are none.
Each module can be individually enabled or disabled. Setting enabled to false at the top level disables the entire library. Setting it for a specific module disables only that module:
const config = {
insights: {
enabled: true,
metrics: { enabled: true },
traces: { enabled: false },
logs: { enabled: true }
}
};The Traces module supports updating its filter configuration at runtime using setFilterConfig():
const newFilters = [
{
source: "myApp.orders",
enabled: true,
level: "DEBUG"
}
];
io.insights.traces.setFilterConfig(newFilters);The top-level Settings object supports properties such as serviceName, serviceId, serviceVersion, userId and platformVersion, which are added as attributes to all published telemetry data. Additional attributes can be specified using additionalAttributes and additionalResourceAttributes.
Standalone Usage
The recommended approach is to use @interopio/otel through the @interopio/desktop library, which bundles all necessary dependencies and handles initialization automatically. However, you can also use @interopio/otel directly as a standalone package. This is useful in the following scenarios:
- You are building a platform-level component (e.g., an adapter, a service or a custom integration layer) that doesn't use the io.Connect client libraries.
- You want full control over the OpenTelemetry SDK initialization, pipeline configuration and plugin setup.
- You are instrumenting a standalone Node.js app or a backend service that doesn't connect to io.Connect Desktop.
When using the library standalone, you must manually import the plugin packages that provide the actual implementations. The @interopio/otel package itself exposes only no-op APIs by default - the implementations are registered as plugins by the following packages:
@interopio/insights-base- required in all cases; provides the core infrastructure.@interopio/insights-metrics- required if using the Metrics module.@interopio/insights-traces- required if using the Traces module.@interopio/insights-logs- required for advanced cases where you need to publish logs over OpenTelemetry (usually not needed, as logging goes through the platform).
It is enough to import these packages before you initialize the library. Whether your app is bundled or executed as is, this ensures the implementation code is present when @interopio/otel looks for it.
The standalone initialization uses the Builder class to configure and build a Container instance, which serves as the main entry point to the Metrics, Traces and Logs APIs:
// Step 1: Import plugin packages to register implementations.
import "@interopio/insights-base";
import "@interopio/insights-metrics";
import "@interopio/insights-traces";
// Step 2: Import the Builder class.
import { Builder } from "@interopio/otel";
// Step 3: Create and configure the builder.
const builder = new Builder();
builder.withSettings({
enabled: true,
serviceName: "my-service",
serviceVersion: "1.0.0",
metrics: {
enabled: true,
url: "http://localhost:4318/v1/metrics"
},
traces: {
enabled: true,
url: "http://localhost:4318/v1/traces",
filters: [
{
source: "myService",
enabled: true,
level: "INFO"
}
],
defaults: {
enabled: false
}
}
});
// Step 4 (optional): Configure individual modules using sub-builders.
builder.withMetrics();
builder.withTraces();
// Step 5: Build and start the container.
const container = builder.build();
await container.start();
// Step 6: Use the API.
container.traces.withSpan("myService.processRequest", (tracingState) => {
tracingState.addData("INFO", { requestId: "abc-123" });
});The Container object exposes the same API as the io.insights object when using the io.Connect client libraries - it has metrics, traces and logs properties, as well as start(), stop() and waitForFinalExport() lifecycle methods.
You can also use the static Traces and withSpan imports from @interopio/otel without holding a reference to the container. These singletons delegate to the initialized container if one exists, or act as no-ops otherwise:
import { Traces, withSpan } from "@interopio/otel";
// These are equivalent.
Traces.withSpan("myService.operation", (tracingState) => { /* ... */ });
withSpan("myService.operation", (tracingState) => { /* ... */ });⚠️ Note that when using the library standalone, there is no automatic merging with the
system.jsonconfiguration - you are responsible for providing the full configuration throughwithSettings(). Additionally, the predefined platform instrumentations (e.g., app startup tracing, User Journey, Click Stream) are not available, as they are driven by the io.Connect Desktop platform. Only your own custom metrics, traces and logs will be published.
Usage in Node.js Apps
The @interopio/otel library works in both browser and Node.js environments. However, the OpenTelemetry SDK ships different exporter implementations for each environment, and these cannot both be exposed by an upstream library. For this reason, Node.js apps need to provide the Node-specific exporters themselves.
When initializing the Insights API through @interopio/desktop in a Node.js app, you must import and pass the Node implementations of the OpenTelemetry exporters in the constructor configuration. You also need to provide a Node-compatible context manager for proper async trace propagation:
import IOConnectDesktop from "@interopio/desktop";
// Node implementations of OTLP exporters.
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
const io = await IOConnectDesktop({
auth: {
username: process.env.USERNAME,
password: ""
},
insights: {
metrics: {
metricExporters: (config) => [new OTLPMetricExporter(config)]
},
traces: {
useOTELContextManager: true,
contextManager: () => new AsyncLocalStorageContextManager().enable(),
spanExporters: (config) => [new OTLPTraceExporter(config)]
}
}
});
// Use the API as usual.
io.insights.traces.withSpan("myNodeApp.processTask", (tracingState) => {
tracingState.addData("INFO", { task: "data-sync" });
});The metricExporters and spanExporters properties accept factory functions that receive the resolved exporter configuration (e.g., URL, headers, compression settings) and must return an array of exporter instances. The contextManager property accepts a factory function that returns a Node-compatible context manager.
The AsyncLocalStorageContextManager from the @opentelemetry/context-async-hooks package uses Node.js AsyncLocalStorage to properly track the active span across async/await boundaries, so you don't need to manually restore the tracing state after asynchronous operations - see the Traces section for more details on async tracing considerations.
When using @interopio/otel directly in a Node.js app (without @interopio/desktop), the exporters are resolved automatically as long as the plugin packages are imported. The @interopio/insights-metrics and @interopio/insights-traces packages detect the Node.js environment and use the appropriate Node exporters. You still need to provide the context manager if you want automatic async context propagation:
import "@interopio/insights-base";
import "@interopio/insights-metrics";
import "@interopio/insights-traces";
import { Builder } from "@interopio/otel";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
const builder = new Builder();
builder.withSettings({
enabled: true,
serviceName: "my-node-service",
metrics: {
enabled: true,
url: "http://localhost:4318/v1/metrics"
},
traces: {
enabled: true,
url: "http://localhost:4318/v1/traces",
useOTELContextManager: true,
contextManager: () => new AsyncLocalStorageContextManager().enable()
}
});
builder.withMetrics();
builder.withTraces();
const container = builder.build();
await container.start();
// Create custom metrics.
const requestCounter = container.metrics.getFromSettings({
type: "custom_counter",
name: "myNodeService.requestCount"
});
await requestCounter.start();
requestCounter.add(1, { endpoint: "/api/orders" });
// Create tracing spans.
container.traces.withSpan("myNodeService.handleRequest", async (tracingState) => {
tracingState.addData("INFO", { endpoint: "/api/orders" });
const result = await processRequest();
tracingState.addData("INFO", { status: "completed", itemCount: result.length });
});There are a few important differences to keep in mind when using io.Insights in Node.js apps:
- Exporters: In a browser environment, the exporters are bundled by
@interopio/desktop. In Node.js, when using@interopio/desktop, you must provide them explicitly throughmetricExportersandspanExporters. When using@interopio/otelstandalone, the plugin packages handle this automatically. - Context Manager: Browsers use the
ZoneContextManager(or manual tracing state restoration) for async context propagation. Node.js apps should use theAsyncLocalStorageContextManagerfrom@opentelemetry/context-async-hooks, which leverages Node.jsAsyncLocalStoragefor reliable async context tracking. - Platform Instrumentations: Browser-specific instrumentations (Click Stream, User Journey, DOM event tracking, fetch/XHR auto-instrumentation) are not available in Node.js. Only the io.Connect API instrumentations (interop, contexts, intents, etc.) and your own custom instrumentations will generate telemetry data.
- Logging: In Node.js apps running within io.Connect Desktop, logging is handled by the platform through the io.Connect logging API, just as with browser apps. Standalone Node.js apps that need log publishing over OpenTelemetry must import
@interopio/insights-logsand configure the Logs module.
Metrics
The Metrics module (io.insights.metrics) provides the ability to create and publish OpenTelemetry metric data. io.Connect Desktop publishes a predefined set of platform metrics (e.g., platform_startup, app_startup, system_cpu), and apps can define their own custom metrics.
Retrieving Metrics
To retrieve a predefined platform metric by type, use the get() method:
const metric = io.insights.metrics.get("platform_startup");To retrieve or create a metric from a settings object, use the getFromSettings() method:
const settings = {
type: "custom_counter",
name: "myApp.orderCount"
};
const metric = io.insights.metrics.getFromSettings(settings);Custom Metrics
The following custom metric types are available:
"custom_counter"- monotonically increasing counter. Useadd()to increment."custom_up_down_counter"- counter that can increase or decrease. Useadd()with positive or negative values."custom_gauge"- records a current value. Userecord()."custom_histogram"- records a distribution of values. Userecord()."custom_observable_counter","custom_observable_gauge","custom_observable_up_down_counter"- callback-based variants that report values through an observe callback.
Creating and using a counter metric:
const counterSettings = {
type: "custom_counter",
name: "myApp.orderCount"
};
const metric = io.insights.metrics.getFromSettings(counterSettings);
await metric.start();
metric.add(1, { client: "ACME" });Creating and using a histogram metric:
const histogramSettings = {
type: "custom_histogram",
name: "myApp.orderDuration",
unit: "ms",
buckets: [10, 50, 100, 250, 500, 1000]
};
const metric = io.insights.metrics.getFromSettings(histogramSettings);
await metric.start();
metric.record(150, { operation: "create" });Observable Metrics
Observable metrics are callback-based variants that don't require explicit add() or record() calls. Instead, the OpenTelemetry SDK periodically invokes a provided callback to collect the current value. Use the getObservableFromSettings() method to create an observable metric:
const observableSettings = {
type: "custom_observable_gauge",
name: "myApp.activeConnections"
};
// Observe callback - invoked periodically by the SDK.
const observeCallback = (result) => {
const count = getActiveConnectionCount();
result.observe(count, { region: "eu-west" });
return Promise.resolve();
};
// Optional subscribe callback - invoked when the metric starts.
const subscribeCallback = () => {
console.log("Started observing connections");
return Promise.resolve();
};
// Optional unsubscribe callback - invoked when the metric stops.
const unsubscribeCallback = () => {
console.log("Stopped observing connections");
return Promise.resolve();
};
const metric = io.insights.metrics.getObservableFromSettings(
observableSettings,
observeCallback,
subscribeCallback,
unsubscribeCallback
);
await metric.start();The getObservableFromSettings() method accepts the following arguments:
settings- metric settings object (same as forgetFromSettings()).observeCallback- required callback invoked periodically by the SDK to collect values. Receives anObservableResultobject with anobserve(value, attributes)method.subscribeCallback- optional callback invoked when the metric is started.unsubscribeCallback- optional callback invoked when the metric is stopped.
You can also use getObservable() to retrieve or create an observable metric by name:
const observeCallback = (result) => {
result.observe(getActiveConnectionCount());
return Promise.resolve();
};
const metric = io.insights.metrics.getObservable(
"myApp.activeConnections",
observeCallback
);Start & Stop
Each metric must be started before it begins collecting data. Use start() and stop() to control the lifecycle:
const metricSettings = {
type: "custom_counter",
name: "myApp.eventCount"
};
const metric = io.insights.metrics.getFromSettings(metricSettings);
await metric.start();
// ... use metric ...
await metric.stop();The Metrics module itself can also be started and stopped:
await io.insights.metrics.start();
await io.insights.metrics.stop();Waiting for Final Export
To ensure all collected metric data is exported before shutting down, use the waitForFinalExport() method with an optional timeout in milliseconds:
await io.insights.metrics.waitForFinalExport(5000);Traces
The Traces module (io.insights.traces) provides distributed tracing for io.Connect apps. It instruments io.Connect API calls automatically and exposes API for creating custom spans in your app code. Spans are controlled by a filtering configuration that determines whether a span is created, its verbosity level, and additional behavior.
Creating Spans
To create a tracing span, use the withSpan() method. The callback receives a TracingState object for adding data and managing the span:
const spanCallback = (tracingState) => {
tracingState.addData("INFO", { orderId: "12345", client: "ACME" });
// Perform operations - any io.Connect API calls made here
// will automatically create child spans.
return result;
};
io.insights.traces.withSpan("myApp.processOrder", spanCallback);The first argument is the source string, typically in the form "<company>.<app>.<operation>". It serves as the span name and is matched against the filters configuration to determine tracing behavior.
You can provide an optional filtering context object as the second argument, which is matched against the context property of filter entries:
const filteringContext = { orderType: "limit", client: "ACME" };
const spanCallback = (tracingState) => {
tracingState.addData("INFO", { status: "processing" });
};
io.insights.traces.withSpan("myApp.processOrder", filteringContext, spanCallback);To explicitly nest a span under a specific parent (e.g., for distributed tracing across system boundaries), pass a propagation info object:
const propagationInfo = parentTracingState.getPropagationInfo();
const spanCallback = (tracingState) => {
tracingState.addData("INFO", { response: "OK" });
};
io.insights.traces.withSpan("myApp.handleResponse", {}, propagationInfo, spanCallback);The addData() method accepts a verbosity level ("OFF", "DIAGNOSTIC", "DEBUG", "INFO", "WARN", "HIGHEST") and a data object. Data is only added if the level meets the span's configured minimum level.
If the callback returns a Promise, the span ends when the Promise resolves. Exceptions thrown in the callback are recorded in the span, which is marked as errored, and then rethrown.
Filtering Context
Filtering controls whether a span is created and its behavior. Filter entries in the traces.filters configuration array are matched against the span's source string and filtering context. Multiple matching filters are merged in a first-wins manner.
const config = {
insights: {
traces: {
enabled: true,
filters: [
{
source: "myApp.orders",
context: { orderType: "limit" },
enabled: true,
level: "DEBUG"
},
{
source: "myApp",
enabled: true,
level: "INFO"
}
],
defaults: {
enabled: false
}
}
}
};A filter entry matches if the span source equals or starts with the entry's source followed by ".", and all properties in the entry's context match the span's filtering context. If no filters match, the defaults settings apply.
Tracing State
The TracingState object passed to the withSpan() callback provides control over the span:
addData(level, data)- adds attributes to the span at the given verbosity level.getPropagationInfo()- returns the propagation info for linking spans across systems.injectPropagationInfo(carrier)- sets propagation info as a hidden property on the carrier object.end()/endSpan()- manually ends the span.addEvent(name, attributes)- adds an OpenTelemetry event to the span.recordException(exception)- records an exception on the span.updateName(name)- updates the span name.enabled- whether the span is active (not a no-op).level- the resolved verbosity level.source- the source string.id/traceId- the span and trace identifiers.
The currentTracingState property on the Traces module provides access to the currently active tracing state. This is especially useful for restoring trace context after asynchronous operations:
const outerCallback = async (tracingState) => {
tracingState.addData("INFO", { step: "start" });
await someAsyncOperation();
// Restore tracing state after async operation.
io.insights.traces.currentTracingState = tracingState;
const innerCallback = (innerTracingState) => {
innerTracingState.addData("INFO", { step: "continued" });
};
io.insights.traces.withSpan("myApp.asyncOperation.continue", innerCallback);
};
await io.insights.traces.withSpan("myApp.asyncOperation", outerCallback);Sequence Spans
The withSequenceSpan() and withSequenceSpanEx() methods allow creating spans for named traces that are addressed by name rather than by trace ID. Unlike withSpan(), which nests spans in the currently active trace, sequence span methods target a specific trace by name, creating it if it doesn't exist.
Use withSequenceSpan() when you want to add spans to a trace named <source>.start:
// Creates (or finds) a trace named "myApp.orderFlow.start"
// and adds a span named "myApp.orderFlow" to it.
const initiateCallback = (tracingState) => {
tracingState.addData("INFO", { step: "initiated" });
};
await io.insights.traces.withSequenceSpan("myApp.orderFlow", initiateCallback);
// Subsequent calls add more spans to the same trace.
const filteringContext = { orderId: "123" };
const validateCallback = (tracingState) => {
tracingState.addData("INFO", { step: "validated" });
};
await io.insights.traces.withSequenceSpan("myApp.orderFlow", filteringContext, validateCallback);Use withSequenceSpanEx() when you need to specify a custom trace name:
// Creates (or finds) a trace named "myApp.checkout"
// and adds a span named "myApp.orderFlow" to it.
const paymentCallback = (tracingState) => {
tracingState.addData("INFO", { step: "payment" });
};
await io.insights.traces.withSequenceSpanEx("myApp.orderFlow", "myApp.checkout", paymentCallback);Both methods support overloads with filtering context and WithSpanOptions:
const filteringContext = { orderId: "123" };
const options = { structure: "sibling" };
const shippingCallback = (tracingState) => {
tracingState.addData("INFO", { step: "shipping" });
};
await io.insights.traces.withSequenceSpanEx(
"myApp.orderFlow",
"myApp.checkout",
filteringContext,
options,
shippingCallback
);@withSpan Decorator
The withSpan() function can also be used as a TypeScript/JavaScript decorator for class methods. When applied as a decorator, every invocation of the method is automatically wrapped in a span:
import { Traces } from "@interopio/otel";
class OrderService {
@Traces.withSpan("myApp.createOrder")
async createOrder(orderId, amount) {
// Method body is automatically wrapped in a span.
return { orderId, status: "created" };
}
}To pass method arguments as filtering context, use argMapping. It accepts an array (positional) or an object (by name):
class OrderService {
// Array mapping: first arg becomes "orderId", second becomes "amount".
@Traces.withSpan("myApp.createOrder", {
argMapping: ["orderId", "amount"]
})
async createOrder(orderId, amount) {
return { orderId, status: "created" };
}
}Use null in the array to skip an argument:
@Traces.withSpan("myApp.processOrder", {
argMapping: [null, "amount", "currency"]
})
async processOrder(internalId, amount, currency) {
// Only "amount" and "currency" are added to the filtering context.
}To map properties from this to the filtering context, use thisMapping:
class OrderService {
constructor() {
this.region = "eu-west";
}
@Traces.withSpan("myApp.createOrder", {
thisMapping: { region: "region" }
})
async createOrder(orderId) {
// The "region" property of `this` is added to the filtering context.
}
}Both argMapping and thisMapping also accept function callbacks for full control:
const argMappingCallback = (orderId, amount) => {
return { orderId: orderId, highValue: amount > 10000 };
};
const thisMappingCallback = (instance) => {
return { region: instance.region };
};
@Traces.withSpan("myApp.createOrder", {
argMapping: argMappingCallback,
thisMapping: thisMappingCallback
})You can also use withSpan() to wrap existing functions without the decorator syntax:
const tracedFn = Traces.withSpan("myApp.processOrder")(originalFn);To wrap multiple methods on an object at once, use Traces.withSpans():
const tracingMap = {
createOrder: "myApp.createOrder",
deleteOrder: { source: "myApp.deleteOrder", decoratorOptions: { argMapping: ["orderId"] } }
};
const tracedService = Traces.withSpans(tracingMap, orderService);Start & Stop
The Traces module can be started and stopped:
await io.insights.traces.start();
await io.insights.traces.stop();Waiting for Final Export
To flush all remaining trace data before shutdown:
await io.insights.traces.waitForFinalExport(5000);Click Stream Marker Spans
The Click Stream trace automatically tracks user interactions with DOM elements (by default, click events). Apps can add custom marker spans to the Click Stream trace using the clickstreamMarker callback:
const data = {
4: { action: "custom-interaction", elementId: "submit-btn" }
};
io.insights.traces.clickstreamMarker("myApp.customClick", data);The data keys correspond to the SpanVerbosity enumeration values (e.g., 4 = INFO). If the source argument is omitted, data is added to the current Click Stream span instead of creating a new one.
User Journey Marker Spans
The User Journey trace tracks which app has focus and for how long. Apps can add custom markers to represent significant milestones using the userJourneyMarker callback:
const data = {
4: { milestone: "checkout-completed", cartSize: 5 }
};
io.insights.traces.userJourneyMarker("myApp.checkoutComplete", data);To set a custom User Journey marker callback, use setUserJourneyMarker():
const markerCallback = (source, data) => {
// Custom logic for creating User Journey marker spans.
};
io.insights.traces.setUserJourneyMarker(markerCallback);Logs
The Logs module (io.insights.logs) bridges the io.Connect logging API with the OpenTelemetry log exporter, allowing platform and app logs to be published via OpenTelemetry. It doesn't provide a standalone logging API - instead, it captures log entries produced by io.Connect's existing logging system.
To enable OpenTelemetry log publishing, set logs.enabled to true in the configuration and provide the collector endpoint:
const config = {
insights: {
enabled: true,
logs: {
enabled: true,
url: "http://localhost:4318/v1/logs"
}
}
};To emit a log record directly through the OpenTelemetry logs pipeline, use the emit() method:
const logRecord = {
body: "Order processed successfully",
severityText: "INFO",
attributes: { orderId: "12345" }
};
io.insights.logs.emit(logRecord);The Logs module supports filtering configuration through the filters and defaults properties in LogsSettings. Filters can match log entries by categoryName, severity and bodyRegex, and can control whether an entry is published, which attributes are included (via allowedAttributes), and whether sensitive data is masked (via hideRegex).
The module can be started, stopped and flushed in the same way as the other modules:
await io.insights.logs.start();
await io.insights.logs.stop();
await io.insights.logs.waitForFinalExport(5000);API Reference
For a complete list of the available Insights API methods and properties, see the Insights API reference documentation.