Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ ARCHIVE_S3_STORAGE_CLASS=STANDARD_IA
AWS_REGION=us-east-1
# AWS_ENDPOINT_URL=http://localhost:4566 # LocalStack for local dev

# ─── OpenTelemetry tracing ────────────────────────────────────────────────────
# Tracing is OFF by default. Uncomment TRACING_ENABLED to turn it on.
# Setting OTLP_ENDPOINT alone also enables tracing (for existing deployments).
# TRACING_ENABLED=true
# OTLP_ENDPOINT=http://localhost:4318
# SERVICE_NAME=octraban

# ─── On-chain registry contract IDs (issue #10) ───────────────────────────────
# The Octraban on-chain registry/explorer contract deployed per network.
# Leave blank to disable on-chain registry reads for that network (safe default).
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,31 @@ Because the server caches keys, a typical rotation involves:
| `INDEXER_BATCH_SIZE` | `100` | Ledgers per batch |
| `ADMIN_SECRET` | — | Bearer token required by every `/api/admin/*` route (indexer). See [`docs/ADMIN_AUTH.md`](./docs/ADMIN_AUTH.md). |

## OpenTelemetry Tracing

Tracing is **off by default** and never starts unless you explicitly enable it.
A missing or unreachable collector is handled gracefully — it logs a warning and
the service continues without tracing.

| Variable | Default | Description |
| ----------------- | -------------------------- | --------------------------------------------------------------------------------------------- |
| `TRACING_ENABLED` | _(unset — tracing is off)_ | Set to `true` or `1` to start the OpenTelemetry SDK |
| `OTLP_ENDPOINT` | `http://localhost:4318` | OTLP collector base URL. Setting this variable alone also enables tracing. |
| `SERVICE_NAME` | `octraban` | Service name reported in traces |

### Quick start (local collector)

```bash
# Start a local Jaeger all-in-one (OTLP HTTP on :4318)
docker run -d -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest

# Enable tracing in your .env
TRACING_ENABLED=true
OTLP_ENDPOINT=http://localhost:4318
```

Spans are flushed cleanly on `SIGTERM` and `SIGINT`.

## Mainnet Config

```env
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"repair": "ts-node src/indexer/repair-run.ts",
"archive": "ts-node src/archival/run.ts",
"seed": "ts-node prisma/seed.ts",
"test": "DATABASE_URL=postgresql://test:test@localhost:5432/test TESTNET_DATABASE_URL=postgresql://test:test@localhost:5432/test STELLAR_NETWORK=testnet vitest run tests/reentrancy-fortress.test.ts tests/api/nlq.test.ts tests/arbitrage-engine.test.ts tests/indexer/token-metadata.test.ts tests/error-handling-integration.test.ts tests/build-queue.test.ts tests/indexer/decoder-parity.test.ts",
"test": "DATABASE_URL=postgresql://test:test@localhost:5432/test TESTNET_DATABASE_URL=postgresql://test:test@localhost:5432/test STELLAR_NETWORK=testnet vitest run tests/reentrancy-fortress.test.ts tests/api/nlq.test.ts tests/arbitrage-engine.test.ts tests/indexer/token-metadata.test.ts tests/error-handling-integration.test.ts tests/build-queue.test.ts tests/indexer/decoder-parity.test.ts tests/tracer.test.ts",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
Expand Down
136 changes: 113 additions & 23 deletions src/tracer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
/**
* OpenTelemetry SDK initialisation.
* Must be imported BEFORE any other application code (see src/index.ts top).
*
* Tracing is **off by default**. Set TRACING_ENABLED=true (and optionally
* OTLP_ENDPOINT) to turn it on. When OTLP_ENDPOINT is set but
* TRACING_ENABLED is absent the SDK is also started, so existing deployments
* that configure only the endpoint continue to work without changes.
*
* Environment variables
* ─────────────────────
* TRACING_ENABLED – "true" / "1" to start the SDK (default: off)
* OTLP_ENDPOINT – OTLP collector base URL (default: http://localhost:4318)
* Setting this variable alone also enables tracing.
* SERVICE_NAME – Reported service name (default: "octraban")
*/

import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
Expand All @@ -11,32 +24,19 @@ import {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
import { logger } from './logger';

const OTLP_ENDPOINT = process.env.OTLP_ENDPOINT ?? 'http://localhost:4318';
const SERVICE_NAME = process.env.SERVICE_NAME ?? 'octraban';
const SERVICE_VERSION = process.env.npm_package_version ?? '1.0.0';

const resource = new Resource({
[SEMRESATTRS_SERVICE_NAME]: SERVICE_NAME,
[SEMRESATTRS_SERVICE_VERSION]: SERVICE_VERSION,
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sdk = new NodeSDK({
resource: resource as any, // duplicate @opentelemetry/resources versions in sub-packages
traceExporter: new OTLPTraceExporter({ url: `${OTLP_ENDPOINT}/v1/traces` }),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
}),
],
});

sdk.start();
// ---------------------------------------------------------------------------
// Public API — tracer and span helper (always available; no-ops when disabled)
// ---------------------------------------------------------------------------

process.on('SIGTERM', () => sdk.shutdown().catch(() => {}));
const SERVICE_NAME_DEFAULT = 'octraban';
const SERVICE_VERSION_DEFAULT = process.env.npm_package_version ?? '1.0.0';

export const tracer = trace.getTracer(SERVICE_NAME, SERVICE_VERSION);
export const tracer = trace.getTracer(
process.env.SERVICE_NAME ?? SERVICE_NAME_DEFAULT,
SERVICE_VERSION_DEFAULT,
);

/** Run fn inside a named span; sets ERROR status on throw. */
export async function withSpan<T>(
Expand All @@ -56,3 +56,93 @@ export async function withSpan<T>(
}

export { trace, SpanStatusCode };

// ---------------------------------------------------------------------------
// Gating logic — exported for testing
// ---------------------------------------------------------------------------

/** Returns true when the environment signals that tracing should start. */
export function isTracingEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
const flag = env.TRACING_ENABLED;
const endpoint = env.OTLP_ENDPOINT;
return flag === 'true' || flag === '1' || (endpoint !== undefined && endpoint !== '');
}

// ---------------------------------------------------------------------------
// SDK initialisation
// ---------------------------------------------------------------------------

let _sdk: NodeSDK | null = null;

/**
* Initialise the OpenTelemetry SDK. Called once at process startup.
* Safe to call multiple times — subsequent calls are no-ops.
*
* Returns true if the SDK was started successfully, false otherwise.
*/
export function initTracing(env: NodeJS.ProcessEnv = process.env): boolean {
if (_sdk !== null) return true; // already initialised

if (!isTracingEnabled(env)) {
logger.info('OpenTelemetry tracing disabled (set TRACING_ENABLED=true to enable)');
return false;
}

const endpoint = env.OTLP_ENDPOINT ?? 'http://localhost:4318';
const serviceName = env.SERVICE_NAME ?? SERVICE_NAME_DEFAULT;
const serviceVersion = env.npm_package_version ?? SERVICE_VERSION_DEFAULT;

const resource = new Resource({
[SEMRESATTRS_SERVICE_NAME]: serviceName,
[SEMRESATTRS_SERVICE_VERSION]: serviceVersion,
});

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_sdk = new NodeSDK({
resource: resource as any, // duplicate @opentelemetry/resources versions in sub-packages
traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
}),
],
});

_sdk.start();
logger.info('OpenTelemetry tracing started', { endpoint, service: serviceName });
return true;
} catch (err) {
// A startup failure (e.g. bad exporter config) must never crash the process.
logger.warn('OpenTelemetry SDK failed to start — tracing disabled', {
error: String(err),
});
_sdk = null;
return false;
}
}

// ---------------------------------------------------------------------------
// Graceful shutdown — flush spans on SIGTERM and SIGINT
// ---------------------------------------------------------------------------

async function shutdownTracing(): Promise<void> {
if (!_sdk) return;
try {
await _sdk.shutdown();
logger.info('OpenTelemetry SDK shut down cleanly');
} catch (err) {
logger.warn('OpenTelemetry SDK shutdown error (spans may be incomplete)', {
error: String(err),
});
}
}

process.on('SIGTERM', () => void shutdownTracing());
process.on('SIGINT', () => void shutdownTracing());

// ---------------------------------------------------------------------------
// Auto-start at import time (production path) — reads process.env
// ---------------------------------------------------------------------------

initTracing();
Loading
Loading