@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 +93 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.js +111 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/middleware.d.ts +13 -0
- package/dist/middleware.js +46 -0
- package/dist/pino-horizon.d.ts +17 -0
- package/dist/pino-horizon.js +99 -0
- package/dist/winston-horizon.d.ts +34 -0
- package/dist/winston-horizon.js +99 -0
- package/package.json +56 -0
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. |
|
package/dist/client.d.ts
ADDED
|
@@ -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;
|
package/dist/index.d.ts
ADDED
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
|
+
}
|