@mastra/loggers 0.0.0-commonjs-20250227130920
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/.turbo/turbo-build.log +25 -0
- package/CHANGELOG.md +662 -0
- package/LICENSE +44 -0
- package/README.md +146 -0
- package/dist/_tsup-dts-rollup.d.cts +50 -0
- package/dist/_tsup-dts-rollup.d.ts +50 -0
- package/dist/file/index.cjs +63 -0
- package/dist/file/index.d.cts +1 -0
- package/dist/file/index.d.ts +1 -0
- package/dist/file/index.js +61 -0
- package/dist/upstash/index.cjs +133 -0
- package/dist/upstash/index.d.cts +1 -0
- package/dist/upstash/index.d.ts +1 -0
- package/dist/upstash/index.js +131 -0
- package/eslint.config.js +6 -0
- package/package.json +62 -0
- package/src/file/fixtures/test.log +0 -0
- package/src/file/index.test.ts +130 -0
- package/src/file/index.ts +73 -0
- package/src/index.ts +0 -0
- package/src/upstash/index.test.ts +184 -0
- package/src/upstash/index.ts +176 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +8 -0
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mastra/loggers",
|
|
3
|
+
"version": "0.0.0-commonjs-20250227130920",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./file": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/file/index.d.ts",
|
|
22
|
+
"default": "./dist/file/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/file/index.d.cts",
|
|
26
|
+
"default": "./dist/file/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"./upstash": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/upstash/index.d.ts",
|
|
32
|
+
"default": "./dist/upstash/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/upstash/index.d.cts",
|
|
36
|
+
"default": "./dist/upstash/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [],
|
|
42
|
+
"author": "",
|
|
43
|
+
"license": "ISC",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@mastra/core": "^0.0.0-commonjs-20250227130920"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@microsoft/api-extractor": "^7.49.2",
|
|
49
|
+
"@types/node": "^22.13.1",
|
|
50
|
+
"tsup": "^8.0.1",
|
|
51
|
+
"typescript": "^5.7.3",
|
|
52
|
+
"vitest": "^3.0.4",
|
|
53
|
+
"eslint": "^9.20.1",
|
|
54
|
+
"@internal/lint": "0.0.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup src/file/index.ts src/upstash/index.ts --format esm,cjs --experimental-dts --clean --treeshake",
|
|
58
|
+
"build:watch": "pnpm build --watch",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"lint": "eslint ."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createLogger, LogLevel } from '@mastra/core/logger';
|
|
4
|
+
import { describe, it, expect, beforeEach, vi, afterAll } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { FileTransport } from './index.js';
|
|
7
|
+
|
|
8
|
+
describe('FileTransport', () => {
|
|
9
|
+
const testDir = __dirname + '/fixtures';
|
|
10
|
+
const testFile = 'test.log';
|
|
11
|
+
const testPath = path.join(testDir, testFile);
|
|
12
|
+
let fileLogger: FileTransport;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
// Create test directory
|
|
16
|
+
if (!fs.existsSync(testDir)) {
|
|
17
|
+
fs.mkdirSync(testDir);
|
|
18
|
+
}
|
|
19
|
+
fileLogger = new FileTransport({ path: testPath });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterAll(async () => {
|
|
23
|
+
// Cleanup test directory
|
|
24
|
+
fs.writeFileSync(testPath, ``);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should create a file stream when instantiated', () => {
|
|
28
|
+
expect(fileLogger.fileStream).toBeDefined();
|
|
29
|
+
expect(fileLogger.path).toBe(testPath);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should work with createLogger', async () => {
|
|
33
|
+
const logger = createLogger({
|
|
34
|
+
name: 'test-logger',
|
|
35
|
+
level: LogLevel.INFO,
|
|
36
|
+
transports: {
|
|
37
|
+
file: fileLogger,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const testMessage = 'test info message';
|
|
42
|
+
logger.info(testMessage);
|
|
43
|
+
|
|
44
|
+
// Wait for file write to complete
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
46
|
+
|
|
47
|
+
const fileContent = fs.readFileSync(testPath, 'utf8');
|
|
48
|
+
expect(fileContent).toContain(testMessage);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle multiple log messages', async () => {
|
|
52
|
+
const logger = createLogger({
|
|
53
|
+
name: 'test-logger',
|
|
54
|
+
level: LogLevel.INFO,
|
|
55
|
+
transports: {
|
|
56
|
+
file: fileLogger,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const messages = ['message1', 'message2', 'message3'];
|
|
61
|
+
messages.forEach(msg => logger.info(msg));
|
|
62
|
+
|
|
63
|
+
// Wait for file writes to complete
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
65
|
+
|
|
66
|
+
const fileContent = fs.readFileSync(testPath, 'utf8');
|
|
67
|
+
messages.forEach(msg => {
|
|
68
|
+
expect(fileContent).toContain(msg);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should properly clean up resources on destroy', () => {
|
|
73
|
+
const destroySpy = vi.spyOn(fileLogger.fileStream, 'destroy');
|
|
74
|
+
|
|
75
|
+
fileLogger._destroy(new Error('test'), () => {
|
|
76
|
+
expect(destroySpy).toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle errors in _transform', () => {
|
|
81
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
82
|
+
const errorObj = new Error('Test error');
|
|
83
|
+
|
|
84
|
+
vi.spyOn(fileLogger.fileStream, 'write').mockImplementationOnce(() => {
|
|
85
|
+
throw errorObj;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
fileLogger._transform('test', 'utf8', error => {
|
|
89
|
+
expect(consoleSpy).toHaveBeenCalledWith('Error parsing log entry:', errorObj);
|
|
90
|
+
expect(error).toBeNull(); // Should not propagate error
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should flush remaining data when stream ends', () => {
|
|
95
|
+
const endSpy = vi.spyOn(fileLogger.fileStream, 'end');
|
|
96
|
+
|
|
97
|
+
fileLogger._flush(() => {
|
|
98
|
+
expect(endSpy).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('getLogs and getLogsByRunId', () => {
|
|
103
|
+
it('should return empty array for getLogs', async () => {
|
|
104
|
+
const logs = await fileLogger.getLogs();
|
|
105
|
+
expect(logs.length).toBeGreaterThan(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return empty array for getLogsByRunId', async () => {
|
|
109
|
+
const logger = createLogger({
|
|
110
|
+
name: 'test-logger',
|
|
111
|
+
level: LogLevel.INFO,
|
|
112
|
+
transports: {
|
|
113
|
+
file: fileLogger,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
let logs = await fileLogger.getLogsByRunId({ runId: 'test-run-id' });
|
|
118
|
+
expect(logs.length).toBe(0);
|
|
119
|
+
|
|
120
|
+
logger.info('test info message', {
|
|
121
|
+
runId: 'test-run-id',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
125
|
+
|
|
126
|
+
logs = await fileLogger.getLogsByRunId({ runId: 'test-run-id' });
|
|
127
|
+
expect(logs.length).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { WriteStream } from 'fs';
|
|
2
|
+
import { createWriteStream, existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { LoggerTransport } from '@mastra/core/logger';
|
|
4
|
+
import type { BaseLogMessage } from '@mastra/core/logger';
|
|
5
|
+
|
|
6
|
+
export class FileTransport extends LoggerTransport {
|
|
7
|
+
path: string;
|
|
8
|
+
fileStream: WriteStream;
|
|
9
|
+
constructor({ path }: { path: string }) {
|
|
10
|
+
super({ objectMode: true });
|
|
11
|
+
this.path = path;
|
|
12
|
+
|
|
13
|
+
if (!existsSync(this.path)) {
|
|
14
|
+
console.log(this.path);
|
|
15
|
+
throw new Error('File path does not exist');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.fileStream = createWriteStream(this.path, { flags: 'a' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_transform(chunk: any, _encoding: string, callback: (error: Error | null, chunk: any) => void) {
|
|
22
|
+
try {
|
|
23
|
+
this.fileStream.write(chunk);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error parsing log entry:', error);
|
|
26
|
+
}
|
|
27
|
+
callback(null, chunk);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_flush(callback: Function) {
|
|
31
|
+
// End the file stream when transform stream ends
|
|
32
|
+
this.fileStream.end(() => {
|
|
33
|
+
callback();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_write(chunk: any, encoding?: string, callback?: (error?: Error | null) => void): boolean {
|
|
38
|
+
if (typeof callback === 'function') {
|
|
39
|
+
this._transform(chunk, encoding || 'utf8', callback);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this._transform(chunk, encoding || 'utf8', (error: Error | null) => {
|
|
44
|
+
if (error) console.error('Transform error in write:', error);
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Clean up resources
|
|
50
|
+
_destroy(error: Error, callback: Function) {
|
|
51
|
+
if (this.fileStream) {
|
|
52
|
+
this.fileStream.destroy(error);
|
|
53
|
+
}
|
|
54
|
+
callback(error);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getLogs(): Promise<BaseLogMessage[]> {
|
|
58
|
+
return readFileSync(this.path, 'utf8')
|
|
59
|
+
.split('\n')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map(log => JSON.parse(log));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getLogsByRunId({ runId }: { runId: string }): Promise<BaseLogMessage[]> {
|
|
65
|
+
try {
|
|
66
|
+
const allLogs = await this.getLogs();
|
|
67
|
+
return (allLogs.filter(log => log?.runId === runId) || []) as BaseLogMessage[];
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error getting logs by runId from Upstash:', error);
|
|
70
|
+
return [] as BaseLogMessage[];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createLogger, LogLevel } from '@mastra/core/logger';
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { UpstashTransport } from './index.js';
|
|
5
|
+
|
|
6
|
+
describe('UpstashTransport', () => {
|
|
7
|
+
const defaultOptions = {
|
|
8
|
+
upstashUrl: 'https://test-url.upstash.io',
|
|
9
|
+
upstashToken: 'test-token',
|
|
10
|
+
listName: 'test-logs',
|
|
11
|
+
maxListLength: 1000,
|
|
12
|
+
batchSize: 10,
|
|
13
|
+
flushInterval: 1000,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let transport: UpstashTransport;
|
|
17
|
+
let fetchMock: any;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
fetchMock = vi.fn(() =>
|
|
21
|
+
Promise.resolve({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: () => Promise.resolve({ result: 'success' }),
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
global.fetch = fetchMock;
|
|
27
|
+
|
|
28
|
+
vi.useFakeTimers();
|
|
29
|
+
transport = new UpstashTransport(defaultOptions);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.clearAllTimers();
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
vi.useRealTimers();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should initialize with correct options', () => {
|
|
39
|
+
expect(transport.upstashUrl).toBe(defaultOptions.upstashUrl);
|
|
40
|
+
expect(transport.upstashToken).toBe(defaultOptions.upstashToken);
|
|
41
|
+
expect(transport.listName).toBe(defaultOptions.listName);
|
|
42
|
+
expect(transport.logBuffer).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should work with createLogger', async () => {
|
|
46
|
+
const logger = createLogger({
|
|
47
|
+
name: 'test-logger',
|
|
48
|
+
level: LogLevel.INFO,
|
|
49
|
+
transports: {
|
|
50
|
+
upstash: transport,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const testMessage = 'test info message';
|
|
55
|
+
logger.info(testMessage);
|
|
56
|
+
|
|
57
|
+
// Trigger flush
|
|
58
|
+
await transport._flush();
|
|
59
|
+
|
|
60
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
61
|
+
defaultOptions.upstashUrl,
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${defaultOptions.upstashToken}`,
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
body: expect.stringContaining(testMessage),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle multiple log messages', async () => {
|
|
74
|
+
const logger = createLogger({
|
|
75
|
+
name: 'test-logger',
|
|
76
|
+
level: LogLevel.INFO,
|
|
77
|
+
transports: {
|
|
78
|
+
upstash: transport,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const messages = ['message1', 'message2', 'message3'];
|
|
83
|
+
messages.forEach(msg => logger.info(msg));
|
|
84
|
+
|
|
85
|
+
// Trigger flush
|
|
86
|
+
await transport._flush();
|
|
87
|
+
|
|
88
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
89
|
+
messages.forEach(msg => {
|
|
90
|
+
expect(body.pipeline[0].some((cmd: string) => cmd.includes(msg))).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should properly clean up resources on destroy', () => {
|
|
95
|
+
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
|
96
|
+
const flushSpy = vi.spyOn(transport, '_flush').mockImplementation(() => Promise.resolve());
|
|
97
|
+
|
|
98
|
+
transport._destroy(new Error('test'), () => {
|
|
99
|
+
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
100
|
+
if (transport.logBuffer.length > 0) {
|
|
101
|
+
expect(flushSpy).toHaveBeenCalled();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle errors in _transform', () => {
|
|
107
|
+
const callback = vi.fn();
|
|
108
|
+
|
|
109
|
+
transport._transform('invalid json', 'utf8', callback);
|
|
110
|
+
|
|
111
|
+
expect(callback).toHaveBeenCalledWith(expect.any(Error));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should automatically flush on interval', async () => {
|
|
115
|
+
const logger = createLogger({
|
|
116
|
+
name: 'test-logger',
|
|
117
|
+
level: LogLevel.INFO,
|
|
118
|
+
transports: {
|
|
119
|
+
upstash: transport,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
logger.info('test message');
|
|
124
|
+
|
|
125
|
+
// Advance timer by flush interval
|
|
126
|
+
vi.advanceTimersByTime(defaultOptions.flushInterval);
|
|
127
|
+
await Promise.resolve();
|
|
128
|
+
|
|
129
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('error handling', () => {
|
|
133
|
+
it('should handle Upstash API errors', async () => {
|
|
134
|
+
fetchMock.mockImplementationOnce(() =>
|
|
135
|
+
Promise.resolve({
|
|
136
|
+
ok: false,
|
|
137
|
+
statusText: 'Test Error',
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const logger = createLogger({
|
|
142
|
+
name: 'test-logger',
|
|
143
|
+
level: LogLevel.INFO,
|
|
144
|
+
transports: {
|
|
145
|
+
upstash: transport,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
logger.info('test message');
|
|
150
|
+
|
|
151
|
+
await expect(transport._flush()).rejects.toThrow('Failed to execute Upstash command: Test Error');
|
|
152
|
+
expect(transport.logBuffer.length).toBeGreaterThan(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should handle network errors', async () => {
|
|
156
|
+
fetchMock.mockImplementationOnce(() => Promise.reject(new Error('Network error')));
|
|
157
|
+
|
|
158
|
+
const logger = createLogger({
|
|
159
|
+
name: 'test-logger',
|
|
160
|
+
level: LogLevel.INFO,
|
|
161
|
+
transports: {
|
|
162
|
+
upstash: transport,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
logger.info('test message');
|
|
167
|
+
|
|
168
|
+
await expect(transport._flush()).rejects.toThrow('Network error');
|
|
169
|
+
expect(transport.logBuffer.length).toBeGreaterThan(0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('getLogs and getLogsByRunId', () => {
|
|
174
|
+
it('should return empty array for getLogs', async () => {
|
|
175
|
+
const logs = await transport.getLogs();
|
|
176
|
+
expect(logs).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return empty array for getLogsByRunId', async () => {
|
|
180
|
+
const logs = await transport.getLogsByRunId({ runId: 'test-run-id' });
|
|
181
|
+
expect(logs).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { LoggerTransport } from '@mastra/core/logger';
|
|
2
|
+
import type { BaseLogMessage } from '@mastra/core/logger';
|
|
3
|
+
|
|
4
|
+
export class UpstashTransport extends LoggerTransport {
|
|
5
|
+
upstashUrl: string;
|
|
6
|
+
upstashToken: string;
|
|
7
|
+
listName: string;
|
|
8
|
+
maxListLength: number;
|
|
9
|
+
batchSize: number;
|
|
10
|
+
flushInterval: number;
|
|
11
|
+
logBuffer: any[];
|
|
12
|
+
lastFlush: number;
|
|
13
|
+
flushIntervalId: NodeJS.Timeout;
|
|
14
|
+
|
|
15
|
+
constructor(opts: {
|
|
16
|
+
listName?: string;
|
|
17
|
+
maxListLength?: number;
|
|
18
|
+
batchSize?: number;
|
|
19
|
+
upstashUrl: string;
|
|
20
|
+
flushInterval?: number;
|
|
21
|
+
upstashToken: string;
|
|
22
|
+
}) {
|
|
23
|
+
super({ objectMode: true });
|
|
24
|
+
|
|
25
|
+
if (!opts.upstashUrl || !opts.upstashToken) {
|
|
26
|
+
throw new Error('Upstash URL and token are required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.upstashUrl = opts.upstashUrl;
|
|
30
|
+
this.upstashToken = opts.upstashToken;
|
|
31
|
+
this.listName = opts.listName || 'application-logs';
|
|
32
|
+
this.maxListLength = opts.maxListLength || 10000;
|
|
33
|
+
this.batchSize = opts.batchSize || 100;
|
|
34
|
+
this.flushInterval = opts.flushInterval || 10000;
|
|
35
|
+
|
|
36
|
+
this.logBuffer = [];
|
|
37
|
+
this.lastFlush = Date.now();
|
|
38
|
+
|
|
39
|
+
// Start flush interval
|
|
40
|
+
this.flushIntervalId = setInterval(() => {
|
|
41
|
+
this._flush().catch(err => {
|
|
42
|
+
console.error('Error flushing logs to Upstash:', err);
|
|
43
|
+
});
|
|
44
|
+
}, this.flushInterval);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async executeUpstashCommand(command: any[]): Promise<any> {
|
|
48
|
+
const response = await fetch(`${this.upstashUrl}/pipeline`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${this.upstashToken}`,
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify([command]),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new Error(`Failed to execute Upstash command: ${response.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return response.json();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async _flush() {
|
|
65
|
+
if (this.logBuffer.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const logs = this.logBuffer.splice(0, this.batchSize);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Prepare the Upstash Redis command
|
|
74
|
+
const command = ['LPUSH', this.listName, ...logs.map(log => JSON.stringify(log))];
|
|
75
|
+
|
|
76
|
+
// Trim the list if it exceeds maxListLength
|
|
77
|
+
if (this.maxListLength > 0) {
|
|
78
|
+
command.push('LTRIM', this.listName, 0 as any, (this.maxListLength - 1) as any);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Send logs to Upstash Redis
|
|
82
|
+
await this.executeUpstashCommand(command);
|
|
83
|
+
this.lastFlush = now;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// On error, put logs back in the buffer
|
|
86
|
+
this.logBuffer.unshift(...logs);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_write(chunk: any, encoding?: string, callback?: (error?: Error | null) => void): boolean {
|
|
92
|
+
if (typeof callback === 'function') {
|
|
93
|
+
this._transform(chunk, encoding || 'utf8', callback);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this._transform(chunk, encoding || 'utf8', (error: Error | null) => {
|
|
98
|
+
if (error) console.error('Transform error in write:', error);
|
|
99
|
+
});
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_transform(chunk: string, _enc: string, cb: Function) {
|
|
104
|
+
try {
|
|
105
|
+
// Parse the log line if it's a string
|
|
106
|
+
const log = typeof chunk === 'string' ? JSON.parse(chunk) : chunk;
|
|
107
|
+
|
|
108
|
+
// Add timestamp if not present
|
|
109
|
+
if (!log.time) {
|
|
110
|
+
log.time = Date.now();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add to buffer
|
|
114
|
+
this.logBuffer.push(log);
|
|
115
|
+
|
|
116
|
+
// Flush if buffer reaches batch size
|
|
117
|
+
if (this.logBuffer.length >= this.batchSize) {
|
|
118
|
+
this._flush().catch(err => {
|
|
119
|
+
console.error('Error flushing logs to Upstash:', err);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Pass through the log
|
|
124
|
+
cb(null, chunk);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
cb(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_destroy(err: Error, cb: Function) {
|
|
131
|
+
clearInterval(this.flushIntervalId);
|
|
132
|
+
|
|
133
|
+
// Final flush
|
|
134
|
+
if (this.logBuffer.length > 0) {
|
|
135
|
+
this._flush()
|
|
136
|
+
.then(() => cb(err))
|
|
137
|
+
.catch(flushErr => {
|
|
138
|
+
console.error('Error in final flush:', flushErr);
|
|
139
|
+
cb(err || flushErr);
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
cb(err);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getLogs(): Promise<BaseLogMessage[]> {
|
|
147
|
+
try {
|
|
148
|
+
// Get all logs from the list
|
|
149
|
+
const command = ['LRANGE', this.listName, 0, -1];
|
|
150
|
+
const response = await this.executeUpstashCommand(command);
|
|
151
|
+
|
|
152
|
+
// Parse the logs from JSON strings back to objects
|
|
153
|
+
return response?.[0]?.result.map((log: string) => {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(log);
|
|
156
|
+
} catch {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
}) as BaseLogMessage[];
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('Error getting logs from Upstash:', error);
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getLogsByRunId({ runId }: { runId: string }): Promise<BaseLogMessage[]> {
|
|
167
|
+
try {
|
|
168
|
+
const allLogs = await this.getLogs();
|
|
169
|
+
const logs = (allLogs.filter((log: any) => log.runId === runId) || []) as BaseLogMessage[];
|
|
170
|
+
return logs;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('Error getting logs by runId from Upstash:', error);
|
|
173
|
+
return [] as BaseLogMessage[];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
package/tsconfig.json
ADDED