Skip to content

Commit 8cf51c5

Browse files
authored
Archive old Cloudwatch Logs to S3 (awslabs#9)
Move Audit Cloudwatch Logs from Cloudwatch Logs to S3 and delete the Cloudwatch Logs. This job is run daily by a step function, and will only move logs that are older than 7 days.
1 parent e69c12f commit 8cf51c5

11 files changed

+8245
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ dist
1111
.idea
1212
yarn-error.log
1313

14+
15+
auditLogMover/.serverless
16+
auditLogMover/node_modules
17+
1418
coverage
1519
##############################
1620
## local Development
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import moment from 'moment';
2+
3+
import { AuditLogMoverHelper } from './auditLogMoverHelper';
4+
5+
describe('getEachDayInTimeFrame', () => {
6+
test('Get three days worth of dates', () => {
7+
const startDate = moment('2020-06-12');
8+
const endDate = moment('2020-06-15');
9+
10+
const days = AuditLogMoverHelper.getEachDayInTimeFrame(startDate, endDate);
11+
12+
const dates = days.map(day => {
13+
return day.format('GGGG-MM-DD');
14+
});
15+
16+
expect(dates).toEqual(['2020-06-12', '2020-06-13', '2020-06-14']);
17+
});
18+
test('Get one days worth of dates', () => {
19+
const startDate = moment('2020-06-12').startOf('day');
20+
const endDate = moment('2020-06-12').endOf('day');
21+
22+
const days = AuditLogMoverHelper.getEachDayInTimeFrame(startDate, endDate);
23+
24+
const dates = days.map(day => {
25+
return day.format('GGGG-MM-DD');
26+
});
27+
28+
expect(dates).toEqual(['2020-06-12']);
29+
});
30+
test('End date is earlier than start date', () => {
31+
expect.hasAssertions();
32+
const startDate = moment('2020-06-13').startOf('day');
33+
const endDate = moment('2020-06-12').endOf('day');
34+
35+
try {
36+
AuditLogMoverHelper.getEachDayInTimeFrame(startDate, endDate);
37+
} catch (e) {
38+
expect(e.message).toEqual('startTime can not be later than endTime');
39+
}
40+
});
41+
});

auditLogMover/auditLogMoverHelper.ts

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import AWS from 'aws-sdk';
2+
import moment from 'moment';
3+
// eslint-disable-next-line import/extensions
4+
import { ListObjectsV2Output } from 'aws-sdk/clients/s3';
5+
6+
export class AuditLogMoverHelper {
7+
static async doesEachDayHaveS3Directory(eachDayInTimeFrame: string[], auditLogBucket: string) {
8+
const yearAndMonthPrefixOfDates = Array.from(
9+
new Set(
10+
eachDayInTimeFrame.map(date => {
11+
return date.substring(0, 7); // Only grab the year and month
12+
}),
13+
),
14+
);
15+
const directoriesInS3 = await this.getDirectoriesInS3GivenPrefixes(yearAndMonthPrefixOfDates, auditLogBucket);
16+
17+
const dateWithoutDirectory = eachDayInTimeFrame.filter(date => !directoriesInS3.includes(date));
18+
19+
return dateWithoutDirectory.length === 0;
20+
}
21+
22+
static getEachDayInTimeFrame(startTimeMoment: moment.Moment, endTimeMoment: moment.Moment): moment.Moment[] {
23+
if (startTimeMoment.isAfter(endTimeMoment)) {
24+
throw new Error('startTime can not be later than endTime');
25+
}
26+
const eachDayInTimeFrame: moment.Moment[] = [];
27+
28+
let currentTimeMoment = moment(startTimeMoment);
29+
do {
30+
eachDayInTimeFrame.push(moment(currentTimeMoment));
31+
currentTimeMoment = currentTimeMoment.add(1, 'days');
32+
} while (currentTimeMoment.valueOf() < endTimeMoment.valueOf());
33+
34+
return eachDayInTimeFrame;
35+
}
36+
37+
private static async getDirectoriesInS3GivenPrefixes(prefixes: string[], auditLogBucket: string) {
38+
const S3 = new AWS.S3();
39+
const listS3Responses: ListObjectsV2Output[] = await Promise.all(
40+
prefixes.map(prefix => {
41+
const s3params: any = {
42+
Bucket: auditLogBucket,
43+
MaxKeys: 31,
44+
Delimiter: '/',
45+
Prefix: prefix,
46+
};
47+
return S3.listObjectsV2(s3params).promise();
48+
}),
49+
);
50+
51+
const directoriesInS3: string[] = [];
52+
listS3Responses.forEach((response: ListObjectsV2Output) => {
53+
if (response.CommonPrefixes) {
54+
response.CommonPrefixes.forEach(commonPrefix => {
55+
// Format of Prefix is 2020-07-04/
56+
// Therefore we need to remove the '/' at the end
57+
if (commonPrefix.Prefix) {
58+
directoriesInS3.push(commonPrefix.Prefix.slice(0, -1));
59+
}
60+
});
61+
}
62+
});
63+
64+
return directoriesInS3;
65+
}
66+
67+
static async getAllLogStreams(cwLogExecutionGroup: string): Promise<LogStreamType[]> {
68+
const params: any = {
69+
logGroupName: cwLogExecutionGroup,
70+
orderBy: 'LastEventTime',
71+
descending: true,
72+
limit: 50,
73+
};
74+
const logStreams: LogStreamType[] = [];
75+
76+
let nextToken = '';
77+
do {
78+
if (nextToken) {
79+
params.nextToken = nextToken;
80+
}
81+
const cloudwatchLogs = new AWS.CloudWatchLogs();
82+
// eslint-disable-next-line no-await-in-loop
83+
const describeResponse = await cloudwatchLogs.describeLogStreams(params).promise();
84+
if (describeResponse.logStreams && describeResponse.logStreams.length > 0) {
85+
describeResponse.logStreams.forEach((logStream: any) => {
86+
logStreams.push({
87+
logStreamName: logStream.logStreamName,
88+
firstEventTimestamp: logStream.firstEventTimestamp,
89+
lastEventTimestamp: logStream.lastEventTimestamp,
90+
});
91+
});
92+
}
93+
nextToken = describeResponse.nextToken || '';
94+
} while (nextToken);
95+
96+
return logStreams;
97+
}
98+
99+
static async putCWMetric(stage: string, functionName: string, isSuccessful: boolean) {
100+
const putMetricDataPromises = [];
101+
if (isSuccessful) {
102+
// Mark a value of value of 1 for metric marking success and a value of 0 for metric marking failure
103+
putMetricDataPromises.push(this.getMetricDataPromise(stage, functionName, true, 1));
104+
putMetricDataPromises.push(this.getMetricDataPromise(stage, functionName, false, 0));
105+
} else {
106+
putMetricDataPromises.push(this.getMetricDataPromise(stage, functionName, true, 0));
107+
putMetricDataPromises.push(this.getMetricDataPromise(stage, functionName, false, 1));
108+
}
109+
110+
try {
111+
await Promise.all(putMetricDataPromises);
112+
} catch (e) {
113+
console.error('Failed to putCWMetric', e);
114+
}
115+
}
116+
117+
private static async getMetricDataPromise(
118+
stage: string,
119+
functionName: string,
120+
isSuccessful: boolean,
121+
metricValue: number,
122+
) {
123+
const MetricName = `${functionName}-${isSuccessful ? 'Succeeded' : 'Failed'}`;
124+
const params: any = {
125+
MetricData: [
126+
{
127+
MetricName,
128+
Dimensions: [
129+
{
130+
Name: 'Stage',
131+
Value: stage,
132+
},
133+
],
134+
StorageResolution: 60,
135+
Timestamp: new Date(),
136+
Unit: 'Count',
137+
Value: metricValue,
138+
},
139+
],
140+
Namespace: 'Audit-Log-Mover',
141+
};
142+
const cloudwatch = new AWS.CloudWatch();
143+
return cloudwatch.putMetricData(params).promise();
144+
}
145+
}
146+
export interface LogStreamType {
147+
logStreamName: string;
148+
firstEventTimestamp: number;
149+
lastEventTimestamp: number;
150+
}

0 commit comments

Comments
 (0)