Skip to content

Getting started: OTel SDK ​

Export logs straight from an application already instrumented with an OpenTelemetry SDK, to a Source's ingest endpoint. There is no Collector in between: the SDK sends over OTLP, with the credential as basic auth.

You need a Source and its credential first: see Getting started.

The variables ​

Every language's SDK, and the auto-instrumentation agents, read these standard variables. The header is built from the credential's two variables, so the secret is not written into the snippet. Set them for your application and restart it:

sh
export OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.usepuck.eu:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic%20$(printf '%s:%s' "$OBS_SOURCE_API_CREDENTIAL_ID" "$OBS_SOURCE_API_SECRET" | base64 | tr -d '\n')"
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=none
export OTEL_METRICS_EXPORTER=none
export OTEL_SERVICE_NAME=my-service

Puck stores logs only, so traces and metrics are turned off here. Set OTEL_SERVICE_NAME to the name of your service: it becomes the Service of every Log record.

The variable names are for a Source called api; the app shows yours.

Over gRPC ​

If your SDK only exports over gRPC, or you prefer it, use the gRPC variables. The endpoint is a URL on port 4317 and the protocol is grpc:

sh
export OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.usepuck.eu:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic%20$(printf '%s:%s' "$OBS_SOURCE_API_CREDENTIAL_ID" "$OBS_SOURCE_API_SECRET" | base64 | tr -d '\n')"
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=none
export OTEL_METRICS_EXPORTER=none
export OTEL_SERVICE_NAME=my-service

Both the Node.js and Python SDKs were tested against Puck over HTTP and over gRPC, configured only by these variables.

Examples ​

Each example sends one Log record, with the variables above already set. The SDKs read the endpoint, protocol, headers and service name from them, so the code names none of them.

Node.js ​

With @opentelemetry/sdk-node and @opentelemetry/api-logs:

js
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK();
sdk.start();

logs.getLogger("my-service").emit({
  body: "payment failed",
  severityText: "ERROR",
  severityNumber: SeverityNumber.ERROR,
  attributes: { order_id: "1042" },
});

// Flushes the buffered records; call it before the process exits.
await sdk.shutdown();

Python ​

With opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http (or -grpc, to match OTEL_EXPORTER_OTLP_PROTOCOL):

python
import logging

from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource

# Resource.create() reads OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
provider = LoggerProvider(resource=Resource.create())
provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
set_logger_provider(provider)

logger = logging.getLogger("my-service")
logger.setLevel(logging.INFO)
logger.addHandler(LoggingHandler(logger_provider=provider))
logger.error("payment failed", extra={"order_id": "1042"})

# Flushes the buffered records; call it before the process exits.
provider.shutdown()

Go ​

We have not tested Go or Java against Puck; they follow the same variables. The Go OTLP log exporter (go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp, or otlploggrpc for gRPC) reads them when it is created with no options:

go
exporter, err := otlploghttp.New(ctx)
if err != nil {
	log.Fatal(err)
}
provider := sdklog.NewLoggerProvider(
	sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),
	sdklog.WithResource(resource.Default()),
)
// Flushes the buffered records; call it before the program exits.
defer provider.Shutdown(ctx)

resource.Default() reads OTEL_SERVICE_NAME. Emit records through a logger from provider, or bridge your logging library with its OpenTelemetry bridge (otelslog for log/slog).

Java ​

The OpenTelemetry Java agent reads the same variables and exports the application's logs from the common logging libraries, with no code change:

sh
java -javaagent:opentelemetry-javaagent.jar -jar my-service.jar

What you give up without a Collector ​

A Host Collector keeps a queue on disk, so an outage of Puck or of the network loses nothing. An SDK does not. Tests with the Node.js and Python SDKs showed:

  • Records leave only when the SDK flushes. It batches them in memory. An application that exits without shutting the SDK down loses the records it had not sent yet.
  • A refused request is not stored, and the application does not notice. With a wrong secret or an unknown credential ID, Puck answers 401 over HTTP (UNAUTHENTICATED over gRPC) and stores nothing. The Node.js SDK prints the failure only at OTEL_LOG_LEVEL=debug; the Python SDK logs a warning. Neither application sees an exception.
  • Retries and buffering are the SDK's. SDKs batch, keep records in an in-memory queue that drops records when it is full, and retry a few times with backoff on temporary errors. There is no file queue. The exact behaviour is each SDK's own, so check its documentation.

For Log records that matter, such as audit trails, or for an application that starts and stops quickly, send to a Collector on the same Host instead: see Your OTel Collector or Linux host.

Resource attributes ​

The SDK's Resource arrives as sent. service.name becomes the Service, and anything in OTEL_RESOURCE_ATTRIBUTES becomes an Attribute of each Log record. The Node.js SDK also adds host.*, process.* and telemetry.sdk.* Attributes of its own.

Check that it works ​

The Sources page shows "Waiting for logs…" until the first Log record arrives, then "Receiving logs". Then search with the Filter source:api. If nothing arrives, see Troubleshooting.