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

# Send Node.js logs and traces to Bronto via OpenTelemetry

> Instrument Node.js and JavaScript applications with the OpenTelemetry JS SDK to send logs and traces to Bronto over OTLP via a local Collector.

This page covers instrumenting a Node.js application with the OpenTelemetry SDK to send **logs and traces** to Bronto over OTLP/HTTP via a local OTel Collector. Both signals are co-equal: logs are bridged via a Winston or Pino transport — no log statement changes needed — and traces are emitted via a tracer provider.

<Tip>
  If you don't have a Collector and want to export directly from your application to Bronto, see [Direct export to Bronto](#direct-export-to-bronto) at the bottom of this page.
</Tip>

## Prerequisites

* Node.js 14 or later
* npm or yarn
* A running OTel Collector configured to forward logs and traces to Bronto — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry)
* [OpenTelemetry JavaScript SDK documentation](https://opentelemetry.io/docs/languages/js/)

## Install dependencies

<CodeGroup>
  ```bash Winston theme={"dark"}
  npm install \
    @opentelemetry/api \
    @opentelemetry/sdk-logs \
    @opentelemetry/exporter-logs-otlp-http \
    @opentelemetry/resources \
    @opentelemetry/semantic-conventions \
    @opentelemetry/winston-transport \
    winston
  ```

  ```bash Pino theme={"dark"}
  npm install \
    @opentelemetry/api \
    @opentelemetry/sdk-logs \
    @opentelemetry/exporter-logs-otlp-http \
    @opentelemetry/resources \
    @opentelemetry/semantic-conventions \
    pino
  ```
</CodeGroup>

| Package                                  | Purpose                            |
| ---------------------------------------- | ---------------------------------- |
| `@opentelemetry/sdk-logs`                | SDK — `LoggerProvider`, processors |
| `@opentelemetry/exporter-logs-otlp-http` | OTLP/HTTP exporter                 |
| `@opentelemetry/resources`               | Resource builder                   |
| `@opentelemetry/semantic-conventions`    | Standard attribute key constants   |
| `@opentelemetry/winston-transport`       | Bridges Winston into OTel          |

## Configure the OTel logger provider

Set up a `LoggerProvider` with an OTLP exporter. This is the same regardless of which logger you use.

```javascript otel-logging.js theme={"dark"}
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_NAMESPACE } = require('@opentelemetry/semantic-conventions');

const resource = new Resource({
  [SEMRESATTRS_SERVICE_NAME]: 'my-service',
  [SEMRESATTRS_SERVICE_NAMESPACE]: 'my-team',
  'deployment.environment': 'production',
});

const exporter = new OTLPLogExporter({
  url: 'http://localhost:4318/v1/logs',
});

const loggerProvider = new LoggerProvider({ resource });
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter));

module.exports = { loggerProvider };
```

## Configure the log bridge

<CodeGroup>
  ```javascript Winston theme={"dark"}
  const winston = require('winston');
  const { OpenTelemetryTransportV3 } = require('@opentelemetry/winston-transport');

  const logger = winston.createLogger({
    level: 'info',
    transports: [
      new winston.transports.Console(),
      new OpenTelemetryTransportV3(), // forwards to the global LoggerProvider
    ],
  });

  module.exports = logger;
  ```

  ```javascript Pino (manual bridge) theme={"dark"}
  const pino = require('pino');
  const { logs, SeverityNumber } = require('@opentelemetry/api-logs');

  // Get a logger from the provider
  const otelLogger = logs.getLogger('my-service');

  // Write a Pino destination that forwards to OTel
  const otelDest = pino.destination({
    write(msg) {
      const record = JSON.parse(msg);
      otelLogger.emit({
        severityNumber: pinoLevelToSeverity(record.level),
        severityText: record.level,
        body: record.msg,
        attributes: record,
      });
    },
  });

  function pinoLevelToSeverity(level) {
    if (level >= 50) return SeverityNumber.ERROR;
    if (level >= 40) return SeverityNumber.WARN;
    if (level >= 30) return SeverityNumber.INFO;
    return SeverityNumber.DEBUG;
  }

  const logger = pino({ level: 'info' }, otelDest);
  module.exports = logger;
  ```
</CodeGroup>

## Set resource attributes

Resource attributes are attached to every log record exported from this process. Two attributes drive how Bronto organises incoming logs:

| OTel attribute      | Bronto concept | Description                                  |
| ------------------- | -------------- | -------------------------------------------- |
| `service.name`      | Dataset        | Groups logs from one service                 |
| `service.namespace` | Collection     | Groups related services or a team's services |

These are set via the `Resource` constructor in the provider setup above.

## Complete example

```javascript app.js theme={"dark"}
// Must be required before any logger usage
const { loggerProvider } = require('./otel-logging');
const winston = require('winston');
const { OpenTelemetryTransportV3 } = require('@opentelemetry/winston-transport');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new OpenTelemetryTransportV3(),
  ],
});

logger.info('Application started');
logger.warn('Low disk space', { disk_free_gb: 2.1 });
logger.error('Database connection failed', { error: 'timeout' });

// Flush on shutdown
process.on('SIGTERM', async () => {
  await loggerProvider.shutdown();
  process.exit(0);
});
```

## Verify delivery

After running your application, check both signals in Bronto:

* **Logs**: open the [Search](https://app.bronto.io/search) page and filter by the dataset name you set in `service.name` — your log records should appear within a few seconds.
* **Traces**: open the Explore Traces page and filter by the same `service.name` — your spans should appear within a few seconds.

If no logs appear, check:

* The OTel Collector is running and reachable at the configured endpoint.
* The Collector's pipeline includes a `logs` pipeline with an `otlp` receiver and the Bronto exporter — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry).
* `otel-logging.js` is required before any logger is created — the `OpenTelemetryTransportV3` picks up the global `LoggerProvider` on instantiation.
* `BatchLogRecordProcessor` exports on a background timer — for short-lived scripts, call `loggerProvider.shutdown()` before exit to flush pending records.

## Traces

<Tip>
  For Express, HTTP, gRPC, database drivers, and other popular libraries, use [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) — it instruments all supported libraries automatically at startup with no code changes. Run `node -r @opentelemetry/auto-instrumentations-node/register app.js` or require it at the top of your entry point. See [Node.js auto-instrumentation](https://opentelemetry.io/docs/zero-code/js/) for setup details.
</Tip>

### Install tracing dependencies

```bash theme={"dark"}
npm install \
  @opentelemetry/sdk-trace-node \
  @opentelemetry/exporter-trace-otlp-http
```

| Package                                   | Purpose                                                 |
| ----------------------------------------- | ------------------------------------------------------- |
| `@opentelemetry/sdk-trace-node`           | `NodeTracerProvider` with automatic context propagation |
| `@opentelemetry/exporter-trace-otlp-http` | OTLP/HTTP span exporter                                 |

### Configure the tracer provider

```javascript otel-tracing.js theme={"dark"}
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');

function configureOtelTracing(resource) {
  const exporter = new OTLPTraceExporter({
    url: 'http://localhost:4318/v1/traces',
  });

  const provider = new NodeTracerProvider({ resource });
  provider.addSpanProcessor(new BatchSpanProcessor(exporter));
  provider.register(); // sets global trace context propagation

  return provider;
}

module.exports = { configureOtelTracing };
```

Pass the same `resource` object used for logging so both signals share `service.name` and `service.namespace`.

### Creating spans

```javascript theme={"dark"}
const { trace } = require('@opentelemetry/api');

const tracer = trace.getTracer('my-service');

async function processPayment(amount, currency) {
  return tracer.startActiveSpan('process-payment', async (span) => {
    span.setAttributes({ 'payment.amount': amount, 'payment.currency': currency });
    logger.info('Processing payment');  // trace_id and span_id injected automatically
    // your code here
    span.end();
  });
}
```

Any log emitted inside an active span will automatically have `trace_id` and `span_id` attached via the `OpenTelemetryTransportV3`.

### Complete example

```javascript app.js theme={"dark"}
const { resource, loggerProvider } = require('./otel-logging');
const { configureOtelTracing } = require('./otel-tracing');
const { trace } = require('@opentelemetry/api');
const winston = require('winston');
const { OpenTelemetryTransportV3 } = require('@opentelemetry/winston-transport');

// Tracing — pass the same resource used for logging
configureOtelTracing(resource);

const logger = winston.createLogger({
  transports: [new winston.transports.Console(), new OpenTelemetryTransportV3()],
});

const tracer = trace.getTracer('my-service');

tracer.startActiveSpan('handle-request', (span) => {
  span.setAttribute('http.method', 'GET');
  logger.info('Handling request');  // trace_id + span_id attached automatically
  span.end();
});
```

## GenAI semantic conventions

If your application calls an LLM (OpenAI, Anthropic, Amazon Bedrock, etc.), OpenTelemetry defines a dedicated set of [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md) — `gen_ai.*` attributes for model, token usage, and prompt/response content.

<Note>
  This page was verified on **July 17, 2026**. GenAI libraries and semantic conventions are evolving rapidly, so package configuration and emitted attributes can change between releases. Keep your instrumentation current and verify the fields emitted by the version you deploy.
</Note>

### Auto-instrumentation

Contrib packages instrument popular provider SDKs automatically. For OpenAI:

```bash theme={"dark"}
npm install @opentelemetry/instrumentation-openai
```

Register it alongside your other instrumentations:

```javascript theme={"dark"}
const { OpenAIInstrumentation } = require('@opentelemetry/instrumentation-openai');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');

registerInstrumentations({
  instrumentations: [new OpenAIInstrumentation()],
});
```

For **Amazon Bedrock**, no extra package is needed beyond the AWS SDK instrumentation you likely already have: [`@opentelemetry/instrumentation-aws-sdk`](https://www.npmjs.com/package/@opentelemetry/instrumentation-aws-sdk) implements the GenAI semantic conventions for Bedrock Runtime calls (`Converse`, `InvokeModel`), and is bundled in `@opentelemetry/auto-instrumentations-node`.

<Tip>
  Need broader provider or framework coverage? See [OpenLLMetry](/integrations/openllmetry) for supported packages, setup, schema guidance, and content-capture controls.
</Tip>

### Experimental: capturing prompt and response content

For the contrib instrumentations, content capture is off by default and controlled by:

```bash theme={"dark"}
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
```

(The `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` opt-in used by the Python instrumentations is not read by the JS contrib packages.)

<Warning>
  With content capture enabled, prompt/response content is emitted as **log records** (OTel log events) — it flows through the logs pipeline, correlated to the span by `trace_id`, rather than landing on the span itself, so it is not queryable in Bronto's traces dataset.

  To shape it for search, emit the content as a structured **log record** with explicit attributes, using the log bridge configured above. See [LLM Observability](/ai-features/llm-observability) for the recommended attribute names, a worked logging example, and Bronto search queries.
</Warning>

### Manual spans

Where no auto-instrumentation package exists for your provider, set the `gen_ai.*` attributes yourself:

```javascript theme={"dark"}
const tracer = trace.getTracer('my-service');

tracer.startActiveSpan('chat gpt-4o-mini', (span) => {
  span.setAttributes({
    'gen_ai.provider.name': 'openai',
    'gen_ai.request.model': 'gpt-4o-mini',
    'gen_ai.usage.input_tokens': 33,
    'gen_ai.usage.output_tokens': 74,
    // An array attribute — Bronto displays it flattened as finish_reasons.0
    'gen_ai.response.finish_reasons': ['stop'],
  });
  span.end();
});
```

See [LLM Observability](/ai-features/llm-observability) for the full recommended attribute set, including `gen_ai.input.messages` / `gen_ai.output.messages`, and how to search and aggregate GenAI fields in Bronto.

## Direct export to Bronto

If you are not using an OTel Collector, export directly to Bronto by replacing the exporter configurations with the Bronto OTLP endpoints and your API key:

```javascript theme={"dark"}
const logExporter = new OTLPLogExporter({
  url: 'https://ingestion.eu.bronto.io/v1/logs', // or ingestion.us.bronto.io
  headers: { 'x-bronto-api-key': '<YOUR_API_KEY>' },
});

const traceExporter = new OTLPTraceExporter({
  url: 'https://ingestion.eu.bronto.io/v1/traces', // or ingestion.us.bronto.io
  headers: { 'x-bronto-api-key': '<YOUR_API_KEY>' },
});
```

| Region | Logs endpoint                            | Traces endpoint                            |
| ------ | ---------------------------------------- | ------------------------------------------ |
| EU     | `https://ingestion.eu.bronto.io/v1/logs` | `https://ingestion.eu.bronto.io/v1/traces` |
| US     | `https://ingestion.us.bronto.io/v1/logs` | `https://ingestion.us.bronto.io/v1/traces` |

See [API Keys](/Account-Management/API-Keys) for how to create a key with ingestion permissions. No other changes to the rest of the setup are required.
