@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.
@@ -0,0 +1,6 @@
1
+ import { createConfig } from '@internal/lint/eslint';
2
+
3
+ const config = await createConfig();
4
+
5
+ /** @type {import("eslint").Linter.Config[]} */
6
+ export default [...config];
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
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.node.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules", "**/*.test.ts"]
5
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ include: ['src/**/*.test.ts'],
7
+ },
8
+ });