@kisameholmes/horizon_node 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # Horizon Node.js SDK
2
+
3
+ High-performance observability and logging for Node.js.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @horizon/node
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Class-based Client**: Easy to use manual logging.
14
+ - **Batching**: Logs are buffered and sent in batches to minimize HTTP overhead.
15
+ - **Resilience**: Automatic exponential backoff for 5xx errors.
16
+ - **Transports**: Native support for Pino and Winston.
17
+ - **Middlewares**: One-line integration for Fastify and Express.
18
+ - **Error Watchdog**: Automatically catches uncaught exceptions and rejections.
19
+
20
+ ## Core Usage
21
+
22
+ ```typescript
23
+ import { HorizonClient } from '@horizon/node';
24
+
25
+ const horizon = new HorizonClient({
26
+ apiKey: 'YOUR_API_KEY',
27
+ environment: 'production'
28
+ });
29
+
30
+ // Capture manual logs
31
+ horizon.captureLog('info', 'Process started', { version: '1.2.0' });
32
+
33
+ // Auto-capture global errors
34
+ horizon.autoCaptureErrors();
35
+ ```
36
+
37
+ ## Framework Integration
38
+
39
+ ### Fastify
40
+
41
+ ```typescript
42
+ import Fastify from 'fastify';
43
+ import { fastifyHorizonMiddleware } from '@horizon/node';
44
+
45
+ const app = Fastify();
46
+ app.addHook('onRequest', fastifyHorizonMiddleware(horizon));
47
+ ```
48
+
49
+ ### Express
50
+
51
+ ```typescript
52
+ import express from 'express';
53
+ import { expressHorizonMiddleware } from '@horizon/node';
54
+
55
+ const app = express();
56
+ app.use(expressHorizonMiddleware(horizon));
57
+ ```
58
+
59
+ ## Transports
60
+
61
+ ### Pino
62
+
63
+ ```typescript
64
+ const logger = pino({
65
+ transport: {
66
+ target: '@horizon/node/pino',
67
+ options: { apiKey: '...' }
68
+ }
69
+ });
70
+ ```
71
+
72
+ ### Winston
73
+
74
+ ```typescript
75
+ import { HorizonWinstonTransport } from '@horizon/node';
76
+
77
+ const logger = winston.createLogger({
78
+ transports: [
79
+ new HorizonWinstonTransport({ apiKey: '...' })
80
+ ]
81
+ });
82
+ ```
83
+
84
+ ## Configuration Options
85
+
86
+ | Option | Type | Default | Description |
87
+ | --- | --- | --- | --- |
88
+ | `apiKey` | string | **Required** | Your environment API key. |
89
+ | `ingestUrl` | string | `https://horizon-api.bylinee.com/v1/ingest` | Horizon ingestion endpoint. |
90
+ | `environment` | string | `production` | Deployment environment name. |
91
+ | `batchSize` | number | `50` | Maximum logs per batch. |
92
+ | `flushInterval` | number | `2000` | Max wait time (ms) before flushing. |
93
+ | `maxRetries` | number | `3` | Number of retry attempts for 5xx errors. |
@@ -0,0 +1,36 @@
1
+ export interface HorizonClientOptions {
2
+ apiKey: string;
3
+ ingestUrl?: string;
4
+ environment?: string;
5
+ batchSize?: number;
6
+ flushInterval?: number;
7
+ maxRetries?: number;
8
+ }
9
+ /**
10
+ * Horizon Node.js SDK Client
11
+ *
12
+ * Provides unified ingestion, batching, and resilience.
13
+ */
14
+ export declare class HorizonClient {
15
+ private apiKey;
16
+ private ingestUrl;
17
+ private environment;
18
+ private batchSize;
19
+ private flushInterval;
20
+ private maxRetries;
21
+ private buffer;
22
+ private timer;
23
+ constructor(opts: HorizonClientOptions);
24
+ /**
25
+ * Send a manual log to Horizon (Buffered)
26
+ */
27
+ captureLog(level: 'debug' | 'info' | 'warn' | 'error' | 'fatal', message: string, metadata?: any): Promise<void>;
28
+ /**
29
+ * Internal flush with Exponential Backoff
30
+ */
31
+ private flush;
32
+ /**
33
+ * Automatically capture uncaught exceptions and unhandled rejections.
34
+ */
35
+ autoCaptureErrors(): void;
36
+ }
package/dist/client.js ADDED
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HorizonClient = void 0;
4
+ /**
5
+ * Horizon Node.js SDK Client
6
+ *
7
+ * Provides unified ingestion, batching, and resilience.
8
+ */
9
+ class HorizonClient {
10
+ constructor(opts) {
11
+ this.buffer = [];
12
+ this.timer = null;
13
+ this.apiKey = opts.apiKey;
14
+ this.ingestUrl = opts.ingestUrl || 'https://horizon-api.bylinee.com/v1/ingest';
15
+ this.environment = opts.environment || 'production';
16
+ this.batchSize = opts.batchSize || 50;
17
+ this.flushInterval = opts.flushInterval || 2000;
18
+ this.maxRetries = opts.maxRetries || 3;
19
+ if (!this.apiKey) {
20
+ throw new Error('Horizon API Key is required');
21
+ }
22
+ }
23
+ /**
24
+ * Send a manual log to Horizon (Buffered)
25
+ */
26
+ async captureLog(level, message, metadata = {}) {
27
+ this.buffer.push({
28
+ level,
29
+ message,
30
+ metadata: {
31
+ ...metadata,
32
+ sdk_env: this.environment,
33
+ },
34
+ timestamp: new Date().toISOString(),
35
+ });
36
+ if (this.buffer.length >= this.batchSize) {
37
+ await this.flush();
38
+ }
39
+ else if (!this.timer) {
40
+ this.timer = setTimeout(() => this.flush(), this.flushInterval);
41
+ }
42
+ }
43
+ /**
44
+ * Internal flush with Exponential Backoff
45
+ */
46
+ async flush() {
47
+ if (this.buffer.length === 0)
48
+ return;
49
+ if (this.timer) {
50
+ clearTimeout(this.timer);
51
+ this.timer = null;
52
+ }
53
+ const payload = [...this.buffer];
54
+ this.buffer = [];
55
+ const send = async (attempt) => {
56
+ try {
57
+ const res = await fetch(this.ingestUrl, {
58
+ method: 'POST',
59
+ headers: {
60
+ 'Content-Type': 'application/json',
61
+ 'x-api-key': this.apiKey,
62
+ },
63
+ body: JSON.stringify(payload),
64
+ });
65
+ if (res.ok || (res.status >= 400 && res.status < 500)) {
66
+ return true;
67
+ }
68
+ if (res.status >= 500 && attempt < this.maxRetries) {
69
+ const delay = Math.pow(2, attempt) * 1000;
70
+ await new Promise(resolve => setTimeout(resolve, delay));
71
+ return send(attempt + 1);
72
+ }
73
+ return false;
74
+ }
75
+ catch (e) {
76
+ if (attempt < this.maxRetries) {
77
+ const delay = Math.pow(2, attempt) * 1000;
78
+ await new Promise(resolve => setTimeout(resolve, delay));
79
+ return send(attempt + 1);
80
+ }
81
+ return false;
82
+ }
83
+ };
84
+ const success = await send(0);
85
+ if (!success) {
86
+ console.error('[Horizon SDK] Failed to send logs after multiple retries.');
87
+ }
88
+ }
89
+ /**
90
+ * Automatically capture uncaught exceptions and unhandled rejections.
91
+ */
92
+ autoCaptureErrors() {
93
+ process.on('uncaughtException', async (error) => {
94
+ console.error('[Horizon SDK] Uncaught Exception detected.');
95
+ await this.captureLog('fatal', error.message || 'Uncaught Exception', {
96
+ stack: error.stack,
97
+ type: 'uncaughtException',
98
+ });
99
+ await this.flush(); // Force flush
100
+ process.exit(1);
101
+ });
102
+ process.on('unhandledRejection', async (reason) => {
103
+ const message = reason instanceof Error ? reason.message : String(reason);
104
+ await this.captureLog('fatal', message || 'Unhandled Rejection', {
105
+ type: 'unhandledRejection',
106
+ reason: reason instanceof Error ? reason.stack : reason
107
+ });
108
+ });
109
+ }
110
+ }
111
+ exports.HorizonClient = HorizonClient;
@@ -0,0 +1,4 @@
1
+ export * from './client';
2
+ export * from './pino-horizon';
3
+ export * from './winston-horizon';
4
+ export * from './middleware';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./client"), exports);
18
+ __exportStar(require("./pino-horizon"), exports);
19
+ __exportStar(require("./winston-horizon"), exports);
20
+ __exportStar(require("./middleware"), exports);
@@ -0,0 +1,13 @@
1
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2
+ import type { Request, Response, NextFunction } from 'express';
3
+ import { HorizonClient } from './client';
4
+ /**
5
+ * Fastify Middleware (Hook)
6
+ * Automatically logs incoming requests and response times.
7
+ */
8
+ export declare function fastifyHorizonMiddleware(client: HorizonClient): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
9
+ /**
10
+ * Express Middleware
11
+ * Automatically logs incoming requests and response times.
12
+ */
13
+ export declare function expressHorizonMiddleware(client: HorizonClient): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fastifyHorizonMiddleware = fastifyHorizonMiddleware;
4
+ exports.expressHorizonMiddleware = expressHorizonMiddleware;
5
+ /**
6
+ * Fastify Middleware (Hook)
7
+ * Automatically logs incoming requests and response times.
8
+ */
9
+ function fastifyHorizonMiddleware(client) {
10
+ return async (request, reply) => {
11
+ const start = Date.now();
12
+ // Listen for when the response is sent
13
+ reply.raw.on('finish', () => {
14
+ const duration = Date.now() - start;
15
+ client.captureLog('info', `${request.method} ${request.url} - ${reply.statusCode}`, {
16
+ method: request.method,
17
+ url: request.url,
18
+ statusCode: reply.statusCode,
19
+ durationMs: duration,
20
+ ip: request.ip,
21
+ userAgent: request.headers['user-agent'],
22
+ });
23
+ });
24
+ };
25
+ }
26
+ /**
27
+ * Express Middleware
28
+ * Automatically logs incoming requests and response times.
29
+ */
30
+ function expressHorizonMiddleware(client) {
31
+ return (req, res, next) => {
32
+ const start = Date.now();
33
+ res.on('finish', () => {
34
+ const duration = Date.now() - start;
35
+ client.captureLog('info', `${req.method} ${req.originalUrl} - ${res.statusCode}`, {
36
+ method: req.method,
37
+ url: req.originalUrl,
38
+ statusCode: res.statusCode,
39
+ durationMs: duration,
40
+ ip: req.ip,
41
+ userAgent: req.get('user-agent'),
42
+ });
43
+ });
44
+ next();
45
+ };
46
+ }
@@ -0,0 +1,17 @@
1
+ import build from 'pino-abstract-transport';
2
+ interface HorizonTransportOptions {
3
+ apiKey: string;
4
+ ingestUrl?: string;
5
+ batchSize?: number;
6
+ flushInterval?: number;
7
+ }
8
+ /**
9
+ * Horizon Pino Transport
10
+ *
11
+ * Performance features:
12
+ * - Batches logs up to batchSize (default 50)
13
+ * - Flushes every flushInterval (default 2s)
14
+ * - Converts Pino numeric levels to Horizon string levels
15
+ */
16
+ export default function (opts: HorizonTransportOptions): Promise<import("stream").Transform & build.OnUnknown>;
17
+ export {};
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = default_1;
7
+ const pino_abstract_transport_1 = __importDefault(require("pino-abstract-transport"));
8
+ /**
9
+ * Pino Numeric Levels:
10
+ * 10: trace
11
+ * 20: debug
12
+ * 30: info
13
+ * 40: warn
14
+ * 50: error
15
+ * 60: fatal
16
+ */
17
+ const PINO_LEVEL_MAP = {
18
+ 10: 'debug', // Map trace to debug as per Horizon capabilities
19
+ 20: 'debug',
20
+ 30: 'info',
21
+ 40: 'warn',
22
+ 50: 'error',
23
+ 60: 'fatal',
24
+ };
25
+ /**
26
+ * Horizon Pino Transport
27
+ *
28
+ * Performance features:
29
+ * - Batches logs up to batchSize (default 50)
30
+ * - Flushes every flushInterval (default 2s)
31
+ * - Converts Pino numeric levels to Horizon string levels
32
+ */
33
+ async function default_1(opts) {
34
+ const { apiKey, ingestUrl = 'https://horizon-api.bylinee.com/v1/ingest', batchSize = 50, flushInterval = 2000, } = opts;
35
+ if (!apiKey) {
36
+ throw new Error('Horizon API Key is required for ingestion');
37
+ }
38
+ let buffer = [];
39
+ let timer = null;
40
+ const flush = async () => {
41
+ if (buffer.length === 0)
42
+ return;
43
+ const payload = [...buffer];
44
+ buffer = [];
45
+ if (timer) {
46
+ clearTimeout(timer);
47
+ timer = null;
48
+ }
49
+ try {
50
+ const res = await fetch(ingestUrl, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'x-api-key': apiKey,
55
+ },
56
+ body: JSON.stringify(payload),
57
+ });
58
+ if (!res.ok) {
59
+ const err = await res.json().catch(() => ({ error: 'Unknown server error' }));
60
+ console.warn(`[Horizon Pino] Ingestion failed (${res.status}): ${err.error}`);
61
+ }
62
+ }
63
+ catch (e) {
64
+ console.error('[Horizon Pino] Critical transport error:', e);
65
+ }
66
+ };
67
+ return (0, pino_abstract_transport_1.default)(async function (source) {
68
+ for await (const obj of source) {
69
+ if (!obj)
70
+ continue;
71
+ const level = PINO_LEVEL_MAP[obj.level] || 'info';
72
+ const message = obj.msg || 'No message';
73
+ const timestamp = obj.time ? new Date(obj.time).toISOString() : new Date().toISOString();
74
+ // Clean pino-specific internal fields from metadata
75
+ const metadata = { ...obj };
76
+ delete metadata.level;
77
+ delete metadata.msg;
78
+ delete metadata.time;
79
+ delete metadata.v;
80
+ buffer.push({
81
+ level,
82
+ message,
83
+ metadata,
84
+ timestamp,
85
+ });
86
+ if (buffer.length >= batchSize) {
87
+ await flush();
88
+ }
89
+ else if (!timer) {
90
+ timer = setTimeout(flush, flushInterval);
91
+ }
92
+ }
93
+ }, {
94
+ async close() {
95
+ // Ensure remaining logs are sent on process exit
96
+ await flush();
97
+ },
98
+ });
99
+ }
@@ -0,0 +1,34 @@
1
+ import Transport from 'winston-transport';
2
+ interface HorizonWinstonOptions extends Transport.TransportStreamOptions {
3
+ apiKey: string;
4
+ ingestUrl?: string;
5
+ batchSize?: number;
6
+ flushInterval?: number;
7
+ }
8
+ /**
9
+ * Winston Transport for Horizon
10
+ *
11
+ * Integration:
12
+ * logger.add(new HorizonWinstonTransport({ apiKey: '...' }));
13
+ *
14
+ * Features:
15
+ * - Batched ingestion (batchSize, flushInterval)
16
+ * - Resilience: Fails silently to console.warn to protect host app
17
+ * - Standard level mapping
18
+ */
19
+ export declare class HorizonWinstonTransport extends Transport {
20
+ private apiKey;
21
+ private ingestUrl;
22
+ private batchSize;
23
+ private flushInterval;
24
+ private buffer;
25
+ private timer;
26
+ constructor(opts: HorizonWinstonOptions);
27
+ log(info: any, callback: () => void): void;
28
+ private flush;
29
+ /**
30
+ * Ensure pending logs are flushed on close/exit
31
+ */
32
+ close(): Promise<void>;
33
+ }
34
+ export {};
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HorizonWinstonTransport = void 0;
7
+ const winston_transport_1 = __importDefault(require("winston-transport"));
8
+ /**
9
+ * Winston Default Levels:
10
+ * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
11
+ */
12
+ const WINSTON_LEVEL_MAP = {
13
+ error: 'error',
14
+ warn: 'warn',
15
+ info: 'info',
16
+ http: 'info',
17
+ verbose: 'debug',
18
+ debug: 'debug',
19
+ silly: 'debug',
20
+ };
21
+ /**
22
+ * Winston Transport for Horizon
23
+ *
24
+ * Integration:
25
+ * logger.add(new HorizonWinstonTransport({ apiKey: '...' }));
26
+ *
27
+ * Features:
28
+ * - Batched ingestion (batchSize, flushInterval)
29
+ * - Resilience: Fails silently to console.warn to protect host app
30
+ * - Standard level mapping
31
+ */
32
+ class HorizonWinstonTransport extends winston_transport_1.default {
33
+ constructor(opts) {
34
+ super(opts);
35
+ this.buffer = [];
36
+ this.timer = null;
37
+ this.apiKey = opts.apiKey;
38
+ this.ingestUrl = opts.ingestUrl || 'https://horizon-api.bylinee.com/v1/ingest';
39
+ this.batchSize = opts.batchSize || 50;
40
+ this.flushInterval = opts.flushInterval || 2000;
41
+ if (!this.apiKey) {
42
+ throw new Error('Horizon API Key is required for Winston Transport');
43
+ }
44
+ }
45
+ log(info, callback) {
46
+ setImmediate(() => {
47
+ this.emit('logged', info);
48
+ });
49
+ const { level, message, ...metadata } = info;
50
+ this.buffer.push({
51
+ level: WINSTON_LEVEL_MAP[level] || 'info',
52
+ message: message || '',
53
+ metadata: metadata || {},
54
+ timestamp: new Date().toISOString(),
55
+ });
56
+ if (this.buffer.length >= this.batchSize) {
57
+ this.flush();
58
+ }
59
+ else if (!this.timer) {
60
+ this.timer = setTimeout(() => this.flush(), this.flushInterval);
61
+ }
62
+ callback();
63
+ }
64
+ async flush() {
65
+ if (this.buffer.length === 0)
66
+ return;
67
+ const payload = [...this.buffer];
68
+ this.buffer = [];
69
+ if (this.timer) {
70
+ clearTimeout(this.timer);
71
+ this.timer = null;
72
+ }
73
+ try {
74
+ const res = await fetch(this.ingestUrl, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'x-api-key': this.apiKey,
79
+ },
80
+ body: JSON.stringify(payload),
81
+ });
82
+ if (!res.ok) {
83
+ const err = await res.json().catch(() => ({ error: 'Unknown response' }));
84
+ console.warn(`[Horizon Winston] Ingestion failed (${res.status}):`, err.error);
85
+ }
86
+ }
87
+ catch (e) {
88
+ // Resilience: Fail silently to protect host application
89
+ console.warn('[Horizon Winston] Resilience Mode: Failed to reach Horizon server.', e instanceof Error ? e.message : e);
90
+ }
91
+ }
92
+ /**
93
+ * Ensure pending logs are flushed on close/exit
94
+ */
95
+ async close() {
96
+ await this.flush();
97
+ }
98
+ }
99
+ exports.HorizonWinstonTransport = HorizonWinstonTransport;
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@kisameholmes/horizon_node",
3
+ "version": "1.0.0",
4
+ "description": "High-performance observability and logging for Node.js",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "logging",
16
+ "tracing",
17
+ "monitoring",
18
+ "horizon",
19
+ "pino",
20
+ "winston"
21
+ ],
22
+ "author": "Horizon",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "axios": "^1.6.0"
26
+ },
27
+ "peerDependencies": {
28
+ "express": "^4.18.0",
29
+ "fastify": "^4.20.0",
30
+ "pino": "^8.0.0",
31
+ "winston": "^3.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "express": {
35
+ "optional": true
36
+ },
37
+ "fastify": {
38
+ "optional": true
39
+ },
40
+ "pino": {
41
+ "optional": true
42
+ },
43
+ "winston": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@types/express": "^4.17.0",
49
+ "@types/node": "^20.0.0",
50
+ "fastify": "^4.0.0",
51
+ "pino": "^8.0.0",
52
+ "winston-transport": "^4.0.0",
53
+ "pino-abstract-transport": "^3.0.0",
54
+ "typescript": "^5.0.0"
55
+ }
56
+ }