@thesharks/analytics
v0.0.0
Published
Opinionated Sentry-on-OpenTelemetry telemetry bootstrap for Sapphire Discord bots: traces, metrics and logs over OTLP, with a Sapphire logger bridge
Readme
@thesharks/analytics
Full documentation: https://wildbeast.guide/analytics/overview/
Using a coding assistant? Give it the docs as one Markdown file: https://wildbeast.guide/_llms-txt/analytics.txt
OpenTelemetry-first analytics helpers for WildBeast. This package provides:
initOpenTelemetrybootstrap for traces/metrics/logs- Sapphire logger bridge (
AnalyticsLogger) - OpenTelemetry API re-exports (
metrics,logs,trace)
OpenTelemetry bootstrap
The shared bootstrap configures traces, metrics, and logs via OTLP HTTP or gRPC exporters. The bootstrap automatically configures exporters for all signals when an OTLP endpoint is provided. If no endpoint is set, exporters remain disabled unless enableExport is explicitly set to true. The bootstrap enables the pg, undici, ioredis and runtime-node auto-instrumentations by default (disable via config or environment variables).
Common config knobs in initOpenTelemetry:
serviceName,serviceVersion,namespace,environment,instanceId,shardIdenableExportto force exporters on/offshutdownTimeoutMillisto bound telemetry flushing on shutdown (default 10s)exportersto override OTLP endpoints/headers/protocol/timeout/compression per signalinstrumentationsto togglepg,undici,ioredis,fsandruntimeNodesentryto overridedsn,tracesSampleRate,tracesSampler,environment,release
Local metric inspection
Pass an additional reader when initializing telemetry to inspect metrics without
an OTLP collector. LocalMetricReader collects cumulative snapshots that can be
sent through worker IPC; collecting them does not drain other metric readers.
import { initOpenTelemetry, LocalMetricReader } from '@thesharks/analytics'
const reader = new LocalMetricReader()
const telemetry = initOpenTelemetry({ metricReaders: [reader] })
// After application instruments have emitted data:
const snapshot = await reader.snapshot()
// snapshot.points contains labels, units, counters, gauges, and histogram summaries.
await telemetry.shutdown()Register the reader before starting application work. Collection has a one-second
observable callback timeout, a 200-series cardinality limit per instrument, and a
2,000-point snapshot cap. Check errors and truncated for partial results.
Exporter configuration
Each signal (traces, metrics, logs) can be configured with:
endpoint: OTLP endpoint URL (for examplehttp://localhost:4317orgrpc://localhost:4317)headers: Custom headers for the exporterprotocol: Explicitly set'http'or'grpc'(optional, auto-detected from endpoint URL)timeout: Request timeout in milliseconds (optional)compression: Compression setting ('gzip'or'none', optional)
Protocol selection
Protocol is determined by precedence:
- Config field:
protocol: 'http' | 'grpc' - Signal-specific env var:
OTEL_EXPORTER_OTLP_{SIGNAL}_PROTOCOL - Global env var:
OTEL_EXPORTER_OTLP_PROTOCOL - URL scheme detection:
grpc://orgrpcs://→ gRPC, otherwise HTTP
Multiple exporters
Each signal can have multiple exporters configured as an array:
exporters: {
traces: [
{ endpoint: "http://primary:4317", headers: { "X-Primary": "true" } },
{ endpoint: "http://secondary:4317", headers: { "X-Secondary": "true" } },
]
}Example exporter override:
const telemetry = initOpenTelemetry({
enableExport: true,
exporters: {
otlp: {
endpoint: "http://localhost:4317",
headers: { "Authorization": "Bearer token" },
},
traces: {
endpoint: "grpc://localhost:4317",
timeout: 30000,
compression: "gzip",
},
},
});import { initOpenTelemetry } from "@thesharks/analytics";
const telemetry = initOpenTelemetry({
serviceName: "@thesharks/discord",
namespace: "@thesharks",
shardId: process.env.SHARD_ID,
instrumentations: {
pg: true,
undici: true,
},
});
process.once("SIGINT", () => telemetry.shutdown());
process.once("SIGTERM", () => telemetry.shutdown());API overview
This package exports the OpenTelemetry API for creating metrics, logs, and traces. Use metrics.getMeter, logs.getLogger, and trace.getTracer to instrument your application.
OTel API usage
import { metrics, logs, trace } from "@thesharks/analytics";
const meter = metrics.getMeter("@thesharks/discord");
const logger = logs.getLogger("@thesharks/discord");
const tracer = trace.getTracer("@thesharks/discord");
const guildGauge = meter.createObservableGauge("discord_guilds_total", {
description: "Total number of Discord guilds the bot is in",
});
logger.emit({
severityText: "info",
body: "Telemetry initialized",
});Sapphire logger bridge
The AnalyticsLogger extends Sapphire's logger to automatically send logs to both OpenTelemetry and Sentry:
import { AnalyticsLogger } from "@thesharks/analytics";
const client = new SapphireClient({
logger: {
instance: new AnalyticsLogger(),
},
});The AnalyticsLogger sends all Sapphire logs to three destinations:
- Console (standard behavior)
- OpenTelemetry as log records
- Sentry using the Logs API (trace, debug, info, warn, error, fatal)
Sentry Logs integration is enabled by default and requires @sentry/node v9.41.0 or later.
Environment variables
OpenTelemetry
Required for export
OTEL_EXPORTER_OTLP_ENDPOINT: Base OTLP endpoint (for examplehttp://localhost:4317orgrpc://localhost:4317)
Endpoints (optional)
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: Overrides traces endpointOTEL_EXPORTER_OTLP_METRICS_ENDPOINT: Overrides metrics endpointOTEL_EXPORTER_OTLP_LOGS_ENDPOINT: Overrides logs endpoint
Headers (optional)
Format: Comma-separated key=value pairs (for example Authorization=Bearer token,X-Custom-Header=value)
OTEL_EXPORTER_OTLP_HEADERS: Global headers for all OTLP exportersOTEL_EXPORTER_OTLP_TRACES_HEADERS: Traces-specific headersOTEL_EXPORTER_OTLP_METRICS_HEADERS: Metrics-specific headersOTEL_EXPORTER_OTLP_LOGS_HEADERS: Logs-specific headers
Protocol (optional)
OTEL_EXPORTER_OTLP_PROTOCOL: Global protocol:http/protobuforgrpcOTEL_EXPORTER_OTLP_TRACES_PROTOCOL: Traces-specific protocolOTEL_EXPORTER_OTLP_METRICS_PROTOCOL: Metrics-specific protocolOTEL_EXPORTER_OTLP_LOGS_PROTOCOL: Logs-specific protocol
Timeout (optional)
OTEL_EXPORTER_OTLP_TIMEOUT: Request timeout in milliseconds
Compression (optional)
OTEL_EXPORTER_OTLP_COMPRESSION: Compression setting:gzipornone
Service configuration (optional)
OTEL_SERVICE_NAME: Overrides service nameOTEL_DIAGNOSTIC_LOG_LEVEL: Set toverboseto enable OTEL diagnosticsOTEL_INSTRUMENTATION_PG_ENABLED: Set tofalseto disable pg instrumentationOTEL_INSTRUMENTATION_UNDICI_ENABLED: Set tofalseto disable undici instrumentation
Sentry
SENTRY_DSN: Sentry DSN for error tracingSENTRY_VALIDATE_OTEL_SETUP: Set totrueto validate OTEL setup on boot (throws if validation fails)
End-to-end testing
test/collector.e2e.test.ts verifies the full pipeline against a real OpenTelemetry collector (both OTLP/HTTP and OTLP/gRPC). The easiest way to run it is the repo-wide integration runner, which provisions the collector (and Redis) in docker automatically:
pnpm test:integration # from the repo rootTo run it manually instead, it is skipped unless OTEL_E2E_OUTPUT is set:
mkdir -p /tmp/otel-e2e && chmod 777 /tmp/otel-e2e
docker run --rm -d --name otel-e2e \
-p 14317:4317 -p 14318:4318 \
-v $PWD/test/fixtures/otel-collector.yaml:/etc/otelcol-contrib/config.yaml \
-v /tmp/otel-e2e:/out \
otel/opentelemetry-collector-contrib:latest
pnpm build # scenarios boot the real pipeline from dist
OTEL_E2E_OUTPUT=/tmp/otel-e2e/telemetry.json pnpm test
docker rm -f otel-e2e