@output.ai/core 0.4.2 → 0.4.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/package.json +1 -1
- package/src/tracing/processors/s3/configs.js +29 -0
- package/src/tracing/processors/s3/configs.spec.js +64 -0
- package/src/tracing/processors/s3/index.js +52 -14
- package/src/tracing/processors/s3/index.spec.js +44 -17
- package/src/tracing/processors/s3/redis_client.js +2 -3
- package/src/tracing/processors/s3/redis_client.spec.js +12 -8
- package/src/tracing/processors/s3/s3_client.js +3 -9
- package/src/tracing/processors/s3/s3_client.spec.js +10 -15
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
const envVarSchema = z.object( {
|
|
4
|
+
OUTPUT_AWS_REGION: z.string(),
|
|
5
|
+
OUTPUT_AWS_ACCESS_KEY_ID: z.string(),
|
|
6
|
+
OUTPUT_AWS_SECRET_ACCESS_KEY: z.string(),
|
|
7
|
+
OUTPUT_TRACE_REMOTE_S3_BUCKET: z.string(),
|
|
8
|
+
OUTPUT_REDIS_URL: z.string(),
|
|
9
|
+
OUTPUT_REDIS_TRACE_TTL: z.coerce.number().int().positive().default( 60 * 60 * 24 * 7 ) // 7 days
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
const env = {};
|
|
13
|
+
|
|
14
|
+
export const loadEnv = () => {
|
|
15
|
+
const parsedFields = envVarSchema.parse( process.env );
|
|
16
|
+
env.awsRegion = parsedFields.OUTPUT_AWS_REGION;
|
|
17
|
+
env.awsAccessKeyId = parsedFields.OUTPUT_AWS_ACCESS_KEY_ID;
|
|
18
|
+
env.awsSecretAccessKey = parsedFields.OUTPUT_AWS_SECRET_ACCESS_KEY;
|
|
19
|
+
env.remoteS3Bucket = parsedFields.OUTPUT_TRACE_REMOTE_S3_BUCKET;
|
|
20
|
+
env.redisUrl = parsedFields.OUTPUT_REDIS_URL;
|
|
21
|
+
env.redisIncompleteWorkflowsTTL = parsedFields.OUTPUT_REDIS_TRACE_TTL;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getVars = () => {
|
|
25
|
+
if ( Object.keys( env ).length === 0 ) {
|
|
26
|
+
throw new Error( 'Env vars not loaded. Use loadEnv() first.' );
|
|
27
|
+
}
|
|
28
|
+
return env;
|
|
29
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
async function loadModule() {
|
|
4
|
+
vi.resetModules();
|
|
5
|
+
return import( './configs.js' );
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe( 'tracing/processors/s3/configs', () => {
|
|
9
|
+
const required = {
|
|
10
|
+
OUTPUT_AWS_REGION: 'us-east-1',
|
|
11
|
+
OUTPUT_AWS_ACCESS_KEY_ID: 'id',
|
|
12
|
+
OUTPUT_AWS_SECRET_ACCESS_KEY: 'sek',
|
|
13
|
+
OUTPUT_TRACE_REMOTE_S3_BUCKET: 'bkt',
|
|
14
|
+
OUTPUT_REDIS_URL: 'redis://localhost:6379'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach( () => {
|
|
18
|
+
vi.stubEnv( 'OUTPUT_AWS_REGION', required.OUTPUT_AWS_REGION );
|
|
19
|
+
vi.stubEnv( 'OUTPUT_AWS_ACCESS_KEY_ID', required.OUTPUT_AWS_ACCESS_KEY_ID );
|
|
20
|
+
vi.stubEnv( 'OUTPUT_AWS_SECRET_ACCESS_KEY', required.OUTPUT_AWS_SECRET_ACCESS_KEY );
|
|
21
|
+
vi.stubEnv( 'OUTPUT_TRACE_REMOTE_S3_BUCKET', required.OUTPUT_TRACE_REMOTE_S3_BUCKET );
|
|
22
|
+
vi.stubEnv( 'OUTPUT_REDIS_URL', required.OUTPUT_REDIS_URL );
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
afterEach( () => {
|
|
26
|
+
vi.unstubAllEnvs();
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
it( 'loadEnv() throws when required env vars are missing', async () => {
|
|
30
|
+
vi.stubEnv( 'OUTPUT_REDIS_URL', undefined );
|
|
31
|
+
const { loadEnv } = await loadModule();
|
|
32
|
+
expect( () => loadEnv() ).toThrow( /OUTPUT_REDIS_URL/ );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
it( 'loadEnv() populates getVars() with parsed env', async () => {
|
|
36
|
+
const { loadEnv, getVars } = await loadModule();
|
|
37
|
+
loadEnv();
|
|
38
|
+
const vars = getVars();
|
|
39
|
+
expect( vars.awsRegion ).toBe( required.OUTPUT_AWS_REGION );
|
|
40
|
+
expect( vars.awsAccessKeyId ).toBe( required.OUTPUT_AWS_ACCESS_KEY_ID );
|
|
41
|
+
expect( vars.awsSecretAccessKey ).toBe( required.OUTPUT_AWS_SECRET_ACCESS_KEY );
|
|
42
|
+
expect( vars.remoteS3Bucket ).toBe( required.OUTPUT_TRACE_REMOTE_S3_BUCKET );
|
|
43
|
+
expect( vars.redisUrl ).toBe( required.OUTPUT_REDIS_URL );
|
|
44
|
+
expect( vars.redisIncompleteWorkflowsTTL ).toBe( 60 * 60 * 24 * 7 );
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
it( 'loadEnv() uses OUTPUT_REDIS_TRACE_TTL when set', async () => {
|
|
48
|
+
vi.stubEnv( 'OUTPUT_REDIS_TRACE_TTL', '3600' );
|
|
49
|
+
const { loadEnv, getVars } = await loadModule();
|
|
50
|
+
loadEnv();
|
|
51
|
+
expect( getVars().redisIncompleteWorkflowsTTL ).toBe( 3600 );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'getVars() throws when loadEnv() was not called', async () => {
|
|
55
|
+
const { getVars } = await loadModule();
|
|
56
|
+
expect( () => getVars() ).toThrow( 'Env vars not loaded. Use loadEnv() first.' );
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
it( 'loadEnv() throws when OUTPUT_REDIS_TRACE_TTL is invalid', async () => {
|
|
60
|
+
vi.stubEnv( 'OUTPUT_REDIS_TRACE_TTL', 'not-a-number' );
|
|
61
|
+
const { loadEnv } = await loadModule();
|
|
62
|
+
expect( () => loadEnv() ).toThrow();
|
|
63
|
+
} );
|
|
64
|
+
} );
|
|
@@ -2,22 +2,51 @@ import { upload } from './s3_client.js';
|
|
|
2
2
|
import { getRedisClient } from './redis_client.js';
|
|
3
3
|
import buildTraceTree from '../../tools/build_trace_tree.js';
|
|
4
4
|
import { EOL } from 'node:os';
|
|
5
|
+
import { loadEnv, getVars } from './configs.js';
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const createRedisKey = ( { workflowId, workflowName } ) => `traces/${workflowName}/${workflowId}`;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Add new entry to list of entries
|
|
11
|
+
* @param {object} entry
|
|
12
|
+
* @param {string} key
|
|
13
|
+
*/
|
|
14
|
+
const addEntry = async ( entry, key ) => {
|
|
15
|
+
const client = await getRedisClient();
|
|
16
|
+
await client.multi()
|
|
17
|
+
.zAdd( key, [ { score: entry.timestamp, value: JSON.stringify( entry ) } ], { NX: true } )
|
|
18
|
+
.expire( key, getVars().redisIncompleteWorkflowsTTL, 'GT' )
|
|
19
|
+
.exec();
|
|
20
|
+
};
|
|
11
21
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Returns entries from cache, parsed to object
|
|
24
|
+
* @param {string} key
|
|
25
|
+
* @returns {object[]}
|
|
26
|
+
*/
|
|
27
|
+
const getEntries = async key => {
|
|
28
|
+
const client = await getRedisClient();
|
|
29
|
+
const zList = await client.zRange( key, 0, -1 );
|
|
18
30
|
return zList.map( v => JSON.parse( v ) );
|
|
19
31
|
};
|
|
20
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Removes the entries from cache
|
|
35
|
+
* @param {string} key
|
|
36
|
+
*/
|
|
37
|
+
const bustEntries = async key => {
|
|
38
|
+
const client = await getRedisClient();
|
|
39
|
+
await client.del( key );
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return the S3 key for the trace file
|
|
44
|
+
* @param {object} args
|
|
45
|
+
* @param {number} args.startTime
|
|
46
|
+
* @param {string} args.workflowId
|
|
47
|
+
* @param {string} args.workflowName
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
21
50
|
const getS3Key = ( { startTime, workflowId, workflowName } ) => {
|
|
22
51
|
const isoDate = new Date( startTime ).toISOString();
|
|
23
52
|
const [ year, month, day ] = isoDate.split( /\D/, 3 );
|
|
@@ -29,6 +58,7 @@ const getS3Key = ( { startTime, workflowId, workflowName } ) => {
|
|
|
29
58
|
* Init this processor
|
|
30
59
|
*/
|
|
31
60
|
export const init = async () => {
|
|
61
|
+
loadEnv();
|
|
32
62
|
await getRedisClient();
|
|
33
63
|
};
|
|
34
64
|
|
|
@@ -41,13 +71,21 @@ export const init = async () => {
|
|
|
41
71
|
*/
|
|
42
72
|
export const exec = async ( { entry, executionContext } ) => {
|
|
43
73
|
const { workflowName, workflowId, startTime } = executionContext;
|
|
44
|
-
const
|
|
74
|
+
const cacheKey = createRedisKey( { workflowId, workflowName } );
|
|
75
|
+
|
|
76
|
+
await addEntry( entry, cacheKey );
|
|
45
77
|
|
|
46
78
|
const isRootWorkflowEnd = !entry.parentId && entry.phase !== 'start';
|
|
47
|
-
|
|
79
|
+
if ( !isRootWorkflowEnd ) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const content = buildTraceTree( await getEntries( cacheKey ) );
|
|
84
|
+
await upload( {
|
|
48
85
|
key: getS3Key( { workflowId, workflowName, startTime } ),
|
|
49
86
|
content: JSON.stringify( content, undefined, 2 ) + EOL
|
|
50
|
-
} )
|
|
87
|
+
} );
|
|
88
|
+
await bustEntries( cacheKey );
|
|
51
89
|
};
|
|
52
90
|
|
|
53
91
|
/**
|
|
@@ -59,4 +97,4 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
59
97
|
* @returns {string} The S3 url of the trace file
|
|
60
98
|
*/
|
|
61
99
|
export const getDestination = ( { startTime, workflowId, workflowName } ) =>
|
|
62
|
-
`https://${
|
|
100
|
+
`https://${getVars().remoteS3Bucket}.s3.amazonaws.com/${getS3Key( { workflowId, workflowName, startTime } )}`;
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
|
|
3
|
+
const loadEnvMock = vi.fn();
|
|
4
|
+
const getVarsMock = vi.fn( () => ( {
|
|
5
|
+
remoteS3Bucket: 'bkt',
|
|
6
|
+
redisIncompleteWorkflowsTTL: 3600
|
|
7
|
+
} ) );
|
|
8
|
+
vi.mock( './configs.js', () => ( { loadEnv: loadEnvMock, getVars: getVarsMock } ) );
|
|
9
|
+
|
|
3
10
|
const redisMulti = {
|
|
4
11
|
zAdd: vi.fn().mockReturnThis(),
|
|
5
12
|
expire: vi.fn().mockReturnThis(),
|
|
6
|
-
zRange: vi.fn().mockReturnThis(),
|
|
7
13
|
exec: vi.fn()
|
|
8
14
|
};
|
|
9
|
-
const
|
|
15
|
+
const zRangeMock = vi.fn();
|
|
16
|
+
const delMock = vi.fn().mockResolvedValue( undefined );
|
|
17
|
+
const getRedisClientMock = vi.fn( async () => ( {
|
|
18
|
+
multi: () => redisMulti,
|
|
19
|
+
zRange: zRangeMock,
|
|
20
|
+
del: delMock
|
|
21
|
+
} ) );
|
|
10
22
|
vi.mock( './redis_client.js', () => ( { getRedisClient: getRedisClientMock } ) );
|
|
11
23
|
|
|
12
24
|
const uploadMock = vi.fn();
|
|
@@ -18,12 +30,13 @@ vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMoc
|
|
|
18
30
|
describe( 'tracing/processors/s3', () => {
|
|
19
31
|
beforeEach( () => {
|
|
20
32
|
vi.clearAllMocks();
|
|
21
|
-
|
|
33
|
+
getVarsMock.mockReturnValue( { remoteS3Bucket: 'bkt', redisIncompleteWorkflowsTTL: 3600 } );
|
|
22
34
|
} );
|
|
23
35
|
|
|
24
|
-
it( 'init(): ensures redis client is created', async () => {
|
|
36
|
+
it( 'init(): loads config and ensures redis client is created', async () => {
|
|
25
37
|
const { init } = await import( './index.js' );
|
|
26
38
|
await init();
|
|
39
|
+
expect( loadEnvMock ).toHaveBeenCalledTimes( 1 );
|
|
27
40
|
expect( getRedisClientMock ).toHaveBeenCalledTimes( 1 );
|
|
28
41
|
} );
|
|
29
42
|
|
|
@@ -32,18 +45,15 @@ describe( 'tracing/processors/s3', () => {
|
|
|
32
45
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
33
46
|
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
34
47
|
|
|
35
|
-
//
|
|
36
|
-
redisMulti.exec
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
JSON.stringify( { name: 'A', phase: 'tick', timestamp: startTime + 1 } ),
|
|
45
|
-
JSON.stringify( { name: 'A', phase: 'end', timestamp: startTime + 2 } )
|
|
46
|
-
] ] );
|
|
48
|
+
// multi().exec() just needs to resolve for addEntry calls
|
|
49
|
+
redisMulti.exec.mockResolvedValue( [] );
|
|
50
|
+
|
|
51
|
+
// zRange is only called once at the end to get all entries
|
|
52
|
+
zRangeMock.mockResolvedValue( [
|
|
53
|
+
JSON.stringify( { name: 'A', phase: 'start', timestamp: startTime } ),
|
|
54
|
+
JSON.stringify( { name: 'A', phase: 'tick', timestamp: startTime + 1 } ),
|
|
55
|
+
JSON.stringify( { name: 'A', phase: 'end', timestamp: startTime + 2 } )
|
|
56
|
+
] );
|
|
47
57
|
|
|
48
58
|
await exec( { ...ctx, entry: { name: 'A', phase: 'start', timestamp: startTime, parentId: 'root' } } );
|
|
49
59
|
await exec( { ...ctx, entry: { name: 'A', phase: 'tick', timestamp: startTime + 1, parentId: 'root' } } );
|
|
@@ -52,13 +62,30 @@ describe( 'tracing/processors/s3', () => {
|
|
|
52
62
|
|
|
53
63
|
// Accumulation happened 3 times
|
|
54
64
|
expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 3 );
|
|
55
|
-
|
|
65
|
+
|
|
66
|
+
// Tree is only built once at the end (not on every event)
|
|
67
|
+
expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
|
|
68
|
+
expect( zRangeMock ).toHaveBeenCalledTimes( 1 );
|
|
56
69
|
|
|
57
70
|
// Only last call triggers upload
|
|
58
71
|
expect( uploadMock ).toHaveBeenCalledTimes( 1 );
|
|
59
72
|
const { key, content } = uploadMock.mock.calls[0][0];
|
|
60
73
|
expect( key ).toMatch( /^WF\/2020\/01\/02\// );
|
|
61
74
|
expect( JSON.parse( content.trim() ).count ).toBe( 3 );
|
|
75
|
+
|
|
76
|
+
expect( delMock ).toHaveBeenCalledTimes( 1 );
|
|
77
|
+
expect( delMock ).toHaveBeenCalledWith( 'traces/WF/id1' );
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
it( 'getDestination(): returns S3 URL using bucket and key from getVars', async () => {
|
|
81
|
+
getVarsMock.mockReturnValue( { remoteS3Bucket: 'my-bucket', redisIncompleteWorkflowsTTL: 3600 } );
|
|
82
|
+
const { getDestination } = await import( './index.js' );
|
|
83
|
+
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
84
|
+
const url = getDestination( { workflowId: 'id1', workflowName: 'WF', startTime } );
|
|
85
|
+
expect( getVarsMock ).toHaveBeenCalled();
|
|
86
|
+
expect( url ).toBe(
|
|
87
|
+
'https://my-bucket.s3.amazonaws.com/WF/2020/01/02/2020-01-02-03-04-05-678Z_id1.json'
|
|
88
|
+
);
|
|
62
89
|
} );
|
|
63
90
|
} );
|
|
64
91
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createClient } from 'redis';
|
|
2
|
-
import { throws } from '#utils';
|
|
3
2
|
import { createChildLogger } from '#logger';
|
|
3
|
+
import { getVars } from './configs.js';
|
|
4
4
|
|
|
5
5
|
const log = createChildLogger( 'RedisClient' );
|
|
6
6
|
|
|
@@ -36,11 +36,10 @@ async function connect( url ) {
|
|
|
36
36
|
* Concurrent calls during connection will receive the same pending promise.
|
|
37
37
|
*
|
|
38
38
|
* @returns {Promise<redis.RedisClientType>} Connected Redis client
|
|
39
|
-
* @throws {Error} If OUTPUT_REDIS_URL env var is missing
|
|
40
39
|
* @throws {Error} If connection fails (wrapped with context)
|
|
41
40
|
*/
|
|
42
41
|
export async function getRedisClient() {
|
|
43
|
-
const url =
|
|
42
|
+
const url = getVars().redisUrl;
|
|
44
43
|
|
|
45
44
|
const pingResult = await state.client?.ping().catch( err => {
|
|
46
45
|
log.error( 'Redis ping failed', { error: err.message, code: err.code } );
|
|
@@ -14,6 +14,9 @@ vi.mock( '#logger', () => ( {
|
|
|
14
14
|
} )
|
|
15
15
|
} ) );
|
|
16
16
|
|
|
17
|
+
const getVarsMock = vi.fn();
|
|
18
|
+
vi.mock( './configs.js', () => ( { getVars: () => getVarsMock() } ) );
|
|
19
|
+
|
|
17
20
|
const createClientImpl = vi.fn();
|
|
18
21
|
vi.mock( 'redis', () => ( { createClient: opts => createClientImpl( opts ) } ) );
|
|
19
22
|
|
|
@@ -25,7 +28,7 @@ async function loadModule() {
|
|
|
25
28
|
describe( 'tracing/processors/s3/redis_client', () => {
|
|
26
29
|
beforeEach( () => {
|
|
27
30
|
vi.clearAllMocks();
|
|
28
|
-
|
|
31
|
+
getVarsMock.mockReturnValue( {} );
|
|
29
32
|
logCalls.warn = [];
|
|
30
33
|
logCalls.error = [];
|
|
31
34
|
} );
|
|
@@ -34,13 +37,14 @@ describe( 'tracing/processors/s3/redis_client', () => {
|
|
|
34
37
|
vi.useRealTimers();
|
|
35
38
|
} );
|
|
36
39
|
|
|
37
|
-
it( 'throws
|
|
40
|
+
it( 'throws when config redisUrl is missing', async () => {
|
|
41
|
+
getVarsMock.mockReturnValue( {} );
|
|
38
42
|
const { getRedisClient } = await loadModule();
|
|
39
|
-
await expect( getRedisClient() ).rejects.toThrow(
|
|
43
|
+
await expect( getRedisClient() ).rejects.toThrow();
|
|
40
44
|
} );
|
|
41
45
|
|
|
42
46
|
it( 'creates client with url, connects once, then reuses cached when ping is PONG', async () => {
|
|
43
|
-
|
|
47
|
+
getVarsMock.mockReturnValue( { redisUrl: 'redis://localhost:6379' } );
|
|
44
48
|
|
|
45
49
|
const pingMock = vi.fn().mockResolvedValue( 'PONG' );
|
|
46
50
|
const connectMock = vi.fn().mockResolvedValue();
|
|
@@ -63,7 +67,7 @@ describe( 'tracing/processors/s3/redis_client', () => {
|
|
|
63
67
|
} );
|
|
64
68
|
|
|
65
69
|
it( 'closes stale client and reconnects when ping fails', async () => {
|
|
66
|
-
|
|
70
|
+
getVarsMock.mockReturnValue( { redisUrl: 'redis://localhost:6379' } );
|
|
67
71
|
|
|
68
72
|
const quitMock = vi.fn().mockResolvedValue();
|
|
69
73
|
const connectMock = vi.fn().mockResolvedValue();
|
|
@@ -92,7 +96,7 @@ describe( 'tracing/processors/s3/redis_client', () => {
|
|
|
92
96
|
} );
|
|
93
97
|
|
|
94
98
|
it( 'reconnects successfully even when quit() on stale client rejects', async () => {
|
|
95
|
-
|
|
99
|
+
getVarsMock.mockReturnValue( { redisUrl: 'redis://localhost:6379' } );
|
|
96
100
|
|
|
97
101
|
const quitMock = vi.fn().mockRejectedValue( new Error( 'Quit failed' ) );
|
|
98
102
|
const connectMock = vi.fn().mockResolvedValue();
|
|
@@ -121,7 +125,7 @@ describe( 'tracing/processors/s3/redis_client', () => {
|
|
|
121
125
|
} );
|
|
122
126
|
|
|
123
127
|
it( 'wraps connect() errors with code and cleans up failed client', async () => {
|
|
124
|
-
|
|
128
|
+
getVarsMock.mockReturnValue( { redisUrl: 'redis://localhost:6379' } );
|
|
125
129
|
|
|
126
130
|
const connectErr = new Error( 'Connection refused' );
|
|
127
131
|
connectErr.code = 'ECONNREFUSED';
|
|
@@ -147,7 +151,7 @@ describe( 'tracing/processors/s3/redis_client', () => {
|
|
|
147
151
|
} );
|
|
148
152
|
|
|
149
153
|
it( 'logs ping failures with error level', async () => {
|
|
150
|
-
|
|
154
|
+
getVarsMock.mockReturnValue( { redisUrl: 'redis://localhost:6379' } );
|
|
151
155
|
|
|
152
156
|
const pingErr = new Error( 'Connection reset' );
|
|
153
157
|
pingErr.code = 'ECONNRESET';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
-
import {
|
|
2
|
+
import { getVars } from './configs.js';
|
|
3
3
|
|
|
4
4
|
const state = { s3Client: null };
|
|
5
5
|
|
|
@@ -12,9 +12,7 @@ const getS3Client = () => {
|
|
|
12
12
|
return state.s3Client;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
const secretAccessKey = process.env.OUTPUT_AWS_SECRET_ACCESS_KEY ?? throws( new Error( 'Missing OUTPUT_AWS_SECRET_ACCESS_KEY env var' ) );
|
|
17
|
-
const accessKeyId = process.env.OUTPUT_AWS_ACCESS_KEY_ID ?? throws( new Error( 'Missing OUTPUT_AWS_ACCESS_KEY_ID env var' ) );
|
|
15
|
+
const { awsRegion: region, awsSecretAccessKey: secretAccessKey, awsAccessKeyId: accessKeyId } = getVars();
|
|
18
16
|
|
|
19
17
|
return state.s3Client = new S3Client( { region, credentials: { accessKeyId, secretAccessKey } } );
|
|
20
18
|
};
|
|
@@ -26,8 +24,4 @@ const getS3Client = () => {
|
|
|
26
24
|
* @param {string} content - File content
|
|
27
25
|
*/
|
|
28
26
|
export const upload = ( { key, content } ) =>
|
|
29
|
-
getS3Client().send( new PutObjectCommand( {
|
|
30
|
-
Bucket: process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET ?? throws( new Error( 'Missing OUTPUT_TRACE_REMOTE_S3_BUCKET env var' ) ),
|
|
31
|
-
Key: key,
|
|
32
|
-
Body: content
|
|
33
|
-
} ) );
|
|
27
|
+
getS3Client().send( new PutObjectCommand( { Bucket: getVars().remoteS3Bucket, Key: key, Body: content } ) );
|
|
@@ -6,6 +6,9 @@ vi.mock( '#utils', () => ( {
|
|
|
6
6
|
}
|
|
7
7
|
} ) );
|
|
8
8
|
|
|
9
|
+
const getVarsMock = vi.fn();
|
|
10
|
+
vi.mock( './configs', () => ( { getVars: () => getVarsMock() } ) );
|
|
11
|
+
|
|
9
12
|
const sendMock = vi.fn();
|
|
10
13
|
const ctorState = { args: null };
|
|
11
14
|
class S3ClientMock {
|
|
@@ -32,23 +35,15 @@ async function loadModule() {
|
|
|
32
35
|
describe( 'tracing/processors/s3/s3_client', () => {
|
|
33
36
|
beforeEach( () => {
|
|
34
37
|
vi.clearAllMocks();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
getVarsMock.mockReturnValue( {
|
|
39
|
+
awsRegion: 'us-east-1',
|
|
40
|
+
awsAccessKeyId: 'id',
|
|
41
|
+
awsSecretAccessKey: 'sek',
|
|
42
|
+
remoteS3Bucket: 'bucket'
|
|
43
|
+
} );
|
|
39
44
|
} );
|
|
40
45
|
|
|
41
|
-
it( '
|
|
42
|
-
const { upload } = await loadModule();
|
|
43
|
-
expect( () => upload( { key: 'k', content: 'c' } ) ).toThrow();
|
|
44
|
-
} );
|
|
45
|
-
|
|
46
|
-
it( 'creates client once with env and uploads with bucket/key/content', async () => {
|
|
47
|
-
process.env.OUTPUT_AWS_REGION = 'us-east-1';
|
|
48
|
-
process.env.OUTPUT_AWS_SECRET_ACCESS_KEY = 'sek';
|
|
49
|
-
process.env.OUTPUT_AWS_ACCESS_KEY_ID = 'id';
|
|
50
|
-
process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET = 'bucket';
|
|
51
|
-
|
|
46
|
+
it( 'creates client once with config and uploads with bucket/key/content', async () => {
|
|
52
47
|
const { upload } = await loadModule();
|
|
53
48
|
|
|
54
49
|
await upload( { key: 'wf/key.json', content: '{"a":1}' } );
|