# Observability NodeJS SDK

The NodeJS observability sdk is a npm package used to setup and implement opentelemetry instrumentation.

## Installation

pnpm

```bash
pnpm i --save @ogcio/o11y-sdk-node
```

npm

```bash
npm i @ogcio/o11y-sdk-node
```

## Usage

Setup using constructor function

```javascript
// instrumentation.ts

import("@ogcio/o11y-sdk-node/lib/index").then((sdk) =>
  sdk.instrumentNode({
    serviceName: "node-microservice",
    collectorUrl: "http://localhost:4317",
    resourceAttributes: {
      "team.infra.cluster": "dev-01",
      "team.infra.pod": "01",
      "team.service.type": "fastify",
    },
    spanAttributes: {
      "signal.namespace": "documentation",
    },
    ignoreUrls: [{ type: "equals", url: "/api/health" }],
  }),
);
```

Run your node script with instrumentation

`node --import instrumentation.js server.js`

Or setup inside your package.json

```json
{
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "start": "node --env-file=.env --import ./dist/instrumentation.js dist/index.js"
  }
}
```

# Traces

## Span Customization

It is possible to customize spans such as traces and logs globally or in a single code statement using predefined functions.

### Global Configuration

In the SDK configuration, you can set the following properties:

- `spanAttributes` Object containing static properties or functions used to evaluate custom attributes for every logs and traces.
- `resourceAttributes` Object containing static properties used as resources attributes for any signal.
- `traceRatio` Faction value from 0 to 1, used by TraceIdRatioBasedSampler which it deterministically samples a percentage of traces that you pass in as a parameter.

```typescript
function generateRandomString(): string {
  return Math.random() + "_" + Date.now();
}

instrumentNode({
  resourceAttributes: {
    "property.name.one": "value_one",
    "property.name.two": "value_two",
  },
  spanAttributes: {
    "custom.span.value": "example",
    "custom.span.value_with_function": generateRandomString,
  },
});
```

### PII Detection

By default the sdk detect and redact email PII from traces and logs

example:

```
input: user access: name.lastname@domain.com
output: user access: [REDACTED EMAIL]
```

You can disable PII detection with `detection` object inside `NodeSDKConfig`

```typescript
instrumentNode({
  detection: {
    email: false,
  },
});
```

### PII Detection

By default the sdk detect and redact email PII from traces and logs

example:

```
input: user acces: name.lastname@domain.com
output: user access: [REDACTED EMAIL]
```

You can disable PII detection with `detection` object inside `NodeSDKConfig`

```typescript
instrumentNode({
  detection: {
    email: false,
  },
});
```

## Extension Points

The SDK provides extension points for integrating with third-party observability vendors (e.g. Sentry) or adding custom processing logic.

### Span Processors

Add custom span processors that run before the default OTLP exporters:

```typescript
import { instrumentNode } from "@ogcio/o11y-sdk-node";
import { MyCustomSpanProcessor } from "./my-processor";

await instrumentNode({
  collectorUrl: "http://localhost:4317",
  serviceName: "my-service",

  // Processors that run before OTLP export (e.g., vendor processors)
  prependSpanProcessors: [new MyCustomSpanProcessor()],
});
```

### Log Processors

Similarly, add custom log record processors:

```typescript
await instrumentNode({
  collectorUrl: "http://localhost:4317",
  serviceName: "my-service",

  prependLogProcessors: [new MyLogProcessor()],
});
```

### Custom Sampler

Wrap or replace the default sampler using `samplerWrapper`:

```typescript
import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";

await instrumentNode({
  collectorUrl: "http://localhost:4317",
  serviceName: "my-service",

  // Wrap the default sampler with custom logic
  samplerWrapper: (_defaultSampler) => {
    // Discard the default sampler and return your custom sampler, or wrap it and leverage it as default
    return new MyCustomSampler();
  },
});
```

### Custom Context Manager

Override the default context manager:

```typescript
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";

await instrumentNode({
  collectorUrl: "http://localhost:4317",
  serviceName: "my-service",

  contextManager: new AsyncLocalStorageContextManager(), //(the current default)
});
```

### Additional Propagators

Add propagators to the composite propagator (W3CTraceContextPropagator is always included):

```typescript
import { B3Propagator } from "@opentelemetry/propagator-b3";

await instrumentNode({
  collectorUrl: "http://localhost:4317",
  serviceName: "my-service",

  additionalPropagators: [new B3Propagator()],
});
```

### Post-Start Callback

Execute code after the SDK has started:

```typescript
await instrumentNode({
  collectorUrl: "http://localhost:4317",
  serviceName: "my-service",

  onSdkStarted: (sdk) => {
    console.log("SDK started successfully");
    // Perform vendor-specific validation, register additional hooks, etc.
  },
});
```

## Vendor Presets

For common integrations, the SDK provides preset functions that configure all extension points automatically. See [Presets Documentation](./lib/presets/README.md) for details.

## Geo Enrichment

The SDK can attach geo attributes (`geo.country`, `geo.city`, `geo.hash`) to your
signals from CloudFront viewer headers. There are two ways to do it.

### Automatic enrichment (opt-in)

Automatic enrichment is **disabled by default**. Enable it to have the SDK add geo
attributes to every span, log, and metric and propagate them downstream via
baggage:

```ts
await instrumentNode({
  // ...other config
  geoEnrichment: { enabled: true },
});
```

When enabled, use `getGeoAttributes()` to read the geo attributes resolved for the
current request context (populated by the geo baggage propagator) and merge them
into any custom span, log, or metric:

```ts
import { getActiveSpan, getGeoAttributes } from "@ogcio/o11y-sdk-node";

getActiveSpan()?.setAttributes(getGeoAttributes());
```

### Pinpoint enrichment (auto-enrichment off)

When automatic enrichment is left disabled, enrich a **single** span, log, or
metric on demand. This is the supported pattern for adding geo attributes without
the processors or baggage propagator.

**From CloudFront headers** — `createGeoAttributesFromHeaders` reads the
cloudfront headers directly, a plain header map such as Node's
`IncomingHttpHeaders` works out of the box, and requires neither auto-enrichment
nor baggage propagation:

```ts
import { withSpan, createGeoAttributesFromHeaders } from "@ogcio/o11y-sdk-node";

app.get("/", async (req, reply) => {
  const geoAttr = createGeoAttributesFromHeaders(req.headers);

  await withSpan({
    spanName: "list users",
    spanOptions: { attributes: { ...geoAttr } },
    fn: async (span) => {
      /* ... */
    },
  });
});
```

**From explicit input** — `createGeoAttributesFrom` builds the same attributes
from values you already have. A geohash is computed automatically when both `lat`
and `lon` are provided:

```ts
import { getMetric, createGeoAttributesFrom } from "@ogcio/o11y-sdk-node";

const geoAttr = createGeoAttributesFrom({
  country: "IE",
  city: "Dublin",
  lat: 53.3498,
  lon: -6.2603,
});

getMetric("counter", { metricName: "user.insert", meterName: "app" }).add(1, {
  ...geoAttr,
});
```

## Utils:

- ## `withSpan`:

  ### Key Features
  - **Automatic Span Management**: Spans are automatically started, ended, and their status is set based on execution success or failure
    - **Exception Recording**: Errors are automatically recorded with proper span status codes
    - **Flexible Configuration**: Support for custom tracer names, span options, and attributes
    - **Promise Support**: Works seamlessly with both synchronous and asynchronous functions
    - **Nested Tracing**: Spans created within existing trace contexts automatically become child spans

  ### Usage:

  ```ts
  import { SpanOptions } from "@opentelemetry/api";

  export type WithSpanParams<T> = {
    /**
     * The name of the trace the span should belong to.
     * NOTE: If you want the new span to belong to an already existing trace, you should provide the same tracer name
     */
    traceName?: string;
    spanName: string;
    spanOptions?: SpanOptions;
    /** A function defining the task you want to be wrapped by this span */
    fn: (span: Span) => T | Promise<T>;
  };
  ```

  ### examples:

  ### **Creating a Top-Level Trace (e.g. Worker Job)**

  Use to create a top-level trace for background jobs, workers, or any standalone operations: `withSpan`

  ```ts
  import { withSpan } from "@ogcio/o11y-sdk-node";

  async function processEmailQueue() {
    return withSpan({
      traceName: "email-worker",
      spanName: "process-email-queue",
      fn: async (span) => {
        // Your worker logic here
        const emails = await fetchPendingEmails();
        span.setAttributes({
          "email.count": emails.length,
          "worker.batch.id": generateBatchId(),
        });

        for (const email of emails) {
          await sendEmail(email);
        }

        return { processed: emails.length };
      },
    });
  }
  ```

  ### **Nesting Spans Inside Existing Traces**

  Create child spans within existing trace contexts to provide detailed operation breakdown:

  ```typescript
  async function orderProcessing(orderId: string) {
    return withSpan({
      spanName: "process-order",
      fn: async (parentSpan) => {
        // Child span for validation
        const validation = await withSpan({
          spanName: "validate-order",
          fn: async (span) => {
            span.setAttribute("order.id", orderId);
            return await validateOrder(orderId);
          },
        });
        // Child span for payment
        const payment = await withSpan({
          spanName: "process-payment",
          fn: async (span) => {
            span.setAttribute("payment.amount", validation.amount);
            return await processPayment(orderId, validation.amount);
          },
        });

        parentSpan.setAttributes({
          "order.status": "completed",
          "order.total": payment.amount,
        });

        return { orderId, status: "completed" };
      },
    });
  }
  ```

- ## `getActiveSpan`:

  Using `getActiveSpan` function, you can access to current transaction span and customize it.

  ### Examples:

  ### **Edit Active Span**

  You can use the function everywhere in your code, and set some custom attributes that are enabled for that single span

  ```typescript
  import { getActiveSpan } from "@ogcio/o11y-sdk-node";

  async function routes(app: FastifyInstance) {
    app.get("/", async (req, reply) => {
      // validation and business logic

      // set span attribute
      getActiveSpan()?.setAttribute("business.info", "dummy");

      reply.status(200).send(response);
    });
  }
  ```

## Sending Custom Metrics

This package gives the possibility to send custom metrics and define them as desired in the code, you can choose between sync metrics and observable async metrics.

To use this functionality, you only need to import `getMetric` and enable the application instrumentation.

```typescript
import { getMetric } from "@ogcio/o11y-sdk-node";
```

### Sync

Sync metrics are signals sent when the function has been called.

Creating a counter, there are 2 types of counter:

- **counter** a simple counter that can only add positive numbers
- **updowncounter** counter that support also negative numbers

```typescript
const counter = getMetric("counter", {
  attributeName: "counter",
  metricName: "fastify-counter",
});

counter.add(1, {
  my: "my",
  custom: "custom",
  attributes: "attributes",
});
```

Creating a Histogram

```typescript
const histogram = getMetric("histogram", {
  metricName: "response_duration",
  attributeName: "http_response",
  options: {
    advice: {
      explicitBucketBoundaries: [0, 100, 200, 500, 1000],
    },
    description: "Response durations",
  },
});

histogram.record(120, { path: "/home" });
```

### Async

Async metrics are called by the scraper collector to read current data using the `Observable` pattern.
Creating an async metric means that the application will subscribe to the observability URL and record data on call (default 60s).

_keep in mind, you can't send signals on-demand with this component_

Creating an async Gauge

```typescript
const asyncGauge = getMetric("async-gauge", {
  metricName: "cpu_usage",
  attributeName: "server_load",
  options: { unit: "percentage" },
}).addCallback((observer) => {
  observer.observe(50, { host: "server1" });
});
```

Creating an async Counter

```typescript
getMetric("async-counter", {
  attributeName: "scraped-memory",
  metricName: "fastify-counter",
}).addCallback((observer) => {
  observer.observe(freemem(), {
    "application.os.memory": "free-memory",
  });
});
```

## API Reference

#### Protocol

protocol is a string parameter used to define how to send signals to observability infrastructure

- **grpc** Use gRPC protocol, usually default port use 4317. Is the most performant option for server side applications.
- **http** Use HTTP standard protocol, usually default port use 4318. Mainly used on web or UI client applications.
- **console** Used for debugging sending signals to observability cluster, every information will be printed to your runtime console.

#### Shared Types

```typescript
export type SDKLogLevel =
  "NONE" | "ERROR" | "WARN" | "INFO" | "DEBUG" | "VERBOSE" | "ALL";
```

### Config

| Parameter                    | Type                                   | Description                                                                                                       |
| :--------------------------- | :------------------------------------- | :---------------------------------------------------------------------------------------------------------------- |
| `collectorUrl`               | `string`                               | **Required**. The opentelemetry collector entrypoint url, if null, instrumentation will not be activated          |
| `serviceName`                | `string`                               | Name of your application used for the collector to group logs                                                     |
| `diagLogLevel`               | `SDKLogLevel`                          | Diagnostic log level for the internal runtime instrumentation                                                     |
| `collectorMode`              | `single \| batch`                      | Signals sending mode, default is batch for performance                                                            |
| `enableFS`                   | `boolean`                              | **Deprecated**. Use `autoInstrumentationConfig` instead. Flag to enable or disable the tracing for node:fs module |
| `autoInstrumentationConfig`  | `InstrumentationConfigMap`             | Configuration object for auto instrumentations. Default: `{"@opentelemetry/instrumentation-fs":{enabled:false}}`  |
| `additionalInstrumentations` | `Instrumentation[]`                    | Additional custom instrumentations to be added to the NodeSDK. Default: `[]`                                      |
| `protocol`                   | `string`                               | Type of the protocol used to send signals                                                                         |
| `prependSpanProcessors`      | `SpanProcessor[]`                      | Span processors to run before OTLP export. Default: `[]`                                                          |
| `prependLogProcessors`       | `LogRecordProcessor[]`                 | Log processors to run before OTLP export. Default: `[]`                                                           |
| `samplerWrapper`             | `(defaultSampler: Sampler) => Sampler` | Function to wrap or replace the default sampler                                                                   |
| `contextManager`             | `ContextManager`                       | Custom context manager to use                                                                                     |
| `additionalPropagators`      | `TextMapPropagator[]`                  | Additional propagators to add to the composite propagator. Default: `[]`                                          |
| `onSdkStarted`               | `(sdk: NodeSDK) => void`               | Callback invoked after the SDK has started                                                                        |
