@bitblit/ratchet-aws 6.0.146-alpha → 6.0.148-alpha
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 +5 -4
- package/src/batch/aws-batch-background-processor.spec.ts +22 -0
- package/src/batch/aws-batch-background-processor.ts +71 -0
- package/src/batch/aws-batch-ratchet.spec.ts +42 -0
- package/src/batch/aws-batch-ratchet.ts +70 -0
- package/src/build/ratchet-aws-info.ts +19 -0
- package/src/cache/memory-storage-provider.ts +39 -0
- package/src/cache/simple-cache-object-wrapper.ts +11 -0
- package/src/cache/simple-cache-read-options.ts +9 -0
- package/src/cache/simple-cache-storage-provider.ts +15 -0
- package/src/cache/simple-cache.spec.ts +42 -0
- package/src/cache/simple-cache.ts +81 -0
- package/src/cloudwatch/cloud-watch-log-group-ratchet.spec.ts +26 -0
- package/src/cloudwatch/cloud-watch-log-group-ratchet.ts +105 -0
- package/src/cloudwatch/cloud-watch-logs-ratchet.spec.ts +123 -0
- package/src/cloudwatch/cloud-watch-logs-ratchet.ts +232 -0
- package/src/cloudwatch/cloud-watch-metrics-ratchet.spec.ts +30 -0
- package/src/cloudwatch/cloud-watch-metrics-ratchet.ts +98 -0
- package/src/dao/example-prototype-dao-item.ts +8 -0
- package/src/dao/memory-prototype-dao-provider.ts +16 -0
- package/src/dao/prototype-dao-config.ts +8 -0
- package/src/dao/prototype-dao-db.ts +4 -0
- package/src/dao/prototype-dao-provider.ts +6 -0
- package/src/dao/prototype-dao.spec.ts +33 -0
- package/src/dao/prototype-dao.ts +110 -0
- package/src/dao/s3-simple-dao.ts +96 -0
- package/src/dao/simple-dao-item.ts +13 -0
- package/src/dynamodb/dynamo-ratchet-like.ts +61 -0
- package/src/dynamodb/dynamo-ratchet.spec.ts +206 -0
- package/src/dynamodb/dynamo-ratchet.ts +850 -0
- package/src/dynamodb/dynamo-table-ratchet.spec.ts +23 -0
- package/src/dynamodb/dynamo-table-ratchet.ts +189 -0
- package/src/dynamodb/hash-spreader.spec.ts +22 -0
- package/src/dynamodb/hash-spreader.ts +89 -0
- package/src/dynamodb/impl/dynamo-db-storage-provider.spec.ts +60 -0
- package/src/dynamodb/impl/dynamo-db-storage-provider.ts +140 -0
- package/src/dynamodb/impl/dynamo-db-sync-lock.spec.ts +41 -0
- package/src/dynamodb/impl/dynamo-db-sync-lock.ts +78 -0
- package/src/dynamodb/impl/dynamo-expiring-code-provider.ts +31 -0
- package/src/dynamodb/impl/dynamo-runtime-parameter-provider.spec.ts +65 -0
- package/src/dynamodb/impl/dynamo-runtime-parameter-provider.ts +44 -0
- package/src/ec2/ec2-ratchet.spec.ts +45 -0
- package/src/ec2/ec2-ratchet.ts +169 -0
- package/src/ecr/ecr-unused-image-cleaner-options.ts +9 -0
- package/src/ecr/ecr-unused-image-cleaner-output.ts +8 -0
- package/src/ecr/ecr-unused-image-cleaner-repository-output.ts +10 -0
- package/src/ecr/ecr-unused-image-cleaner.spec.ts +40 -0
- package/src/ecr/ecr-unused-image-cleaner.ts +183 -0
- package/src/ecr/retained-image-descriptor.ts +7 -0
- package/src/ecr/retained-image-reason.ts +4 -0
- package/src/ecr/used-image-finder.ts +6 -0
- package/src/ecr/used-image-finders/aws-batch-used-image-finder.ts +40 -0
- package/src/ecr/used-image-finders/lambda-used-image-finder.ts +51 -0
- package/src/environment/cascade-environment-service-provider.ts +28 -0
- package/src/environment/env-var-environment-service-provider.ts +36 -0
- package/src/environment/environment-service-config.ts +7 -0
- package/src/environment/environment-service-provider.ts +7 -0
- package/src/environment/environment-service.spec.ts +41 -0
- package/src/environment/environment-service.ts +89 -0
- package/src/environment/fixed-environment-service-provider.ts +26 -0
- package/src/environment/ssm-environment-service-provider.spec.ts +18 -0
- package/src/environment/ssm-environment-service-provider.ts +71 -0
- package/src/expiring-code/expiring-code-params.ts +7 -0
- package/src/expiring-code/expiring-code-provider.ts +6 -0
- package/src/expiring-code/expiring-code-ratchet.spec.ts +10 -0
- package/src/expiring-code/expiring-code-ratchet.ts +44 -0
- package/src/expiring-code/expiring-code.ts +6 -0
- package/src/iam/aws-credentials-ratchet.ts +25 -0
- package/src/lambda/lambda-event-detector.ts +55 -0
- package/src/lambda/lambda-event-type-guards.ts +38 -0
- package/src/model/cloud-watch-metrics-minute-level-dynamo-count-request.ts +18 -0
- package/src/model/dynamo-count-result.ts +8 -0
- package/src/route53/route-53-ratchet.ts +77 -0
- package/src/runtime-parameter/cached-stored-runtime-parameter.ts +5 -0
- package/src/runtime-parameter/global-variable-override-runtime-parameter-provider.spec.ts +41 -0
- package/src/runtime-parameter/global-variable-override-runtime-parameter-provider.ts +82 -0
- package/src/runtime-parameter/memory-runtime-parameter-provider.ts +42 -0
- package/src/runtime-parameter/runtime-parameter-provider.ts +12 -0
- package/src/runtime-parameter/runtime-parameter-ratchet.spec.ts +53 -0
- package/src/runtime-parameter/runtime-parameter-ratchet.ts +84 -0
- package/src/runtime-parameter/stored-runtime-parameter.ts +6 -0
- package/src/s3/expanded-file-children.ts +5 -0
- package/src/s3/impl/s3-environment-service-provider.ts +41 -0
- package/src/s3/impl/s3-expiring-code-provider.spec.ts +63 -0
- package/src/s3/impl/s3-expiring-code-provider.ts +71 -0
- package/src/s3/impl/s3-prototype-dao-provider.spec.ts +45 -0
- package/src/s3/impl/s3-prototype-dao-provider.ts +37 -0
- package/src/s3/impl/s3-remote-file-tracking-provider-options.ts +6 -0
- package/src/s3/impl/s3-remote-file-tracking-provider.spec.ts +67 -0
- package/src/s3/impl/s3-remote-file-tracking-provider.ts +157 -0
- package/src/s3/impl/s3-storage-provider.spec.ts +32 -0
- package/src/s3/impl/s3-storage-provider.ts +60 -0
- package/src/s3/s3-cache-ratchet-like.ts +64 -0
- package/src/s3/s3-cache-ratchet.spec.ts +150 -0
- package/src/s3/s3-cache-ratchet.ts +476 -0
- package/src/s3/s3-location-sync-ratchet.ts +207 -0
- package/src/s3/s3-ratchet.spec.ts +26 -0
- package/src/s3/s3-ratchet.ts +26 -0
- package/src/ses/ses-mail-sending-provider.ts +85 -0
- package/src/sns/sns-ratchet.spec.ts +24 -0
- package/src/sns/sns-ratchet.ts +52 -0
- package/src/sync-lock/memory-sync-lock.ts +48 -0
- package/src/sync-lock/sync-lock-provider.ts +5 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
2
|
+
import { CloudWatchLogsRatchet } from './cloud-watch-logs-ratchet.js';
|
|
3
|
+
import {
|
|
4
|
+
CloudWatchLogsClient,
|
|
5
|
+
DeleteLogGroupCommand,
|
|
6
|
+
DeleteLogStreamCommand,
|
|
7
|
+
DescribeLogGroupsCommand,
|
|
8
|
+
DescribeLogStreamsCommand,
|
|
9
|
+
GetQueryResultsCommand,
|
|
10
|
+
GetQueryResultsCommandOutput,
|
|
11
|
+
LogGroup,
|
|
12
|
+
LogStream,
|
|
13
|
+
StartQueryCommand,
|
|
14
|
+
StartQueryCommandInput,
|
|
15
|
+
} from '@aws-sdk/client-cloudwatch-logs';
|
|
16
|
+
import { mockClient } from 'aws-sdk-client-mock';
|
|
17
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
18
|
+
|
|
19
|
+
let mockCW;
|
|
20
|
+
|
|
21
|
+
describe('#cloudWatchLogsRatchet', function () {
|
|
22
|
+
mockCW = mockClient(CloudWatchLogsClient);
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockCW.reset();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should find the stream with the oldest timestamp in a group', async () => {
|
|
29
|
+
mockCW.on(DescribeLogStreamsCommand).resolves({
|
|
30
|
+
logStreams: [
|
|
31
|
+
{ logStreamName: '1', firstEventTimestamp: 100 },
|
|
32
|
+
{ logStreamName: '2', firstEventTimestamp: 200 },
|
|
33
|
+
],
|
|
34
|
+
} as never);
|
|
35
|
+
|
|
36
|
+
const cw: CloudWatchLogsRatchet = new CloudWatchLogsRatchet(mockCW);
|
|
37
|
+
const res: LogStream = await cw.findStreamWithOldestEventInGroup('test');
|
|
38
|
+
expect(res).toBeTruthy();
|
|
39
|
+
expect(res.logStreamName).toEqual('1');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should find the oldest timestamp in group', async () => {
|
|
43
|
+
mockCW.on(DescribeLogStreamsCommand).resolves({
|
|
44
|
+
logStreams: [
|
|
45
|
+
{ logStreamName: '1', firstEventTimestamp: 100 },
|
|
46
|
+
{ logStreamName: '2', firstEventTimestamp: 200 },
|
|
47
|
+
],
|
|
48
|
+
} as never);
|
|
49
|
+
|
|
50
|
+
const cw: CloudWatchLogsRatchet = new CloudWatchLogsRatchet(mockCW);
|
|
51
|
+
const res: number = await cw.findOldestEventTimestampInGroup('test');
|
|
52
|
+
expect(res).toEqual(100);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should remove log groups', async () => {
|
|
56
|
+
mockCW.on(DeleteLogGroupCommand).resolves(null);
|
|
57
|
+
|
|
58
|
+
const cw: CloudWatchLogsRatchet = new CloudWatchLogsRatchet(mockCW);
|
|
59
|
+
const res: boolean[] = await cw.removeLogGroups([{ logGroupName: '1' }, { logGroupName: '2' }]);
|
|
60
|
+
expect(res.length).toEqual(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should remove log groups with prefix', async () => {
|
|
64
|
+
mockCW.on(DescribeLogGroupsCommand).resolves({
|
|
65
|
+
logGroups: [{ logGroupName: 'pre1' }, { logGroupName: 'pre2' }],
|
|
66
|
+
} as never);
|
|
67
|
+
|
|
68
|
+
mockCW.on(DeleteLogGroupCommand).resolves(null);
|
|
69
|
+
|
|
70
|
+
const cw: CloudWatchLogsRatchet = new CloudWatchLogsRatchet(mockCW);
|
|
71
|
+
const res: boolean[] = await cw.removeLogGroupsWithPrefix('pre');
|
|
72
|
+
expect(res.length).toEqual(2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should remove empty or old log streams', async () => {
|
|
76
|
+
mockCW.on(DescribeLogStreamsCommand).resolves({
|
|
77
|
+
logStreams: [{ logStreamName: '1' }, { logStreamName: '2' }],
|
|
78
|
+
} as never);
|
|
79
|
+
|
|
80
|
+
mockCW.on(DeleteLogStreamCommand).resolves(null);
|
|
81
|
+
|
|
82
|
+
const cw: CloudWatchLogsRatchet = new CloudWatchLogsRatchet(mockCW);
|
|
83
|
+
const res: LogStream[] = await cw.removeEmptyOrOldLogStreams('test', 1000);
|
|
84
|
+
expect(res).toBeTruthy();
|
|
85
|
+
expect(res.length).toEqual(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should find all matching groups', async () => {
|
|
89
|
+
mockCW.on(DescribeLogGroupsCommand).resolves({ logGroups: [{ logGroupName: '1' }, { logGroupName: '2' }] } as never);
|
|
90
|
+
|
|
91
|
+
const cw: CloudWatchLogsRatchet = new CloudWatchLogsRatchet(mockCW);
|
|
92
|
+
const prefix: string = '/';
|
|
93
|
+
|
|
94
|
+
const res: LogGroup[] = await cw.findLogGroups(prefix);
|
|
95
|
+
expect(res).toBeTruthy();
|
|
96
|
+
expect(res.length).toEqual(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should execute an insights query', async () => {
|
|
100
|
+
mockCW.on(StartQueryCommand).resolves({ queryId: 'test' });
|
|
101
|
+
|
|
102
|
+
mockCW.on(GetQueryResultsCommand).resolves({});
|
|
103
|
+
|
|
104
|
+
const cw: CloudWatchLogsRatchet = new CloudWatchLogsRatchet(mockCW);
|
|
105
|
+
|
|
106
|
+
const logGroups: string[] = ['/someGroup'];
|
|
107
|
+
|
|
108
|
+
const now: number = Math.floor(new Date().getTime() / 1000);
|
|
109
|
+
|
|
110
|
+
const req: StartQueryCommandInput = {
|
|
111
|
+
endTime: now,
|
|
112
|
+
limit: 200,
|
|
113
|
+
logGroupNames: logGroups,
|
|
114
|
+
queryString: 'fields @timestamp, @message | sort @timestamp desc',
|
|
115
|
+
startTime: now - 60 * 60 * 24, // 1 day ago
|
|
116
|
+
};
|
|
117
|
+
const res: GetQueryResultsCommandOutput = await cw.fullyExecuteInsightsQuery(req);
|
|
118
|
+
|
|
119
|
+
expect(res).toBeTruthy();
|
|
120
|
+
|
|
121
|
+
Logger.info('Got : %j', res);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloudWatchLogsClient,
|
|
3
|
+
DeleteLogGroupCommand,
|
|
4
|
+
DeleteLogGroupCommandInput,
|
|
5
|
+
DeleteLogStreamCommand,
|
|
6
|
+
DescribeLogGroupsCommand,
|
|
7
|
+
DescribeLogGroupsCommandInput,
|
|
8
|
+
DescribeLogGroupsCommandOutput,
|
|
9
|
+
DescribeLogStreamsCommand,
|
|
10
|
+
DescribeLogStreamsCommandOutput,
|
|
11
|
+
GetQueryResultsCommand,
|
|
12
|
+
GetQueryResultsCommandOutput,
|
|
13
|
+
LogGroup,
|
|
14
|
+
LogStream,
|
|
15
|
+
OrderBy,
|
|
16
|
+
StartQueryCommand,
|
|
17
|
+
StartQueryCommandInput,
|
|
18
|
+
StartQueryCommandOutput,
|
|
19
|
+
StopQueryCommand,
|
|
20
|
+
StopQueryCommandOutput,
|
|
21
|
+
} from '@aws-sdk/client-cloudwatch-logs';
|
|
22
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
23
|
+
import { PromiseRatchet } from '@bitblit/ratchet-common/lang/promise-ratchet';
|
|
24
|
+
import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
|
|
25
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
26
|
+
|
|
27
|
+
export class CloudWatchLogsRatchet {
|
|
28
|
+
private static readonly MAX_DELETE_RETRIES = 5;
|
|
29
|
+
private cwLogs: CloudWatchLogsClient;
|
|
30
|
+
|
|
31
|
+
constructor(cloudwatchLogs: CloudWatchLogsClient = null) {
|
|
32
|
+
this.cwLogs = cloudwatchLogs ? cloudwatchLogs : new CloudWatchLogsClient({ region: 'us-east-1' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public get cloudWatchLogsClient(): CloudWatchLogsClient {
|
|
36
|
+
return this.cwLogs;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async removeEmptyOrOldLogStreams(
|
|
40
|
+
logGroupName: string,
|
|
41
|
+
maxToRemove = 1000,
|
|
42
|
+
oldestEventEpochMS: number = null,
|
|
43
|
+
): Promise<LogStream[]> {
|
|
44
|
+
Logger.info('Removing empty streams from %s, oldest event epoch MS : %d', logGroupName, oldestEventEpochMS);
|
|
45
|
+
const streamSearchParams: any = {
|
|
46
|
+
logGroupName: logGroupName,
|
|
47
|
+
orderBy: OrderBy.LastEventTime,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const oldestEventTester: number = oldestEventEpochMS || 1;
|
|
51
|
+
let totalStreams = 0;
|
|
52
|
+
const removedStreams: LogStream[] = [];
|
|
53
|
+
const failedRemovedStreams: LogStream[] = [];
|
|
54
|
+
let waitPerDescribe = 10;
|
|
55
|
+
|
|
56
|
+
do {
|
|
57
|
+
Logger.debug('Executing search for streams');
|
|
58
|
+
try {
|
|
59
|
+
const streams: DescribeLogStreamsCommandOutput = await this.cwLogs.send(new DescribeLogStreamsCommand(streamSearchParams));
|
|
60
|
+
totalStreams += streams.logStreams.length;
|
|
61
|
+
|
|
62
|
+
Logger.debug('Found %d streams (%d so far, %d to delete)', streams.logStreams.length, totalStreams, removedStreams.length);
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < streams.logStreams.length && removedStreams.length < maxToRemove; i++) {
|
|
65
|
+
const st: LogStream = streams.logStreams[i];
|
|
66
|
+
if (!st.firstEventTimestamp) {
|
|
67
|
+
removedStreams.push(st);
|
|
68
|
+
} else if (st.lastEventTimestamp < oldestEventTester) {
|
|
69
|
+
removedStreams.push(st);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
streamSearchParams['nextToken'] = streams.nextToken;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const oldWait: number = waitPerDescribe;
|
|
76
|
+
waitPerDescribe = Math.min(1000, waitPerDescribe * 1.5);
|
|
77
|
+
Logger.info('Caught while describing %s, increasing wait between deletes (was %d, now %d)', err, oldWait, waitPerDescribe);
|
|
78
|
+
}
|
|
79
|
+
} while (!!streamSearchParams['nextToken'] && removedStreams.length < maxToRemove);
|
|
80
|
+
|
|
81
|
+
Logger.info('Found %d streams to delete', removedStreams.length);
|
|
82
|
+
let waitPer = 10;
|
|
83
|
+
|
|
84
|
+
for (const rStream of removedStreams) {
|
|
85
|
+
//for (let i = 0; i < removedStreams.length; i++) {
|
|
86
|
+
const delParams = {
|
|
87
|
+
logGroupName: logGroupName,
|
|
88
|
+
logStreamName: rStream.logStreamName,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const type: string = rStream.storedBytes === 0 ? 'empty' : 'old';
|
|
92
|
+
Logger.info('Removing %s stream %s', type, rStream.logStreamName);
|
|
93
|
+
let removed = false;
|
|
94
|
+
let retry = 0;
|
|
95
|
+
while (!removed && retry < CloudWatchLogsRatchet.MAX_DELETE_RETRIES) {
|
|
96
|
+
try {
|
|
97
|
+
await this.cwLogs.send(new DeleteLogStreamCommand(delParams));
|
|
98
|
+
removed = true;
|
|
99
|
+
await PromiseRatchet.wait(waitPer);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
retry++;
|
|
102
|
+
const oldWait: number = waitPer;
|
|
103
|
+
waitPer = Math.min(1000, waitPer * 1.5);
|
|
104
|
+
Logger.info(
|
|
105
|
+
'Caught %s, increasing wait between deletes and retrying (wait was %d, now %d) (Retry %d of %d)',
|
|
106
|
+
err,
|
|
107
|
+
oldWait,
|
|
108
|
+
waitPer,
|
|
109
|
+
retry,
|
|
110
|
+
CloudWatchLogsRatchet.MAX_DELETE_RETRIES,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!removed) {
|
|
115
|
+
// Ran out of retries
|
|
116
|
+
failedRemovedStreams.push(rStream);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Logger.warn('Failed to remove streams : %j', failedRemovedStreams);
|
|
121
|
+
return removedStreams;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public async findOldestEventTimestampInGroup(logGroupName: string): Promise<number> {
|
|
125
|
+
const stream: LogStream = await this.findStreamWithOldestEventInGroup(logGroupName);
|
|
126
|
+
return stream ? stream.firstEventTimestamp : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async findStreamWithOldestEventInGroup(logGroupName: string): Promise<LogStream> {
|
|
130
|
+
Logger.info('Finding oldest event in : %s', logGroupName);
|
|
131
|
+
let rval: LogStream = null;
|
|
132
|
+
try {
|
|
133
|
+
const streamSearchParams = {
|
|
134
|
+
logGroupName: logGroupName,
|
|
135
|
+
orderBy: OrderBy.LastEventTime,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let totalStreams = 0;
|
|
139
|
+
do {
|
|
140
|
+
Logger.debug('Executing search for streams');
|
|
141
|
+
const streams: DescribeLogStreamsCommandOutput = await this.cwLogs.send(new DescribeLogStreamsCommand(streamSearchParams));
|
|
142
|
+
totalStreams += streams.logStreams.length;
|
|
143
|
+
|
|
144
|
+
Logger.debug('Found %d streams (%d so far)', streams.logStreams.length, totalStreams);
|
|
145
|
+
|
|
146
|
+
streams.logStreams.forEach((s) => {
|
|
147
|
+
if (s.firstEventTimestamp && (rval === null || s.firstEventTimestamp < rval.firstEventTimestamp)) {
|
|
148
|
+
rval = s;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
streamSearchParams['nextToken'] = streams.nextToken;
|
|
153
|
+
} while (streamSearchParams['nextToken']);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
Logger.error('Error attempting to find oldest event in group : %s : %s', logGroupName, err, err);
|
|
156
|
+
}
|
|
157
|
+
return rval;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public async findLogGroups(prefix: string): Promise<LogGroup[]> {
|
|
161
|
+
RequireRatchet.notNullOrUndefined(prefix);
|
|
162
|
+
|
|
163
|
+
const params: DescribeLogGroupsCommandInput = {
|
|
164
|
+
logGroupNamePrefix: prefix,
|
|
165
|
+
};
|
|
166
|
+
let rval: LogGroup[] = [];
|
|
167
|
+
|
|
168
|
+
do {
|
|
169
|
+
Logger.info('%d found, pulling log groups : %j', rval.length, params);
|
|
170
|
+
const res: DescribeLogGroupsCommandOutput = await this.cwLogs.send(new DescribeLogGroupsCommand(params));
|
|
171
|
+
rval = rval.concat(res.logGroups);
|
|
172
|
+
params.nextToken = res.nextToken;
|
|
173
|
+
} while (params.nextToken);
|
|
174
|
+
|
|
175
|
+
return rval;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public async removeLogGroups(groups: LogGroup[]): Promise<boolean[]> {
|
|
179
|
+
RequireRatchet.notNullOrUndefined(groups);
|
|
180
|
+
const rval: boolean[] = [];
|
|
181
|
+
|
|
182
|
+
for (const dGroup of groups) {
|
|
183
|
+
try {
|
|
184
|
+
Logger.info('Deleting %j', dGroup);
|
|
185
|
+
const req: DeleteLogGroupCommandInput = {
|
|
186
|
+
logGroupName: dGroup.logGroupName,
|
|
187
|
+
};
|
|
188
|
+
await this.cwLogs.send(new DeleteLogGroupCommand(req));
|
|
189
|
+
rval.push(true);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
Logger.error('Failure to delete %j : %s', dGroup, err);
|
|
192
|
+
rval.push(false);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return rval;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public async removeLogGroupsWithPrefix(prefix: string): Promise<boolean[]> {
|
|
200
|
+
RequireRatchet.notNullOrUndefined(prefix);
|
|
201
|
+
RequireRatchet.true(StringRatchet.trimToEmpty(prefix).length > 0); // Don't allow nuke all here
|
|
202
|
+
|
|
203
|
+
Logger.info('Removing log groups with prefix %s', prefix);
|
|
204
|
+
const groups: LogGroup[] = await this.findLogGroups(prefix);
|
|
205
|
+
return await this.removeLogGroups(groups);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public async fullyExecuteInsightsQuery(sqr: StartQueryCommandInput): Promise<GetQueryResultsCommandOutput> {
|
|
209
|
+
RequireRatchet.notNullOrUndefined(sqr);
|
|
210
|
+
Logger.debug('Starting insights query : %j', sqr);
|
|
211
|
+
const resp: StartQueryCommandOutput = await this.cwLogs.send(new StartQueryCommand(sqr));
|
|
212
|
+
Logger.debug('Got query id %j', resp);
|
|
213
|
+
|
|
214
|
+
let rval: GetQueryResultsCommandOutput = null;
|
|
215
|
+
let delayMS: number = 100;
|
|
216
|
+
while (!rval || ['Running', 'Scheduled'].includes(rval.status)) {
|
|
217
|
+
rval = await this.cwLogs.send(new GetQueryResultsCommand({ queryId: resp.queryId }));
|
|
218
|
+
await PromiseRatchet.wait(delayMS);
|
|
219
|
+
delayMS *= 2;
|
|
220
|
+
Logger.info('Got : %j', rval);
|
|
221
|
+
}
|
|
222
|
+
return rval;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public async abortInsightsQuery(queryId: string): Promise<StopQueryCommandOutput> {
|
|
226
|
+
let rval: StopQueryCommandOutput = null;
|
|
227
|
+
if (queryId) {
|
|
228
|
+
rval = await this.cwLogs.send(new StopQueryCommand({ queryId: queryId }));
|
|
229
|
+
}
|
|
230
|
+
return rval;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CloudWatchMetricsRatchet } from './cloud-watch-metrics-ratchet.js';
|
|
2
|
+
|
|
3
|
+
import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from '@aws-sdk/client-cloudwatch';
|
|
4
|
+
import { mockClient } from 'aws-sdk-client-mock';
|
|
5
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
6
|
+
import { KeyValue } from '@bitblit/ratchet-common/lang/key-value';
|
|
7
|
+
|
|
8
|
+
let mockCW;
|
|
9
|
+
|
|
10
|
+
describe('#cloudWatchMetricsRatchet', function () {
|
|
11
|
+
mockCW = mockClient(CloudWatchClient);
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockCW.reset();
|
|
14
|
+
mockCW.reset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should log a cloudwatch metric', async () => {
|
|
18
|
+
mockCW.on(PutMetricDataCommand).resolves({ ok: true } as never);
|
|
19
|
+
|
|
20
|
+
const cw: CloudWatchMetricsRatchet = new CloudWatchMetricsRatchet(mockCW);
|
|
21
|
+
const dims: KeyValue<any>[] = [
|
|
22
|
+
{ key: 'server', value: 'prod' } as KeyValue<any>,
|
|
23
|
+
{ key: 'stage', value: 'v0' },
|
|
24
|
+
{ key: 'version', value: '20190529-01' },
|
|
25
|
+
];
|
|
26
|
+
const res: any = await cw.writeSingleMetric('Ratchet/TestMetric01', 'MyMetric01', dims, StandardUnit.Count, 2, new Date(), false);
|
|
27
|
+
|
|
28
|
+
expect(res).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Service for interacting with cloudwatch
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CloudWatchClient,
|
|
7
|
+
PutMetricDataCommand,
|
|
8
|
+
PutMetricDataCommandInput,
|
|
9
|
+
PutMetricDataCommandOutput,
|
|
10
|
+
StandardUnit,
|
|
11
|
+
} from '@aws-sdk/client-cloudwatch';
|
|
12
|
+
import { DynamoCountResult } from '../model/dynamo-count-result.js';
|
|
13
|
+
import { CloudWatchMetricsMinuteLevelDynamoCountRequest } from '../model/cloud-watch-metrics-minute-level-dynamo-count-request.js';
|
|
14
|
+
import { DateTime } from 'luxon';
|
|
15
|
+
import { KeyValue } from '@bitblit/ratchet-common/lang/key-value';
|
|
16
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
17
|
+
|
|
18
|
+
export class CloudWatchMetricsRatchet {
|
|
19
|
+
private cw: CloudWatchClient;
|
|
20
|
+
|
|
21
|
+
constructor(cloudWatch: CloudWatchClient = null) {
|
|
22
|
+
this.cw = cloudWatch ? cloudWatch : new CloudWatchClient({ region: 'us-east-1', apiVersion: '2010-08-01' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public get cloudWatchClient(): CloudWatchClient {
|
|
26
|
+
return this.cw;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async writeSingleMetric(
|
|
30
|
+
namespace: string,
|
|
31
|
+
metric: string,
|
|
32
|
+
dims: KeyValue<any>[],
|
|
33
|
+
unit: StandardUnit = StandardUnit.None,
|
|
34
|
+
value: number,
|
|
35
|
+
timestampDate: Date = new Date(),
|
|
36
|
+
highResolution = false,
|
|
37
|
+
): Promise<any> {
|
|
38
|
+
const cwDims: any[] = [];
|
|
39
|
+
if (!!dims && dims.length > 0) {
|
|
40
|
+
dims.forEach((d) => {
|
|
41
|
+
cwDims.push({ Name: d.key, Value: d.value });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const storageResolution: number = highResolution ? 1 : 60;
|
|
45
|
+
|
|
46
|
+
const metricData: PutMetricDataCommandInput = {
|
|
47
|
+
Namespace: namespace,
|
|
48
|
+
MetricData: [
|
|
49
|
+
{
|
|
50
|
+
MetricName: metric,
|
|
51
|
+
Dimensions: cwDims,
|
|
52
|
+
Unit: unit,
|
|
53
|
+
Value: value,
|
|
54
|
+
Timestamp: timestampDate,
|
|
55
|
+
StorageResolution: storageResolution,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
Logger.silly('Writing metric to cw : %j', metricData);
|
|
60
|
+
|
|
61
|
+
const result: PutMetricDataCommandOutput = await this.cw.send(new PutMetricDataCommand(metricData));
|
|
62
|
+
Logger.silly('Result: %j', result);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async writeDynamoCountAsMinuteLevelMetric(req: CloudWatchMetricsMinuteLevelDynamoCountRequest): Promise<number> {
|
|
67
|
+
Logger.info('Publishing %s / %s metric for %s UTC', req.namespace, req.metric, req.minuteUTC);
|
|
68
|
+
|
|
69
|
+
if (!!req.scan && !!req.query) {
|
|
70
|
+
throw new Error('Must send query or scan, but not both');
|
|
71
|
+
}
|
|
72
|
+
if (!req.scan && !req.query) {
|
|
73
|
+
throw new Error('You must specify either a scan or a query');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cnt: DynamoCountResult = req.query
|
|
77
|
+
? await req.dynamoRatchet.fullyExecuteQueryCount(req.query)
|
|
78
|
+
: await req.dynamoRatchet.fullyExecuteScanCount(req.scan);
|
|
79
|
+
|
|
80
|
+
Logger.debug('%s / %s for %s are %j', req.namespace, req.metric, req.minuteUTC, cnt);
|
|
81
|
+
|
|
82
|
+
const parseDateString: string = req.minuteUTC.split(' ').join('T') + ':00Z';
|
|
83
|
+
const parseDate: Date = DateTime.fromISO(parseDateString).toJSDate();
|
|
84
|
+
|
|
85
|
+
const metricRes: any = await this.writeSingleMetric(
|
|
86
|
+
req.namespace,
|
|
87
|
+
req.metric,
|
|
88
|
+
req.dims,
|
|
89
|
+
StandardUnit.Count,
|
|
90
|
+
cnt.count,
|
|
91
|
+
parseDate,
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
94
|
+
Logger.debug('Metrics response: %j', metricRes);
|
|
95
|
+
|
|
96
|
+
return cnt.count;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Memory version for testing
|
|
2
|
+
import { PrototypeDaoProvider } from './prototype-dao-provider.js';
|
|
3
|
+
import { PrototypeDaoDb } from './prototype-dao-db.js';
|
|
4
|
+
|
|
5
|
+
export class MemoryPrototypeDaoProvider implements PrototypeDaoProvider<any> {
|
|
6
|
+
private _db: PrototypeDaoDb<any> = null;
|
|
7
|
+
|
|
8
|
+
public async loadDatabase(): Promise<PrototypeDaoDb<any>> {
|
|
9
|
+
return this._db;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async storeDatabase(db: PrototypeDaoDb<any>): Promise<boolean> {
|
|
13
|
+
this._db = db;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PrototypeDao } from './prototype-dao.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { MemoryPrototypeDaoProvider } from './memory-prototype-dao-provider.js';
|
|
4
|
+
import { ExamplePrototypeDaoItem } from './example-prototype-dao-item.js';
|
|
5
|
+
|
|
6
|
+
describe('#PrototypeDao', () => {
|
|
7
|
+
test.skip('Should save/load files', async () => {
|
|
8
|
+
const svc: PrototypeDao<ExamplePrototypeDaoItem> = new PrototypeDao<ExamplePrototypeDaoItem>(new MemoryPrototypeDaoProvider());
|
|
9
|
+
await svc.resetDatabase();
|
|
10
|
+
|
|
11
|
+
await svc.store({ fieldA: 1, fieldB: 'test1', type: 'a' });
|
|
12
|
+
await svc.store({ fieldA: 2, fieldB: 'test2', type: 'a' });
|
|
13
|
+
await svc.store({ fieldA: 2, fieldB: 'test3', type: 'b' });
|
|
14
|
+
await svc.store({ guid: 'forceGuid', fieldA: 4, fieldB: 'test4', type: 'c' });
|
|
15
|
+
|
|
16
|
+
const test1: ExamplePrototypeDaoItem[] = await svc.fetchAll();
|
|
17
|
+
expect(test1.length).toBe(4);
|
|
18
|
+
|
|
19
|
+
const test2: ExamplePrototypeDaoItem = await svc.fetchById('forceGuid');
|
|
20
|
+
expect(test2).not.toBeNull();
|
|
21
|
+
expect(test2.createdEpochMS).not.toBeNull();
|
|
22
|
+
expect(test2.updatedEpochMS).not.toBeNull();
|
|
23
|
+
|
|
24
|
+
const test3: ExamplePrototypeDaoItem[] = await svc.searchByField('fieldA', 4);
|
|
25
|
+
expect(test3.length).toBe(1);
|
|
26
|
+
expect(test3[0].guid).toEqual('forceGuid');
|
|
27
|
+
|
|
28
|
+
const test4: ExamplePrototypeDaoItem[] = await svc.searchByFieldMap({ type: 'a', fieldA: 2 });
|
|
29
|
+
expect(test4.length).toBe(1);
|
|
30
|
+
|
|
31
|
+
await svc.resetDatabase();
|
|
32
|
+
}, 300_000);
|
|
33
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { PrototypeDaoProvider } from './prototype-dao-provider.js';
|
|
2
|
+
import { PrototypeDaoDb } from './prototype-dao-db.js';
|
|
3
|
+
import { PrototypeDaoConfig } from './prototype-dao-config.js';
|
|
4
|
+
import { DateTime } from 'luxon';
|
|
5
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
6
|
+
import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
|
|
7
|
+
import { MapRatchet } from '@bitblit/ratchet-common/lang/map-ratchet';
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
PrototypeDao makes it quick to stand up a simple data access object
|
|
11
|
+
key/value store backed by something simple (usually a file in S3).
|
|
12
|
+
|
|
13
|
+
It is slow and not very powerful, but is meant to be stood up rapidly, and then
|
|
14
|
+
replaced once you have something useful to do the Dao's job
|
|
15
|
+
|
|
16
|
+
Note this thing pulls the WHOLE DB for every single operation. You really don't wanna use it
|
|
17
|
+
for anything like serious workloads
|
|
18
|
+
|
|
19
|
+
*/
|
|
20
|
+
export class PrototypeDao<T> {
|
|
21
|
+
public static defaultDaoConfig(): PrototypeDaoConfig {
|
|
22
|
+
return {
|
|
23
|
+
guidCreateFunction: StringRatchet.createType4Guid,
|
|
24
|
+
guidFieldName: 'guid',
|
|
25
|
+
createdEpochMSFieldName: 'createdEpochMS',
|
|
26
|
+
updatedEpochMSFieldName: 'updatedEpochMS',
|
|
27
|
+
createdUtcTimestampFieldName: null,
|
|
28
|
+
updatedUtcTimestampFieldName: null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private provider: PrototypeDaoProvider<T>,
|
|
34
|
+
private cfg: PrototypeDaoConfig = PrototypeDao.defaultDaoConfig(),
|
|
35
|
+
) {
|
|
36
|
+
RequireRatchet.notNullOrUndefined(provider, 'provider');
|
|
37
|
+
RequireRatchet.notNullOrUndefined(cfg, 'cfg');
|
|
38
|
+
RequireRatchet.notNullOrUndefined(cfg.guidCreateFunction, 'cfg.guidCreateFunction');
|
|
39
|
+
RequireRatchet.notNullOrUndefined(cfg.guidFieldName, 'cfg.guidFieldName');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async fetchAll(): Promise<T[]> {
|
|
43
|
+
const db: PrototypeDaoDb<T> = await this.provider.loadDatabase();
|
|
44
|
+
return db.items || [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async resetDatabase(): Promise<void> {
|
|
48
|
+
await this.provider.storeDatabase({ items: [], lastModifiedEpochMS: Date.now() });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async removeItems(guids: string[]): Promise<T[]> {
|
|
52
|
+
let old: T[] = await this.fetchAll();
|
|
53
|
+
if (guids) {
|
|
54
|
+
old = old.filter((t) => !guids.includes(t[this.cfg.guidFieldName]));
|
|
55
|
+
await this.provider.storeDatabase({ items: old, lastModifiedEpochMS: Date.now() });
|
|
56
|
+
}
|
|
57
|
+
return old;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async store(value: T): Promise<T> {
|
|
61
|
+
let old: T[] = await this.fetchAll();
|
|
62
|
+
if (value) {
|
|
63
|
+
value[this.cfg.guidFieldName] = value[this.cfg.guidFieldName] || this.cfg.guidCreateFunction();
|
|
64
|
+
if (this.cfg.createdEpochMSFieldName) {
|
|
65
|
+
value[this.cfg.createdEpochMSFieldName] = value[this.cfg.createdEpochMSFieldName] || Date.now();
|
|
66
|
+
}
|
|
67
|
+
if (this.cfg.createdUtcTimestampFieldName) {
|
|
68
|
+
value[this.cfg.createdUtcTimestampFieldName] = value[this.cfg.createdUtcTimestampFieldName] || DateTime.utc().toISO();
|
|
69
|
+
}
|
|
70
|
+
if (this.cfg.updatedEpochMSFieldName) {
|
|
71
|
+
value[this.cfg.updatedEpochMSFieldName] = Date.now();
|
|
72
|
+
}
|
|
73
|
+
if (this.cfg.updatedUtcTimestampFieldName) {
|
|
74
|
+
value[this.cfg.updatedUtcTimestampFieldName] = DateTime.utc().toISO();
|
|
75
|
+
}
|
|
76
|
+
old = old.filter((t) => t[this.cfg.guidFieldName] !== value[this.cfg.guidFieldName]);
|
|
77
|
+
old.push(value);
|
|
78
|
+
await this.provider.storeDatabase({ items: old, lastModifiedEpochMS: Date.now() });
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async fetchById(guid: string): Promise<T> {
|
|
84
|
+
const old: T[] = await this.fetchAll();
|
|
85
|
+
return old.find((t) => t[this.cfg.guidFieldName] === guid);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async searchByField<R>(fieldDotPath: string, fieldValue: R): Promise<T[]> {
|
|
89
|
+
RequireRatchet.notNullOrUndefined(fieldDotPath, 'fieldDotPath');
|
|
90
|
+
RequireRatchet.notNullOrUndefined(fieldValue, 'fieldValue');
|
|
91
|
+
const map: Record<string, any> = {};
|
|
92
|
+
map[fieldDotPath] = fieldValue;
|
|
93
|
+
return this.searchByFieldMap(map);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async searchByFieldMap(input: Record<string, any>): Promise<T[]> {
|
|
97
|
+
RequireRatchet.notNullOrUndefined(input, 'input');
|
|
98
|
+
let old: T[] = await this.fetchAll();
|
|
99
|
+
old = old.filter((t) => {
|
|
100
|
+
let matchAll: boolean = true;
|
|
101
|
+
Object.keys(input).forEach((k) => {
|
|
102
|
+
const val: any = MapRatchet.findValueDotPath(t, k);
|
|
103
|
+
const fieldValue: any = input[k];
|
|
104
|
+
matchAll = matchAll && val === fieldValue;
|
|
105
|
+
});
|
|
106
|
+
return matchAll;
|
|
107
|
+
});
|
|
108
|
+
return old;
|
|
109
|
+
}
|
|
110
|
+
}
|