> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sazabi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Porter

> Add OpenTelemetry environment variables to your Porter service and initialize the SDK to send application logs and traces directly to Sazabi.

[All Porter integration capabilities](https://docs.sazabi.com/catalogs/integrations/porter)

## About

Forward your Porter application logs and traces into Sazabi. Porter has no platform log drain, so you instrument your application with the OpenTelemetry SDK and point it at a Sazabi endpoint. Your app then sends logs and traces straight to Sazabi from your Porter deployments.

Porter's injected `PORTER_*` variables carry service and deployment metadata. Porter does not expand them inside other variable values, so read them from the start command or in code (below).

> [!NOTE]
>
> **Already running Datadog, New Relic, or Elastic APM?** Start here instead of the full SDK path:
>
> * With the Datadog Agent (for example Porter's Datadog add-on), use the [Datadog Agent](https://docs.sazabi.com/catalogs/log-sources/send-to-an-endpoint/datadog.md) source. It adds Sazabi as a second logs endpoint on the Agent, with no app changes.
> * With an in-process APM agent such as `ddtrace`, keep it for traces and use the [Python logs-only](#send-python-logs-without-auto-instrumentation) setup. Do not wrap `ddtrace-run` or another agent with `opentelemetry-instrument`: both patch the same libraries and compete for the global tracer provider.

## Prerequisites

* A service deployed on [Porter](https://dashboard.porter.run).
* Your Sazabi intake URL (shown above).

## Set up in the dashboard

**Step 1**

### Set environment variables

Add these variables in the Porter service **Variables** page. `OTEL_EXPORTER_OTLP_ENDPOINT` is your intake URL (shown above).

```bash theme={"system"}
OTEL_EXPORTER_OTLP_ENDPOINT=<your intake URL>
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
```

`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` is what sends Python `logging` lines with `opentelemetry-instrument`; without it only traces arrive. Node services and the Python logs-only setup can leave it set.

You can also put shared values in a Porter environment group. Porter passes values through as written, so do not reference `$PORTER_*` inside a value: `OTEL_SERVICE_NAME=$PORTER_APP_SERVICE_NAME` ships the literal text `$PORTER_APP_SERVICE_NAME`. Set the service name and version in the next steps instead.

**Step 2**

### Install the OpenTelemetry SDK

Install the OpenTelemetry packages for your runtime if your app does not already initialize OpenTelemetry.

Log through your runtime's logger: Python `logging`, or pino, winston, or bunyan on Node. `print` and `console.log` stay in Porter's application logs; the SDK does not forward stdout.

**Node.js**

```bash theme={"system"}
bun add @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-proto @opentelemetry/exporter-logs-otlp-proto @opentelemetry/sdk-logs
```

**Python**

Add the packages to your dependency manifest so locked builds keep working. `uv` is shown; `poetry add` or a pinned `requirements.txt` work the same. Avoid `opentelemetry-bootstrap -a install`: it installs outside the manifest and breaks `uv sync --locked`.

```bash theme={"system"}
uv add opentelemetry-distro opentelemetry-exporter-otlp-proto-http
# List the instrumentation packages for the libraries you use, then add those to the manifest too
uv run opentelemetry-bootstrap -a requirements

# Porter start command: put opentelemetry-instrument in front of the command you already use
# Run these inside the project environment: put .venv/bin on PATH in your image, or prefix them with uv run
opentelemetry-instrument uvicorn main:app --host 0.0.0.0 --port $PORT   # FastAPI
opentelemetry-instrument gunicorn app:app                               # Flask
opentelemetry-instrument gunicorn myproject.wsgi:application            # Django
opentelemetry-instrument python app.py                                  # plain script
```

**Python (logs only)**

Skip auto-instrumentation and send only `logging` records. Use this next to `ddtrace` or another APM agent, or when you only want logs.

```bash theme={"system"}
uv add opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

Then add `otel_logs.py` from [Send Python logs without auto-instrumentation](#send-python-logs-without-auto-instrumentation).

For other runtimes, initialize OpenTelemetry before the app handles requests, or run an OpenTelemetry Collector, Vector, or Fluent Bit service in Porter.

**Step 3**

### Name the service from Porter metadata

Porter sets `PORTER_APP_SERVICE_NAME`, `PORTER_IMAGE_TAG`, `PORTER_POD_REVISION`, and `PORTER_POD_NAME` at runtime. Export the OpenTelemetry variables from a shell in the Porter **start command** so the real values reach Sazabi, then `exec` your usual command:

```bash theme={"system"}
sh -c 'export OTEL_SERVICE_NAME="$PORTER_APP_SERVICE_NAME" OTEL_RESOURCE_ATTRIBUTES="service.version=$PORTER_IMAGE_TAG,porter.revision=$PORTER_POD_REVISION,porter.pod.name=$PORTER_POD_NAME"; exec opentelemetry-instrument gunicorn myproject.wsgi:application'
```

Replace the command after `exec` with your own, for example `exec node server.js`. The Python logs-only setup reads these values in code instead.

**Step 4**

### Add the Node.js instrumentation file

For Node.js, create an `instrumentation.cjs` file that wires up both the trace and log exporters. Load it from your **start command** only — for example `NODE_OPTIONS='--require ./instrumentation.cjs' node server.js`.

> [!WARNING]
>
> Do not set `NODE_OPTIONS` as a global environment variable. It applies during
> build and install, where the relative `--require` path resolves from inside
> `node_modules/` and can break dependency postinstall scripts. If your platform
> forces an env var, use an absolute path such as `--require
>             /workspace/instrumentation.cjs`.

```javascript theme={"system"}
// instrumentation.cjs
const { NodeSDK } = require("@opentelemetry/sdk-node");
const {
  getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node");
const {
  OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-proto");
const { OTLPLogExporter } = require("@opentelemetry/exporter-logs-otlp-proto");
const { BatchLogRecordProcessor } = require("@opentelemetry/sdk-logs");

try {
  const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter(),
    logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
    instrumentations: [getNodeAutoInstrumentations()],
  });
  sdk.start();
  process.on("SIGTERM", () => {
    void sdk.shutdown();
  });
} catch (err) {
  console.error("OpenTelemetry initialization failed", err);
}
```

`@opentelemetry/auto-instrumentations-node/register` configures traces only, so the log exporter requires an explicit `BatchLogRecordProcessor`. Without it, logs silently do not flow. Use the `.cjs` extension so the file loads as CommonJS even when your app's `package.json` sets `"type": "module"`.

**Step 5**

### Send Python logs without auto-instrumentation

For the Python logs-only option, add this module and call `setup_otel_logging()` once per process:

```python theme={"system"}
# otel_logs.py
import logging
import os

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


def setup_otel_logging() -> LoggerProvider:
    # Read Porter metadata here: Porter does not expand $VAR inside variable values
    resource = Resource.create(
        {
            "service.name": os.environ.get("PORTER_APP_SERVICE_NAME", "my-service"),
            "service.version": os.environ.get("PORTER_IMAGE_TAG", "unknown"),
            "porter.revision": os.environ.get("PORTER_POD_REVISION", ""),
            "porter.pod.name": os.environ.get("PORTER_POD_NAME", ""),
        }
    )
    provider = LoggerProvider(resource=resource)
    # Reads OTEL_EXPORTER_OTLP_ENDPOINT and sends to its /v1/logs path
    provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
    set_logger_provider(provider)
    # The root logger defaults to WARNING: set your app's level (for example logging.basicConfig(level=logging.INFO)) so INFO records reach the handler
    logging.getLogger().addHandler(LoggingHandler(logger_provider=provider))
    return provider
```

Under gunicorn or Celery prefork, call it in each worker after the fork, not at import time in the master process, and shut the provider down when the worker exits so the last batch is sent:

```python theme={"system"}
# gunicorn.conf.py
def post_fork(server, worker):
    from otel_logs import setup_otel_logging

    setup_otel_logging()


def worker_exit(server, worker):
    from opentelemetry._logs import get_logger_provider

    get_logger_provider().shutdown()
```

```python theme={"system"}
# Celery app module
from celery.signals import worker_process_init, worker_process_shutdown
from opentelemetry._logs import get_logger_provider

from otel_logs import setup_otel_logging


@worker_process_init.connect(weak=False)
def init_otel_logging(**kwargs):
    setup_otel_logging()


@worker_process_shutdown.connect(weak=False)
def shutdown_otel_logging(**kwargs):
    get_logger_provider().shutdown()
```

Single-process servers such as uvicorn or a plain script call `setup_otel_logging()` once at startup and `shutdown()` on exit.

**Step 6**

### Redeploy the service

Redeploy the Porter service so it picks up the new environment variables and instrumentation file.

**Step 7**

### Hand off the change

Commit these changes on a new branch, push it, and open a pull request. Keep the intake URL and any keys in environment variables or a secret store, and commit only configuration that references them.

## Set up with the CLI

You can also register the Porter log source with the [Sazabi CLI](https://docs.sazabi.com/reference/cli.md) (installed and authenticated — see [CLI reference](https://docs.sazabi.com/reference/cli/log-sources.md)).

Registering the source mints the same intake URL the dashboard shows above (the public key is embedded in its hostname):

```bash theme={"system"}
sazabi log-sources create porter --mode manual
```

The command prints the intake URL for the new source — copy the whole URL and point your sender at it using the configuration shown above. Run `sazabi log-sources get <log-source-id>` at any time to reprint the endpoint, or `sazabi log-sources list` to see every log source in the project.

## Verify

Generate a request or test log line from your Porter service. Open the [Intake page](https://docs.sazabi.com/guides/instrument/intake.md) in the Sazabi dashboard and confirm logs or traces from the **Porter** source appear. Records typically arrive within a minute or two of the request.

To check counts from your side, ask the [Sazabi CLI](https://docs.sazabi.com/reference/cli.md) for lines per service over a window:

```bash theme={"system"}
sazabi logs ask "how many log lines did each service send in the last 24 hours"
```

The answer is a per-service table with the time window it covers. Compare it with the volume your app emits.

## Troubleshooting

**No telemetry appears** — Check the Porter deployment logs for OpenTelemetry exporter errors. Confirm `OTEL_EXPORTER_OTLP_ENDPOINT` is set on the service, not only at build time.

**Traces appear but logs do not** — `auto-instrumentations-node/register` sets up traces only. Ensure your `instrumentation.cjs` also initializes the `BatchLogRecordProcessor` with `OTLPLogExporter`.

**Service name or version shows `$PORTER_...`** — Porter does not expand `$VAR` references inside variable values. Remove `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` from the Porter variables and set them from the start command or in code, as shown above.

**Some log lines are missing under load** — The SDK buffers up to `OTEL_BLRP_MAX_QUEUE_SIZE` records per process (default 2048) and exports a batch every second or every 512 records. The queue only fills when exports fall behind, for example while intake is slow or retrying. When it is full, the SDK drops records; current Python SDKs log a `Queue full, dropping` warning. Raise `OTEL_BLRP_MAX_QUEUE_SIZE` to cover your peak lines per second per process times the longest export stall you want to ride out.

**Every line arrives twice from gunicorn or Celery workers** — `setup_otel_logging()` ran in the master process and again in the worker hook, which adds a second handler. Call it only from the `post_fork` or `worker_process_init` hook.

**Metrics are not stored** — Sazabi does not store OpenTelemetry metrics sent to this endpoint. Point your metrics exporter at the OpenTelemetry Metrics source instead.

## Further reading

> [card] [Intake](https://docs.sazabi.com/guides/instrument/intake.md)
>
> See per-source ingestion volume and health in the Intake page.

> [card] [Instrument](https://docs.sazabi.com/guides/instrument.md)
>
> Learn how to send data to Sazabi.

> [card] [Log sources](https://docs.sazabi.com/catalogs/log-sources.md)
>
> Browse the full log source catalog.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.