Skip to content

Collect application logs

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.

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.

  • 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:443 for 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:

Terminal window
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"
LanguageLoggersInstrumentation tree
Node.jsWinston, PinoNode.js
Pythonlogging, LoguruPython
Goslog, ZapGo
JavaLogback, Log4j2Java
.NETILogger.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.

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.

Terminal window
npm install \
@opentelemetry/api-logs \
@opentelemetry/sdk-logs \
@opentelemetry/exporter-logs-otlp-proto \
@opentelemetry/instrumentation \
@opentelemetry/instrumentation-winston \
@opentelemetry/winston-transport
const { 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()],
});

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.

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.

Terminal window
pip install opentelemetry-sdk opentelemetry-exporter-otlp
import logging
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from 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.

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.

Terminal window
go get \
go.opentelemetry.io/otel/log/global \
go.opentelemetry.io/otel/sdk/log \
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc

Set 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:

Terminal window
go get go.opentelemetry.io/contrib/bridges/otelslog
import "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.

The otlploggrpc exporter reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS from the environment, so otlploggrpc.New(ctx) needs no arguments.

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:

Terminal window
export OTEL_LOGS_EXPORTER="otlp"
java -javaagent:./opentelemetry-javaagent.jar -jar app.jar

When 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);

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.

Terminal window
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

In 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.

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.

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:443 for 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/osuite exporter 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.