Collect application logs
Overview
Section titled “Overview”Application logs are the richest kind of log data — they carry your own context, such as user IDs, order IDs, and feature flags. Routed through the OpenTelemetry logs SDK, each record also carries the trace_id and span_id of the active request, so every line links to the request that produced it.
How it works
Section titled “How it works”The approach is the same in every language: instrument the service for traces, then bridge your existing logger into the OpenTelemetry logs SDK. Your logger.info(...) calls do not change — a bridge (an appender, handler, transport, or core) converts each record to an OpenTelemetry log record, attaches the active trace context, and exports it over OTLP to Osuite. Tracing setup is covered in the per-language instrumentation guides; the correlation model is in Trace correlation.
Prerequisites
Section titled “Prerequisites”- A running service you can restart.
- Traces already exporting to Osuite, so log records have trace context to attach. Follow your language’s instrumentation guide first.
- Your Osuite ingest endpoint and token. The endpoint is
ingest.<region>.osuite.io:443for your region; the token is your<your-ingest-token>.
Every bridge below reads the ingest endpoint and token from the standard OpenTelemetry environment variables, so no credentials live in source:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443"export OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>"export OTEL_SERVICE_NAME="<service-name>"export OTEL_RESOURCE_ATTRIBUTES="service.environment=production"Languages
Section titled “Languages”| Language | Loggers | Instrumentation tree |
|---|---|---|
| Node.js | Winston, Pino | Node.js |
| Python | logging, Loguru | Python |
| Go | slog, Zap | Go |
| Java | Logback, Log4j2 | Java |
| .NET | ILogger | .NET |
If your language is not listed, any logger that can export over OTLP — or write structured JSON that the Collector parses — works with Osuite. See Collect log files for the file-based path.
Node.js — Winston and Pino
Section titled “Node.js — Winston and Pino”Register a global LoggerProvider with an OTLP exporter, then enable the instrumentation for your logger. The instrumentation adds a transport (Winston) or destination (Pino) that forwards every record to the logs SDK and injects trace_id, span_id, and trace_flags when a log call runs inside an active span. Without a registered LoggerProvider, the instrumentation is a no-op.
Load the module below with node --require ./logging.js app.js, before your application requires its logger.
npm install \ @opentelemetry/api-logs \ @opentelemetry/sdk-logs \ @opentelemetry/exporter-logs-otlp-proto \ @opentelemetry/instrumentation \ @opentelemetry/instrumentation-winston \ @opentelemetry/winston-transportconst { logs } = require('@opentelemetry/api-logs');const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-proto');const { registerInstrumentations } = require('@opentelemetry/instrumentation');const { WinstonInstrumentation } = require('@opentelemetry/instrumentation-winston');
const loggerProvider = new LoggerProvider({ processors: [new BatchLogRecordProcessor(new OTLPLogExporter())],});logs.setGlobalLoggerProvider(loggerProvider);
registerInstrumentations({ instrumentations: [new WinstonInstrumentation()],});npm install \ @opentelemetry/api-logs \ @opentelemetry/sdk-logs \ @opentelemetry/exporter-logs-otlp-proto \ @opentelemetry/instrumentation \ @opentelemetry/instrumentation-pinoconst { logs } = require('@opentelemetry/api-logs');const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-proto');const { registerInstrumentations } = require('@opentelemetry/instrumentation');const { PinoInstrumentation } = require('@opentelemetry/instrumentation-pino');
const loggerProvider = new LoggerProvider({ processors: [new BatchLogRecordProcessor(new OTLPLogExporter())],});logs.setGlobalLoggerProvider(loggerProvider);
registerInstrumentations({ instrumentations: [new PinoInstrumentation()],});The @opentelemetry/auto-instrumentations-node bundle used in the Node.js tracing setup already includes the Winston and Pino instrumentations — if you use it, you only need to register the LoggerProvider shown above.
Python — logging and Loguru
Section titled “Python — logging and Loguru”The SDK’s LoggingHandler converts each record into an OpenTelemetry log record and, when a span is active, attaches its trace context. Attach the handler to the standard library root logger, or add it as a Loguru sink.
pip install opentelemetry-sdk opentelemetry-exporter-otlpimport logging
from opentelemetry._logs import set_logger_providerfrom opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporterfrom opentelemetry.sdk._logs import LoggerProvider, LoggingHandlerfrom opentelemetry.sdk._logs.export import BatchLogRecordProcessor
logger_provider = LoggerProvider()set_logger_provider(logger_provider)logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)logging.getLogger().addHandler(handler)Every module that calls logging.getLogger(__name__) now exports through Osuite.
Loguru accepts any standard library handler as a sink, so the same LoggingHandler bridges it:
from loguru import logger
from opentelemetry._logs import set_logger_providerfrom opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporterfrom opentelemetry.sdk._logs import LoggerProvider, LoggingHandlerfrom opentelemetry.sdk._logs.export import BatchLogRecordProcessor
logger_provider = LoggerProvider()set_logger_provider(logger_provider)logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
logger.add(LoggingHandler(logger_provider=logger_provider), format="{message}")logger.add keeps Loguru’s existing console sink and adds Osuite alongside it.
Go — slog and Zap
Section titled “Go — slog and Zap”Configure a logs LoggerProvider once, set it as the global provider, then attach the bridge for your logger. otelslog implements slog.Handler; otelzap implements zapcore.Core. Both take a context.Context at the log call, so pass the request context to attach trace_id and span_id.
go get \ go.opentelemetry.io/otel/log/global \ go.opentelemetry.io/otel/sdk/log \ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpcSet up the provider from main:
package main
import ( "context"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/log/global" "go.opentelemetry.io/otel/sdk/log")
func setupLogs(ctx context.Context) (*log.LoggerProvider, error) { exporter, err := otlploggrpc.New(ctx) if err != nil { return nil, err } provider := log.NewLoggerProvider( log.WithProcessor(log.NewBatchProcessor(exporter)), ) global.SetLoggerProvider(provider) return provider, nil}Call setupLogs at the top of main and defer provider.Shutdown(ctx) so the batch processor flushes before exit. Then build your logger:
go get go.opentelemetry.io/contrib/bridges/otelslogimport "go.opentelemetry.io/contrib/bridges/otelslog"
logger := otelslog.NewLogger("checkout-service")logger.InfoContext(ctx, "order placed", "order.id", orderID)Use the Context methods (InfoContext, ErrorContext) so the bridge reads the active span from ctx.
go get go.opentelemetry.io/contrib/bridges/otelzapTee an OpenTelemetry core alongside your existing console core so logs still print locally:
import ( "os"
"go.opentelemetry.io/contrib/bridges/otelzap" "go.uber.org/zap" "go.uber.org/zap/zapcore")
core := zapcore.NewTee( zapcore.NewCore( zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), zapcore.InfoLevel, ), otelzap.NewCore("checkout-service"),)logger := zap.New(core)The otlploggrpc exporter reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS from the environment, so otlploggrpc.New(ctx) needs no arguments.
Java — Logback and Log4j2
Section titled “Java — Logback and Log4j2”The OpenTelemetry Java agent bridges Logback and Log4j2 automatically — no logging config changes. Add the log exporter to the agent environment and application logs flow to Osuite with trace context attached:
export OTEL_LOGS_EXPORTER="otlp"
java -javaagent:./opentelemetry-javaagent.jar -jar app.jarWhen you cannot run the agent — a GraalVM native image, or the Spring Boot starter path — add the OpenTelemetry appender to your logging config instead. The appender forwards events to the logs SDK; call OpenTelemetryAppender.install(openTelemetrySdk) once at startup so it has a provider to export through.
Add the dependency:
implementation("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:2.27.0-alpha")Declare the appender in logback.xml:
<configuration> <appender name="OpenTelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"> <captureExperimentalAttributes>true</captureExperimentalAttributes> <captureMdcAttributes>*</captureMdcAttributes> </appender>
<root level="INFO"> <appender-ref ref="OpenTelemetry"/> </root></configuration>Wire the appender to your SDK during startup:
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
OpenTelemetryAppender.install(openTelemetrySdk);Add the dependency:
implementation("io.opentelemetry.instrumentation:opentelemetry-log4j-appender-2.17:2.27.0-alpha")Declare the appender in log4j2.xml. The packages attribute must point at the appender package so Log4j2 can find it:
<Configuration packages="io.opentelemetry.instrumentation.log4j.appender.v2_17"> <Appenders> <OpenTelemetry name="OpenTelemetryAppender"/> </Appenders>
<Loggers> <Root level="INFO"> <AppenderRef ref="OpenTelemetryAppender"/> </Root> </Loggers></Configuration>Wire the appender to your SDK during startup:
import io.opentelemetry.instrumentation.log4j.appender.v2_17.OpenTelemetryAppender;
OpenTelemetryAppender.install(openTelemetrySdk);.NET — ILogger
Section titled “.NET — ILogger”ILogger is the native logging abstraction in .NET, so there is no separate bridge — add the OpenTelemetry logging provider to the host and every ILogger<T> your code injects exports over OTLP. Trace context is attached automatically when a log runs inside an active Activity.
dotnet add package OpenTelemetry.Extensions.Hostingdotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocolIn Program.cs:
using OpenTelemetry.Logs;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddOpenTelemetry(options =>{ options.IncludeFormattedMessage = true; options.IncludeScopes = true; options.AddOtlpExporter();});AddOtlpExporter() reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS from the environment. After this, inject and use ILogger as usual — no other change is needed:
public class OrdersController(ILogger<OrdersController> logger){ public void Place(int orderId) { logger.LogInformation("Order {OrderId} placed", orderId); }}For a console app without the generic host, build a LoggerFactory and call the same AddOpenTelemetry on it.
Verify in Osuite
Section titled “Verify in Osuite”What you should see
Start the service, exercise a request, and its log lines appear in the Logs Explorer within a minute. Records emitted inside a request carry a trace_id — open one and Osuite links straight to the trace that produced it.
Troubleshooting
Section titled “Troubleshooting”If logs never arrive but traces do, the logs pipeline is the likely gap. Confirm a LoggerProvider (Node, Go), LoggerProvider/LoggingHandler (Python), the appender install call (Java manual path), or the logging provider (.NET) is registered before the first log call. If logs arrive without a trace_id, the log is running outside an active span, or trace correlation is disabled on the bridge — check that tracing is set up and that the log call happens within a request. See Trace correlation for the full model.
No data in Osuite after a few minutes? Work through these checks.
- Endpoint — confirm the exporter targets exactly
ingest.<region>.osuite.io:443for your region, over TLS. - Token — confirm the ingest header carries a valid
<your-ingest-token>and has not been rotated. - Pipeline — confirm the signal you expect (traces, logs, or metrics) is wired into an active pipeline with the
otlp/osuiteexporter attached. - Export errors — check the application or Collector logs for OTLP export failures (auth, DNS, TLS, connection refused).
- Network — confirm the host has outbound access to the ingest endpoint on port
443. - Timing — allow up to a minute for the first data to appear before assuming a failure.
Still stuck? Ask the Investigation Agent or contact support.
Next steps
Section titled “Next steps”- Trace correlation — how logs link to traces
- Collect logs from any source — hosts, containers, and cloud services