@forgehive/hive-sdk 0.1.2 → 0.1.4
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/dist/index.d.ts +4 -1
- package/dist/index.js +56 -2
- package/dist/test/sendLogByUuid.test.d.ts +1 -0
- package/dist/test/sendLogByUuid.test.js +222 -0
- package/package.json +5 -3
- package/src/index.ts +72 -3
- package/src/test/sendLogByUuid.test.ts +272 -0
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface Metadata {
|
|
|
5
5
|
export type { ExecutionRecord } from '@forgehive/task';
|
|
6
6
|
export interface HiveLogClientConfig {
|
|
7
7
|
projectName: string;
|
|
8
|
+
projectUuid?: string;
|
|
8
9
|
apiKey?: string;
|
|
9
10
|
apiSecret?: string;
|
|
10
11
|
host?: string;
|
|
@@ -35,12 +36,14 @@ export declare class HiveLogClient {
|
|
|
35
36
|
private apiSecret;
|
|
36
37
|
private host;
|
|
37
38
|
private projectName;
|
|
39
|
+
private projectUuid;
|
|
38
40
|
private baseMetadata;
|
|
39
41
|
private isInitialized;
|
|
40
42
|
constructor(config: HiveLogClientConfig);
|
|
41
43
|
isActive(): boolean;
|
|
42
44
|
private mergeMetadata;
|
|
43
|
-
sendLog(record: ExecutionRecord, metadata?: Metadata): Promise<'success' | 'error' | 'silent'>;
|
|
45
|
+
sendLog(record: ExecutionRecord, metadata?: Metadata): Promise<'success' | 'error' | 'silent' | LogApiSuccess>;
|
|
46
|
+
sendLogByUuid(record: ExecutionRecord, taskUuid: string, metadata?: Metadata): Promise<'success' | 'error' | 'silent' | LogApiSuccess>;
|
|
44
47
|
getListener(): (record: ExecutionRecord) => Promise<void>;
|
|
45
48
|
getLog(taskName: string, uuid: string): Promise<LogApiResult | null>;
|
|
46
49
|
setQuality(taskName: string, uuid: string, quality: Quality): Promise<boolean>;
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.isApiError = isApiError;
|
|
|
8
8
|
exports.isInvokeError = isInvokeError;
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
10
|
const debug_1 = __importDefault(require("debug"));
|
|
11
|
+
const uuid_1 = require("uuid");
|
|
11
12
|
const log = (0, debug_1.default)('hive-sdk');
|
|
12
13
|
// Type guard to check if response is an error
|
|
13
14
|
function isApiError(response) {
|
|
@@ -19,6 +20,7 @@ class HiveLogClient {
|
|
|
19
20
|
const apiSecret = config.apiSecret || process.env.HIVE_API_SECRET;
|
|
20
21
|
const host = config.host || process.env.HIVE_HOST || 'https://www.forgehive.cloud';
|
|
21
22
|
this.projectName = config.projectName;
|
|
23
|
+
this.projectUuid = config.projectUuid || null;
|
|
22
24
|
this.baseMetadata = config.metadata || {};
|
|
23
25
|
if (!apiKey || !apiSecret) {
|
|
24
26
|
this.apiKey = null;
|
|
@@ -58,19 +60,21 @@ class HiveLogClient {
|
|
|
58
60
|
log('Silent mode: Skipping sendLog for task "%s" - client not initialized', taskName);
|
|
59
61
|
return 'silent';
|
|
60
62
|
}
|
|
63
|
+
// Deprecation warning for legacy endpoint
|
|
64
|
+
log('DEPRECATION WARNING: sendLog() is deprecated. Use sendLogByUuid() with project and task UUIDs for enhanced features and better performance.');
|
|
61
65
|
try {
|
|
62
66
|
const logsUrl = `${this.host}/api/tasks/log-ingest`;
|
|
63
67
|
log('Sending log for task "%s" to %s', taskName, logsUrl);
|
|
64
68
|
const authToken = `${this.apiKey}:${this.apiSecret}`;
|
|
65
69
|
// Merge metadata with priority: sendLog > record.metadata > client
|
|
66
70
|
const finalMetadata = this.mergeMetadata(record, metadata);
|
|
67
|
-
// Create logItem with merged metadata
|
|
71
|
+
// Create logItem with merged metadata (no UUID generation for legacy method)
|
|
68
72
|
const logItem = {
|
|
69
73
|
...record,
|
|
70
74
|
taskName,
|
|
71
75
|
metadata: finalMetadata
|
|
72
76
|
};
|
|
73
|
-
await axios_1.default.post(logsUrl, {
|
|
77
|
+
const response = await axios_1.default.post(logsUrl, {
|
|
74
78
|
projectName: this.projectName,
|
|
75
79
|
taskName,
|
|
76
80
|
logItem: JSON.stringify(logItem)
|
|
@@ -81,6 +85,10 @@ class HiveLogClient {
|
|
|
81
85
|
}
|
|
82
86
|
});
|
|
83
87
|
log('Success: Sent log for task "%s"', taskName);
|
|
88
|
+
// Return the full response data if available
|
|
89
|
+
if (response.data && typeof response.data === 'object' && 'uuid' in response.data) {
|
|
90
|
+
return response.data;
|
|
91
|
+
}
|
|
84
92
|
return 'success';
|
|
85
93
|
}
|
|
86
94
|
catch (e) {
|
|
@@ -89,6 +97,52 @@ class HiveLogClient {
|
|
|
89
97
|
return 'error';
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
async sendLogByUuid(record, taskUuid, metadata) {
|
|
101
|
+
if (!this.isInitialized) {
|
|
102
|
+
log('Silent mode: Skipping sendLogByUuid for task UUID "%s" - client not initialized', taskUuid);
|
|
103
|
+
return 'silent';
|
|
104
|
+
}
|
|
105
|
+
if (!this.projectUuid) {
|
|
106
|
+
log('Error: sendLogByUuid requires projectUuid to be set in client config');
|
|
107
|
+
return 'error';
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const logsUrl = `${this.host}/api/log-ingest`;
|
|
111
|
+
log('Sending log for task UUID "%s" to %s', taskUuid, logsUrl);
|
|
112
|
+
const authToken = `${this.apiKey}:${this.apiSecret}`;
|
|
113
|
+
// Merge metadata with priority: sendLog > record.metadata > client
|
|
114
|
+
const finalMetadata = this.mergeMetadata(record, metadata);
|
|
115
|
+
// Ensure execution record has a UUID - generate one if missing
|
|
116
|
+
const recordWithUuid = {
|
|
117
|
+
...record,
|
|
118
|
+
uuid: record.uuid || (0, uuid_1.v7)(),
|
|
119
|
+
metadata: finalMetadata
|
|
120
|
+
};
|
|
121
|
+
// Create logItem with merged metadata and UUID
|
|
122
|
+
const logItem = recordWithUuid;
|
|
123
|
+
const response = await axios_1.default.post(logsUrl, {
|
|
124
|
+
projectUuid: this.projectUuid,
|
|
125
|
+
taskUuid,
|
|
126
|
+
logItem: JSON.stringify(logItem)
|
|
127
|
+
}, {
|
|
128
|
+
headers: {
|
|
129
|
+
Authorization: `Bearer ${authToken}`,
|
|
130
|
+
'Content-Type': 'application/json'
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
log('Success: Sent log for task UUID "%s"', taskUuid);
|
|
134
|
+
// Return the full response data if available
|
|
135
|
+
if (response.data && typeof response.data === 'object' && 'uuid' in response.data) {
|
|
136
|
+
return response.data;
|
|
137
|
+
}
|
|
138
|
+
return 'success';
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
const error = e;
|
|
142
|
+
log('Error: Failed to send log for task UUID "%s": %s', taskUuid, error.message);
|
|
143
|
+
return 'error';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
92
146
|
getListener() {
|
|
93
147
|
return async (record) => {
|
|
94
148
|
await this.sendLog(record);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
const axios_1 = __importDefault(require("axios"));
|
|
7
|
+
const index_1 = require("../index");
|
|
8
|
+
// Mock axios
|
|
9
|
+
jest.mock('axios');
|
|
10
|
+
const mockedAxios = axios_1.default;
|
|
11
|
+
describe('HiveLogClient sendLogByUuid', () => {
|
|
12
|
+
const testConfig = {
|
|
13
|
+
projectName: 'test-project',
|
|
14
|
+
projectUuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
15
|
+
apiKey: 'test-key',
|
|
16
|
+
apiSecret: 'test-secret',
|
|
17
|
+
host: 'https://test.example.com'
|
|
18
|
+
};
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('UUID Generation', () => {
|
|
23
|
+
it('should generate UUID v7 when execution record has no UUID', async () => {
|
|
24
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
25
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
26
|
+
const executionRecord = {
|
|
27
|
+
input: { value: 'test-input' },
|
|
28
|
+
output: { result: 'test-output' },
|
|
29
|
+
taskName: 'test-task',
|
|
30
|
+
type: 'success',
|
|
31
|
+
boundaries: {},
|
|
32
|
+
metadata: {}
|
|
33
|
+
};
|
|
34
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
35
|
+
expect(result).toBe('success');
|
|
36
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
37
|
+
// Check that the request was made with a UUID
|
|
38
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
39
|
+
const requestBody = callArgs[1];
|
|
40
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
41
|
+
expect(logItem.uuid).toBeDefined();
|
|
42
|
+
expect(typeof logItem.uuid).toBe('string');
|
|
43
|
+
expect(logItem.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); // UUID v7 pattern
|
|
44
|
+
});
|
|
45
|
+
it('should preserve existing UUID when execution record already has one', async () => {
|
|
46
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
47
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
48
|
+
const existingUuid = '01234567-89ab-7def-8123-456789abcdef';
|
|
49
|
+
const executionRecord = {
|
|
50
|
+
uuid: existingUuid,
|
|
51
|
+
input: { value: 'test-input' },
|
|
52
|
+
output: { result: 'test-output' },
|
|
53
|
+
taskName: 'test-task',
|
|
54
|
+
type: 'success',
|
|
55
|
+
boundaries: {},
|
|
56
|
+
metadata: {}
|
|
57
|
+
};
|
|
58
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
59
|
+
expect(result).toBe('success');
|
|
60
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
61
|
+
// Check that the existing UUID was preserved
|
|
62
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
63
|
+
const requestBody = callArgs[1];
|
|
64
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
65
|
+
expect(logItem.uuid).toBe(existingUuid);
|
|
66
|
+
});
|
|
67
|
+
it('should generate UUID v7 when execution record has empty UUID', async () => {
|
|
68
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
69
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
70
|
+
const executionRecord = {
|
|
71
|
+
uuid: '', // Empty string should trigger UUID generation
|
|
72
|
+
input: { value: 'test-input' },
|
|
73
|
+
output: { result: 'test-output' },
|
|
74
|
+
taskName: 'test-task',
|
|
75
|
+
type: 'success',
|
|
76
|
+
boundaries: {},
|
|
77
|
+
metadata: {}
|
|
78
|
+
};
|
|
79
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
80
|
+
expect(result).toBe('success');
|
|
81
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
82
|
+
// Check that a new UUID was generated (not empty string)
|
|
83
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
84
|
+
const requestBody = callArgs[1];
|
|
85
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
86
|
+
expect(logItem.uuid).toBeDefined();
|
|
87
|
+
expect(logItem.uuid).not.toBe('');
|
|
88
|
+
expect(typeof logItem.uuid).toBe('string');
|
|
89
|
+
expect(logItem.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); // UUID v7 pattern
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('API Integration', () => {
|
|
93
|
+
it('should send correct request to log-ingest endpoint', async () => {
|
|
94
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
95
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
96
|
+
const taskUuid = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
|
|
97
|
+
const executionRecord = {
|
|
98
|
+
input: { userId: 123 },
|
|
99
|
+
output: { result: 'processed' },
|
|
100
|
+
taskName: 'process-user',
|
|
101
|
+
type: 'success',
|
|
102
|
+
boundaries: {},
|
|
103
|
+
metadata: { session: 'abc123' }
|
|
104
|
+
};
|
|
105
|
+
await client.sendLogByUuid(executionRecord, taskUuid);
|
|
106
|
+
expect(mockedAxios.post).toHaveBeenCalledWith('https://test.example.com/api/log-ingest', {
|
|
107
|
+
projectUuid: testConfig.projectUuid,
|
|
108
|
+
taskUuid: taskUuid,
|
|
109
|
+
logItem: expect.any(String)
|
|
110
|
+
}, {
|
|
111
|
+
headers: {
|
|
112
|
+
'Authorization': 'Bearer test-key:test-secret',
|
|
113
|
+
'Content-Type': 'application/json'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// Verify the logItem contains the execution record with UUID
|
|
117
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
118
|
+
const requestBody = callArgs[1];
|
|
119
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
120
|
+
expect(logItem.uuid).toBeDefined();
|
|
121
|
+
expect(logItem.input).toEqual({ userId: 123 });
|
|
122
|
+
expect(logItem.output).toEqual({ result: 'processed' });
|
|
123
|
+
expect(logItem.taskName).toBe('process-user');
|
|
124
|
+
expect(logItem.type).toBe('success');
|
|
125
|
+
expect(logItem.metadata).toEqual({ session: 'abc123' });
|
|
126
|
+
});
|
|
127
|
+
it('should return "silent" when client is not initialized', async () => {
|
|
128
|
+
const client = new index_1.HiveLogClient({
|
|
129
|
+
projectName: 'test-project',
|
|
130
|
+
projectUuid: testConfig.projectUuid
|
|
131
|
+
// Missing apiKey and apiSecret
|
|
132
|
+
});
|
|
133
|
+
const executionRecord = {
|
|
134
|
+
input: { value: 'test' },
|
|
135
|
+
taskName: 'test-task',
|
|
136
|
+
type: 'success',
|
|
137
|
+
boundaries: {},
|
|
138
|
+
metadata: {}
|
|
139
|
+
};
|
|
140
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
141
|
+
expect(result).toBe('silent');
|
|
142
|
+
expect(mockedAxios.post).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
it('should return "error" when projectUuid is missing', async () => {
|
|
145
|
+
const client = new index_1.HiveLogClient({
|
|
146
|
+
projectName: 'test-project',
|
|
147
|
+
apiKey: 'test-key',
|
|
148
|
+
apiSecret: 'test-secret',
|
|
149
|
+
host: 'https://test.example.com'
|
|
150
|
+
// Missing projectUuid
|
|
151
|
+
});
|
|
152
|
+
const executionRecord = {
|
|
153
|
+
input: { value: 'test' },
|
|
154
|
+
taskName: 'test-task',
|
|
155
|
+
type: 'success',
|
|
156
|
+
boundaries: {},
|
|
157
|
+
metadata: {}
|
|
158
|
+
};
|
|
159
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
160
|
+
expect(result).toBe('error');
|
|
161
|
+
expect(mockedAxios.post).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
it('should handle API error responses', async () => {
|
|
164
|
+
mockedAxios.post.mockRejectedValueOnce(new Error('Network error'));
|
|
165
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
166
|
+
const executionRecord = {
|
|
167
|
+
input: { value: 'test' },
|
|
168
|
+
taskName: 'test-task',
|
|
169
|
+
type: 'success',
|
|
170
|
+
boundaries: {},
|
|
171
|
+
metadata: {}
|
|
172
|
+
};
|
|
173
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
174
|
+
expect(result).toBe('error');
|
|
175
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
it('should return response data when API returns object with uuid', async () => {
|
|
178
|
+
const responseData = { uuid: 'log-uuid-123', success: true };
|
|
179
|
+
mockedAxios.post.mockResolvedValueOnce({ data: responseData });
|
|
180
|
+
const client = new index_1.HiveLogClient(testConfig);
|
|
181
|
+
const executionRecord = {
|
|
182
|
+
input: { value: 'test' },
|
|
183
|
+
taskName: 'test-task',
|
|
184
|
+
type: 'success',
|
|
185
|
+
boundaries: {},
|
|
186
|
+
metadata: {}
|
|
187
|
+
};
|
|
188
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123');
|
|
189
|
+
expect(result).toEqual(responseData);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('Metadata Handling', () => {
|
|
193
|
+
it('should merge metadata correctly with UUID generation', async () => {
|
|
194
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
|
195
|
+
const baseMetadata = { environment: 'test', version: '1.0' };
|
|
196
|
+
const client = new index_1.HiveLogClient({ ...testConfig, metadata: baseMetadata });
|
|
197
|
+
const recordMetadata = { session: 'abc123' };
|
|
198
|
+
const sendLogMetadata = { priority: 'high' };
|
|
199
|
+
const executionRecord = {
|
|
200
|
+
input: { value: 'test' },
|
|
201
|
+
taskName: 'test-task',
|
|
202
|
+
type: 'success',
|
|
203
|
+
boundaries: {},
|
|
204
|
+
metadata: recordMetadata
|
|
205
|
+
};
|
|
206
|
+
await client.sendLogByUuid(executionRecord, 'task-uuid-123', sendLogMetadata);
|
|
207
|
+
const callArgs = mockedAxios.post.mock.calls[0];
|
|
208
|
+
const requestBody = callArgs[1];
|
|
209
|
+
const logItem = JSON.parse(requestBody.logItem);
|
|
210
|
+
// Should have UUID generated
|
|
211
|
+
expect(logItem.uuid).toBeDefined();
|
|
212
|
+
expect(typeof logItem.uuid).toBe('string');
|
|
213
|
+
// Should have merged metadata (sendLog > record > client)
|
|
214
|
+
expect(logItem.metadata).toEqual({
|
|
215
|
+
environment: 'test', // from client
|
|
216
|
+
version: '1.0', // from client
|
|
217
|
+
session: 'abc123', // from record
|
|
218
|
+
priority: 'high' // from sendLog (highest priority)
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgehive/hive-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@forgehive/task": "^0.2.
|
|
10
|
+
"@forgehive/task": "^0.2.6"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/jest": "^29.5.14",
|
|
15
15
|
"@types/node": "^24.0.3",
|
|
16
|
+
"@types/uuid": "^10.0.0",
|
|
16
17
|
"jest": "^29.7.0",
|
|
17
18
|
"ts-jest": "^29.1.2"
|
|
18
19
|
},
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"axios": "^1.8.4",
|
|
24
25
|
"debug": "^4.4.1",
|
|
25
|
-
"
|
|
26
|
+
"uuid": "^11.1.0",
|
|
27
|
+
"@forgehive/task": "0.2.6"
|
|
26
28
|
},
|
|
27
29
|
"scripts": {
|
|
28
30
|
"build": "tsc",
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
2
|
import debug from 'debug'
|
|
3
|
+
import { v7 as uuidv7 } from 'uuid'
|
|
3
4
|
import type { ExecutionRecord } from '@forgehive/task'
|
|
4
5
|
|
|
5
6
|
const log = debug('hive-sdk')
|
|
@@ -15,6 +16,7 @@ export type { ExecutionRecord } from '@forgehive/task'
|
|
|
15
16
|
// Configuration interface for HiveLogClient
|
|
16
17
|
export interface HiveLogClientConfig {
|
|
17
18
|
projectName: string
|
|
19
|
+
projectUuid?: string // Optional UUID for new endpoint
|
|
18
20
|
apiKey?: string
|
|
19
21
|
apiSecret?: string
|
|
20
22
|
host?: string
|
|
@@ -56,6 +58,7 @@ export class HiveLogClient {
|
|
|
56
58
|
private apiSecret: string | null
|
|
57
59
|
private host: string | null
|
|
58
60
|
private projectName: string
|
|
61
|
+
private projectUuid: string | null
|
|
59
62
|
private baseMetadata: Metadata
|
|
60
63
|
private isInitialized: boolean
|
|
61
64
|
|
|
@@ -65,6 +68,7 @@ export class HiveLogClient {
|
|
|
65
68
|
const host = config.host || process.env.HIVE_HOST || 'https://www.forgehive.cloud'
|
|
66
69
|
|
|
67
70
|
this.projectName = config.projectName
|
|
71
|
+
this.projectUuid = config.projectUuid || null
|
|
68
72
|
this.baseMetadata = config.metadata || {}
|
|
69
73
|
|
|
70
74
|
if (!apiKey || !apiSecret) {
|
|
@@ -103,7 +107,7 @@ export class HiveLogClient {
|
|
|
103
107
|
return finalMetadata
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
async sendLog(record: ExecutionRecord, metadata?: Metadata): Promise<'success' | 'error' | 'silent'> {
|
|
110
|
+
async sendLog(record: ExecutionRecord, metadata?: Metadata): Promise<'success' | 'error' | 'silent' | LogApiSuccess> {
|
|
107
111
|
// Extract taskName from record
|
|
108
112
|
const taskName = record.taskName || 'unknown-task'
|
|
109
113
|
|
|
@@ -112,6 +116,9 @@ export class HiveLogClient {
|
|
|
112
116
|
return 'silent'
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
// Deprecation warning for legacy endpoint
|
|
120
|
+
log('DEPRECATION WARNING: sendLog() is deprecated. Use sendLogByUuid() with project and task UUIDs for enhanced features and better performance.')
|
|
121
|
+
|
|
115
122
|
try {
|
|
116
123
|
const logsUrl = `${this.host}/api/tasks/log-ingest`
|
|
117
124
|
log('Sending log for task "%s" to %s', taskName, logsUrl)
|
|
@@ -121,14 +128,14 @@ export class HiveLogClient {
|
|
|
121
128
|
// Merge metadata with priority: sendLog > record.metadata > client
|
|
122
129
|
const finalMetadata = this.mergeMetadata(record, metadata)
|
|
123
130
|
|
|
124
|
-
// Create logItem with merged metadata
|
|
131
|
+
// Create logItem with merged metadata (no UUID generation for legacy method)
|
|
125
132
|
const logItem = {
|
|
126
133
|
...record,
|
|
127
134
|
taskName,
|
|
128
135
|
metadata: finalMetadata
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
await axios.post(logsUrl, {
|
|
138
|
+
const response = await axios.post(logsUrl, {
|
|
132
139
|
projectName: this.projectName,
|
|
133
140
|
taskName,
|
|
134
141
|
logItem: JSON.stringify(logItem)
|
|
@@ -140,6 +147,12 @@ export class HiveLogClient {
|
|
|
140
147
|
})
|
|
141
148
|
|
|
142
149
|
log('Success: Sent log for task "%s"', taskName)
|
|
150
|
+
|
|
151
|
+
// Return the full response data if available
|
|
152
|
+
if (response.data && typeof response.data === 'object' && 'uuid' in response.data) {
|
|
153
|
+
return response.data as LogApiSuccess
|
|
154
|
+
}
|
|
155
|
+
|
|
143
156
|
return 'success'
|
|
144
157
|
} catch (e) {
|
|
145
158
|
const error = e as Error
|
|
@@ -148,6 +161,62 @@ export class HiveLogClient {
|
|
|
148
161
|
}
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
async sendLogByUuid(record: ExecutionRecord, taskUuid: string, metadata?: Metadata): Promise<'success' | 'error' | 'silent' | LogApiSuccess> {
|
|
165
|
+
if (!this.isInitialized) {
|
|
166
|
+
log('Silent mode: Skipping sendLogByUuid for task UUID "%s" - client not initialized', taskUuid)
|
|
167
|
+
return 'silent'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this.projectUuid) {
|
|
171
|
+
log('Error: sendLogByUuid requires projectUuid to be set in client config')
|
|
172
|
+
return 'error'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const logsUrl = `${this.host}/api/log-ingest`
|
|
177
|
+
log('Sending log for task UUID "%s" to %s', taskUuid, logsUrl)
|
|
178
|
+
|
|
179
|
+
const authToken = `${this.apiKey}:${this.apiSecret}`
|
|
180
|
+
|
|
181
|
+
// Merge metadata with priority: sendLog > record.metadata > client
|
|
182
|
+
const finalMetadata = this.mergeMetadata(record, metadata)
|
|
183
|
+
|
|
184
|
+
// Ensure execution record has a UUID - generate one if missing
|
|
185
|
+
const recordWithUuid = {
|
|
186
|
+
...record,
|
|
187
|
+
uuid: record.uuid || uuidv7(),
|
|
188
|
+
metadata: finalMetadata
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create logItem with merged metadata and UUID
|
|
192
|
+
const logItem = recordWithUuid
|
|
193
|
+
|
|
194
|
+
const response = await axios.post(logsUrl, {
|
|
195
|
+
projectUuid: this.projectUuid,
|
|
196
|
+
taskUuid,
|
|
197
|
+
logItem: JSON.stringify(logItem)
|
|
198
|
+
}, {
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `Bearer ${authToken}`,
|
|
201
|
+
'Content-Type': 'application/json'
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
log('Success: Sent log for task UUID "%s"', taskUuid)
|
|
206
|
+
|
|
207
|
+
// Return the full response data if available
|
|
208
|
+
if (response.data && typeof response.data === 'object' && 'uuid' in response.data) {
|
|
209
|
+
return response.data as LogApiSuccess
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return 'success'
|
|
213
|
+
} catch (e) {
|
|
214
|
+
const error = e as Error
|
|
215
|
+
log('Error: Failed to send log for task UUID "%s": %s', taskUuid, error.message)
|
|
216
|
+
return 'error'
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
151
220
|
getListener(): (record: ExecutionRecord) => Promise<void> {
|
|
152
221
|
return async (record: ExecutionRecord) => {
|
|
153
222
|
await this.sendLog(record)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { HiveLogClient } from '../index'
|
|
3
|
+
|
|
4
|
+
// Mock axios
|
|
5
|
+
jest.mock('axios')
|
|
6
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>
|
|
7
|
+
|
|
8
|
+
describe('HiveLogClient sendLogByUuid', () => {
|
|
9
|
+
const testConfig = {
|
|
10
|
+
projectName: 'test-project',
|
|
11
|
+
projectUuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
12
|
+
apiKey: 'test-key',
|
|
13
|
+
apiSecret: 'test-secret',
|
|
14
|
+
host: 'https://test.example.com'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('UUID Generation', () => {
|
|
22
|
+
it('should generate UUID v7 when execution record has no UUID', async () => {
|
|
23
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } })
|
|
24
|
+
|
|
25
|
+
const client = new HiveLogClient(testConfig)
|
|
26
|
+
const executionRecord = {
|
|
27
|
+
input: { value: 'test-input' },
|
|
28
|
+
output: { result: 'test-output' },
|
|
29
|
+
taskName: 'test-task',
|
|
30
|
+
type: 'success' as const,
|
|
31
|
+
boundaries: {},
|
|
32
|
+
metadata: {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123')
|
|
36
|
+
|
|
37
|
+
expect(result).toBe('success')
|
|
38
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
|
|
39
|
+
|
|
40
|
+
// Check that the request was made with a UUID
|
|
41
|
+
const callArgs = mockedAxios.post.mock.calls[0]
|
|
42
|
+
const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
|
|
43
|
+
const logItem = JSON.parse(requestBody.logItem)
|
|
44
|
+
|
|
45
|
+
expect(logItem.uuid).toBeDefined()
|
|
46
|
+
expect(typeof logItem.uuid).toBe('string')
|
|
47
|
+
expect(logItem.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) // UUID v7 pattern
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should preserve existing UUID when execution record already has one', async () => {
|
|
51
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } })
|
|
52
|
+
|
|
53
|
+
const client = new HiveLogClient(testConfig)
|
|
54
|
+
const existingUuid = '01234567-89ab-7def-8123-456789abcdef'
|
|
55
|
+
const executionRecord = {
|
|
56
|
+
uuid: existingUuid,
|
|
57
|
+
input: { value: 'test-input' },
|
|
58
|
+
output: { result: 'test-output' },
|
|
59
|
+
taskName: 'test-task',
|
|
60
|
+
type: 'success' as const,
|
|
61
|
+
boundaries: {},
|
|
62
|
+
metadata: {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123')
|
|
66
|
+
|
|
67
|
+
expect(result).toBe('success')
|
|
68
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
|
|
69
|
+
|
|
70
|
+
// Check that the existing UUID was preserved
|
|
71
|
+
const callArgs = mockedAxios.post.mock.calls[0]
|
|
72
|
+
const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
|
|
73
|
+
const logItem = JSON.parse(requestBody.logItem)
|
|
74
|
+
|
|
75
|
+
expect(logItem.uuid).toBe(existingUuid)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should generate UUID v7 when execution record has empty UUID', async () => {
|
|
79
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } })
|
|
80
|
+
|
|
81
|
+
const client = new HiveLogClient(testConfig)
|
|
82
|
+
const executionRecord = {
|
|
83
|
+
uuid: '', // Empty string should trigger UUID generation
|
|
84
|
+
input: { value: 'test-input' },
|
|
85
|
+
output: { result: 'test-output' },
|
|
86
|
+
taskName: 'test-task',
|
|
87
|
+
type: 'success' as const,
|
|
88
|
+
boundaries: {},
|
|
89
|
+
metadata: {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123')
|
|
93
|
+
|
|
94
|
+
expect(result).toBe('success')
|
|
95
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
|
|
96
|
+
|
|
97
|
+
// Check that a new UUID was generated (not empty string)
|
|
98
|
+
const callArgs = mockedAxios.post.mock.calls[0]
|
|
99
|
+
const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
|
|
100
|
+
const logItem = JSON.parse(requestBody.logItem)
|
|
101
|
+
|
|
102
|
+
expect(logItem.uuid).toBeDefined()
|
|
103
|
+
expect(logItem.uuid).not.toBe('')
|
|
104
|
+
expect(typeof logItem.uuid).toBe('string')
|
|
105
|
+
expect(logItem.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) // UUID v7 pattern
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('API Integration', () => {
|
|
110
|
+
it('should send correct request to log-ingest endpoint', async () => {
|
|
111
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } })
|
|
112
|
+
|
|
113
|
+
const client = new HiveLogClient(testConfig)
|
|
114
|
+
const taskUuid = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
|
115
|
+
const executionRecord = {
|
|
116
|
+
input: { userId: 123 },
|
|
117
|
+
output: { result: 'processed' },
|
|
118
|
+
taskName: 'process-user',
|
|
119
|
+
type: 'success' as const,
|
|
120
|
+
boundaries: {},
|
|
121
|
+
metadata: { session: 'abc123' }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await client.sendLogByUuid(executionRecord, taskUuid)
|
|
125
|
+
|
|
126
|
+
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
127
|
+
'https://test.example.com/api/log-ingest',
|
|
128
|
+
{
|
|
129
|
+
projectUuid: testConfig.projectUuid,
|
|
130
|
+
taskUuid: taskUuid,
|
|
131
|
+
logItem: expect.any(String)
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
headers: {
|
|
135
|
+
'Authorization': 'Bearer test-key:test-secret',
|
|
136
|
+
'Content-Type': 'application/json'
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// Verify the logItem contains the execution record with UUID
|
|
142
|
+
const callArgs = mockedAxios.post.mock.calls[0]
|
|
143
|
+
const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
|
|
144
|
+
const logItem = JSON.parse(requestBody.logItem)
|
|
145
|
+
|
|
146
|
+
expect(logItem.uuid).toBeDefined()
|
|
147
|
+
expect(logItem.input).toEqual({ userId: 123 })
|
|
148
|
+
expect(logItem.output).toEqual({ result: 'processed' })
|
|
149
|
+
expect(logItem.taskName).toBe('process-user')
|
|
150
|
+
expect(logItem.type).toBe('success')
|
|
151
|
+
expect(logItem.metadata).toEqual({ session: 'abc123' })
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should return "silent" when client is not initialized', async () => {
|
|
155
|
+
const client = new HiveLogClient({
|
|
156
|
+
projectName: 'test-project',
|
|
157
|
+
projectUuid: testConfig.projectUuid
|
|
158
|
+
// Missing apiKey and apiSecret
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const executionRecord = {
|
|
162
|
+
input: { value: 'test' },
|
|
163
|
+
taskName: 'test-task',
|
|
164
|
+
type: 'success' as const,
|
|
165
|
+
boundaries: {},
|
|
166
|
+
metadata: {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123')
|
|
170
|
+
|
|
171
|
+
expect(result).toBe('silent')
|
|
172
|
+
expect(mockedAxios.post).not.toHaveBeenCalled()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should return "error" when projectUuid is missing', async () => {
|
|
176
|
+
const client = new HiveLogClient({
|
|
177
|
+
projectName: 'test-project',
|
|
178
|
+
apiKey: 'test-key',
|
|
179
|
+
apiSecret: 'test-secret',
|
|
180
|
+
host: 'https://test.example.com'
|
|
181
|
+
// Missing projectUuid
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const executionRecord = {
|
|
185
|
+
input: { value: 'test' },
|
|
186
|
+
taskName: 'test-task',
|
|
187
|
+
type: 'success' as const,
|
|
188
|
+
boundaries: {},
|
|
189
|
+
metadata: {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123')
|
|
193
|
+
|
|
194
|
+
expect(result).toBe('error')
|
|
195
|
+
expect(mockedAxios.post).not.toHaveBeenCalled()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should handle API error responses', async () => {
|
|
199
|
+
mockedAxios.post.mockRejectedValueOnce(new Error('Network error'))
|
|
200
|
+
|
|
201
|
+
const client = new HiveLogClient(testConfig)
|
|
202
|
+
const executionRecord = {
|
|
203
|
+
input: { value: 'test' },
|
|
204
|
+
taskName: 'test-task',
|
|
205
|
+
type: 'success' as const,
|
|
206
|
+
boundaries: {},
|
|
207
|
+
metadata: {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123')
|
|
211
|
+
|
|
212
|
+
expect(result).toBe('error')
|
|
213
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should return response data when API returns object with uuid', async () => {
|
|
217
|
+
const responseData = { uuid: 'log-uuid-123', success: true }
|
|
218
|
+
mockedAxios.post.mockResolvedValueOnce({ data: responseData })
|
|
219
|
+
|
|
220
|
+
const client = new HiveLogClient(testConfig)
|
|
221
|
+
const executionRecord = {
|
|
222
|
+
input: { value: 'test' },
|
|
223
|
+
taskName: 'test-task',
|
|
224
|
+
type: 'success' as const,
|
|
225
|
+
boundaries: {},
|
|
226
|
+
metadata: {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = await client.sendLogByUuid(executionRecord, 'task-uuid-123')
|
|
230
|
+
|
|
231
|
+
expect(result).toEqual(responseData)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('Metadata Handling', () => {
|
|
236
|
+
it('should merge metadata correctly with UUID generation', async () => {
|
|
237
|
+
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } })
|
|
238
|
+
|
|
239
|
+
const baseMetadata = { environment: 'test', version: '1.0' }
|
|
240
|
+
const client = new HiveLogClient({ ...testConfig, metadata: baseMetadata })
|
|
241
|
+
|
|
242
|
+
const recordMetadata = { session: 'abc123' }
|
|
243
|
+
const sendLogMetadata = { priority: 'high' }
|
|
244
|
+
|
|
245
|
+
const executionRecord = {
|
|
246
|
+
input: { value: 'test' },
|
|
247
|
+
taskName: 'test-task',
|
|
248
|
+
type: 'success' as const,
|
|
249
|
+
boundaries: {},
|
|
250
|
+
metadata: recordMetadata
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await client.sendLogByUuid(executionRecord, 'task-uuid-123', sendLogMetadata)
|
|
254
|
+
|
|
255
|
+
const callArgs = mockedAxios.post.mock.calls[0]
|
|
256
|
+
const requestBody = callArgs[1] as { projectUuid: string; taskUuid: string; logItem: string }
|
|
257
|
+
const logItem = JSON.parse(requestBody.logItem)
|
|
258
|
+
|
|
259
|
+
// Should have UUID generated
|
|
260
|
+
expect(logItem.uuid).toBeDefined()
|
|
261
|
+
expect(typeof logItem.uuid).toBe('string')
|
|
262
|
+
|
|
263
|
+
// Should have merged metadata (sendLog > record > client)
|
|
264
|
+
expect(logItem.metadata).toEqual({
|
|
265
|
+
environment: 'test', // from client
|
|
266
|
+
version: '1.0', // from client
|
|
267
|
+
session: 'abc123', // from record
|
|
268
|
+
priority: 'high' // from sendLog (highest priority)
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
})
|