Routing OpenTelemetry logs to Sentry using OTLP

James W. -

If you've already instrumented your app with OpenTelemetry, you don't have to rip it out to use Sentry. Two environment variables and your logs start flowing into Sentry, no SDK changes, no re-instrumentation. Here's how to set it up in a sample app, and when the native Sentry SDK might be the better call.
Why you'd use OTLP instead of the native SDK
The main advantage of OTLP is that your logging code stays decoupled from any specific observability backend. You can switch where logs go by changing a few config lines. That's useful if you:
Already have OpenTelemetry logging in place
Want to send logs to multiple backends
Need vendor-neutral instrumentation
Work with AI or LLM frameworks that use OpenTelemetry by default
Want to use the broader OpenTelemetry ecosystem
If you're starting from scratch and only need Sentry, the native Sentry SDK is probably the better call. With the native SDK, you get issue creation from logs, session replay integration, automatic breadcrumbs, and built-in error correlation. Ingesting OpenTelemetry traces and logs with Sentry via OTLP endpoints is still in beta and currently lacks these integrated features.
Guide prerequisites
Before we start, you need:
A Sentry account (the free tier works)
Node.js 18 or later installed
Basic familiarity with Express.js
If you don't have a Sentry project yet, create one now. Select Express as the platform. You can skip the DSN setup instructions because you'll use the OTLP endpoint instead.
Get your Sentry OTLP credentials
Sentry exposes separate OTLP endpoints for logs and traces. In this guide, we're focusing on the Logs endpoint. To find your OTLP credentials:
Click Settings in the left sidebar.
Under the Organization section in the Settings sidebar, click Projects.
Find your project in the list and click on it to open the project settings.
In the project settings sidebar, click Client Keys (DSN) under the SDK Setup section.
Select the OpenTelemetry tab. Click the Expand button to see all OTLP endpoint values.
Keep this tab open. We'll use the following values in the next step:
OTLP Logs Endpoint: The URL where Sentry receives logs (which looks like
https://o{ORG_ID}.ingest.us.sentry.io/api/{PROJECT_ID}/integration/otlp/v1/logs)OTLP Logs Endpoint Headers: The authentication header (which looks like
x-sentry-auth=sentry sentry_key={YOUR_PUBLIC_KEY})
One thing worth knowing: most OTLP exporters expect headers as raw key/value pairs, not full header strings. You'll need to parse the header in your app. We'll handle this in the setup below.
Connect your OpenTelemetry app to Sentry
We'll use a sample payment processing service that already has OpenTelemetry logging instrumentation. You don't need to touch the logging code itself. Just point it at Sentry's OTLP endpoint.
Clone the starter app
Run the following commands to clone the payment processing app:
git clone https://github.com/getsentry/otlp-logging-sentry.git
cd otlp-logging-sentry
npm installThis app includes the OpenTelemetry SDK already configured, structured logging throughout, multiple log severity levels (INFO, DEBUG, WARN, and ERROR), and rich log attributes for every entry.
Configure Sentry as the OTLP destination
Create a .env file in the project root:
cp .env.example .envNow edit .env and add your Sentry OTLP credentials from the previous step:
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://o{YOUR_ORG_ID}.ingest.us.sentry.io/api/{YOUR_PROJECT_ID}/integration/otlp/v1/logs
OTEL_EXPORTER_OTLP_LOGS_HEADERS=x-sentry-auth=sentry sentry_key={YOUR_PUBLIC_KEY}
OTEL_SERVICE_NAME=payment-processing-service
PORT=3000Replace the placeholders with your actual Sentry credentials. The OTEL_SERVICE_NAME will let you filter logs by service in Sentry later.
That's it. Two config lines and OpenTelemetry logs are flowing to Sentry.
Test the integration
Start the app:
npm startYou should see:
OpenTelemetry logging initialized
Service: payment-processing-service
Payment Processing Service running on http://localhost:3000Generate some logs
In a new terminal window, send a request to process a payment:
curl -X POST http://localhost:3000/process-payment \
-H "Content-Type: application/json" \
-d '{"userId": "user123", "amount": 99.99, "paymentMethod": "credit_card"}'You'll get a JSON response confirming the payment:
{
"success": true,
"transactionId": "txn_1730123456789_abc123def",
"amount": 99.99,
"currency": "USD",
"status": "completed"
}View the logs in Sentry
Now let's see what this looks like in Sentry's Logs view:
Go to your Sentry project.
Navigate to Explore in the left sidebar, then click Logs.
You'll see a list of log entries from your payment processing workflow. Each log shows a timestamp, severity indicator (colored dot), and message.
Explore log attributes
Click on any log entry to expand it and see all its attributes.
For example, the High-risk transaction detected log includes attributes like the following:
fraud_check.score:97.98fraud_check.threshold:70fraud_check.reason:unusual_amount_patternuser.id:user123transaction.id:txn_1762164637756_0hscczobmseverity:warn
All of these are searchable. To add any attribute as a filter, hover over it, click the overflow menu (three dots), and select Add to filter.
How OpenTelemetry logging works
Here's what's happening under the hood, in case you're applying these patterns to your own app.
OpenTelemetry SDK initialization
The instrument.js file configures the OTLP exporter and wires up the logger provider:
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
// Configure the OTLP log exporter
const logExporter = new OTLPLogExporter({
url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
headers: {
'x-sentry-auth': process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS?.replace('x-sentry-auth=', '') || '',
},
});
// Create logger provider
const loggerProvider = new LoggerProvider({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'payment-processing-service',
}),
});
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
// Make logger provider available globally
global.loggerProvider = loggerProvider;These are the key parts:
OTLPLogExporter sends logs to the OTLP endpoint.
LoggerProvider manages the logging system.
BatchLogRecordProcessor groups log records before export, which reduces network overhead at scale.
Emitting structured logs
The index.js file imports instrument.js first, then creates a logger and emits records:
import './instrument.js';
import { logs, SeverityNumber } from '@opentelemetry/api-logs';
const logger = logs.getLogger('payment-processing-service', '1.0.0');Here's how we emit a structured log:
function log(severity, severityNumber, message, attributes = {}) {
logger.emit({
severityNumber,
severityText: severity,
body: message,
attributes,
});
}
// Example usage
log('INFO', SeverityNumber.INFO, 'Payment request received', {
'user.id': userId,
'payment.amount': amount,
'payment.method': paymentMethod,
'transaction.id': transactionId,
});Each call to logger.emit() takes a severity level, a message body, and a set of attributes. The attributes are what make logs searchable — the more context you add here, the easier it is to find specific events later.
Log severity levels
OpenTelemetry supports six severity levels:
import { SeverityNumber } from '@opentelemetry/api-logs';
// TRACE (most detailed)
log('TRACE', SeverityNumber.TRACE, 'Function entry', {...});
// DEBUG (debugging info)
log('DEBUG', SeverityNumber.DEBUG, 'Validating payment', {...});
// INFO (informational)
log('INFO', SeverityNumber.INFO, 'Payment received', {...});
// WARN (warnings)
log('WARN', SeverityNumber.WARN, 'High-risk transaction', {...});
// ERROR (errors)
log('ERROR', SeverityNumber.ERROR, 'Payment failed', {...});
// FATAL (critical)
log('FATAL', SeverityNumber.FATAL, 'System failure', {...});Adding rich attributes
The more attributes you add, the easier it is to debug issues. Here's an example from the fraud detection path:
log('WARN', SeverityNumber.WARN, 'High-risk transaction detected', {
'user.id': userId,
'transaction.id': transactionId,
'fraud_check.score': 85.2,
'fraud_check.threshold': 70,
'fraud_check.reason': 'unusual_amount_pattern',
});All these attributes are searchable in Sentry, so you can find specific transactions quickly without scanning log text.
OTLP vs native Sentry SDK
Both approaches send logs to Sentry. The difference is in what you get automatically.
Setup and configuration
OTLP
// instrument.js
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
const logExporter = new OTLPLogExporter({
url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
headers: {
'x-sentry-auth': process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS
},
});
const loggerProvider = new LoggerProvider({...});
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));Native Sentry SDK
// instrument.js
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
enableLogs: true, // Required for structured logging
});Note that Sentry.logger requires Sentry JavaScript SDK v9.41.0 or above.
Emitting logs
OTLP
import { logs, SeverityNumber } from '@opentelemetry/api-logs';
const logger = logs.getLogger('my-service', '1.0.0');
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: 'INFO',
body: 'Payment request received',
attributes: {
'user.id': userId,
'payment.amount': amount,
},
});Native Sentry SDK
import * as Sentry from '@sentry/node';
Sentry.logger.info('Payment request received', {
'user.id': userId,
'payment.amount': amount,
});With OpenTelemetry, you specify both severityNumber and severityText manually. The Sentry SDK infers both from the method you call (info(), warn(), and so on). The SDK also associates logs with errors, transactions, and user sessions automatically, without any extra setup.
Log levels
OTLP
import { SeverityNumber } from '@opentelemetry/api-logs';
logger.emit({ severityNumber: SeverityNumber.DEBUG, ... });
logger.emit({ severityNumber: SeverityNumber.INFO, ... });
logger.emit({ severityNumber: SeverityNumber.WARN, ... });
logger.emit({ severityNumber: SeverityNumber.ERROR, ... });Native Sentry SDK
Sentry.logger.debug('message', {...});
Sentry.logger.info('message', {...});
Sentry.logger.warn('message', {...});
Sentry.logger.error('message', {...});What's next
You now have OpenTelemetry logs flowing into Sentry. A few ways to get more value from here:
Add context to your logs. The more attributes you add, the easier it is to debug issues. Add user IDs, request IDs, transaction IDs, feature flags, or any relevant business context to every log entry.
Use consistent attribute naming. Follow OpenTelemetry Semantic Conventions for standardized attribute names. This keeps your logs consistent and easier to search across services.
Set up alerts. Configure Sentry alerts to notify you when certain log patterns appear —
ERRORlogs exceeding a threshold, or high-risk transactions crossing a fraud score cutoff.Combine logs with traces. If you're also sending traces to Sentry, you can correlate them with logs to get a complete picture of your application's behavior.
OTLP logging support is still in open beta. If you run into a limitation not listed here, open an issue on GitHub. That's the fastest way to get it on our radar.
