Skip to content

Commit 6670848

Browse files
CR-5828 logs-offloader (#709)
1 parent 20d2ad5 commit 6670848

File tree

6 files changed

+354
-0
lines changed

6 files changed

+354
-0
lines changed

lib/interface/cli/commands/context/create/secret-store/base.cmd.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const yargs = require('yargs');
2+
13
const Command = require('../../../../Command');
24
const createContext = require('../../create.cmd');
35

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const yargs = require('yargs');
2+
const Command = require('../../Command');
3+
4+
const command = new Command({
5+
command: 'offline-logs',
6+
root: true,
7+
description: 'Manages offline logs',
8+
webDocs: {
9+
category: 'Logs category',
10+
subCategory: 'Logs sub category',
11+
title: 'Archives old logs to file or collection.',
12+
},
13+
builder: (yargs) => {
14+
// set options which are used in both sub-commands
15+
return yargs
16+
.option('uri', {
17+
describe: "Mongodb URI",
18+
demandOption: true,
19+
type: "string",
20+
})
21+
.option('db', {
22+
describe: "Database name",
23+
demandOption: true,
24+
type: "string",
25+
})
26+
.option('collections', {
27+
alias: "c",
28+
describe: "Source collections names",
29+
default: ["logs", "metadata"],
30+
array: true,
31+
type: "string",
32+
})
33+
.option('cutoffDate', {
34+
alias: "cod",
35+
describe:
36+
"The date in ISO format (yyyy-MM-dd) from which logs will be archived (going backwards, including logs from that day).",
37+
demandOption: true,
38+
type: "string",
39+
});
40+
},
41+
handler: () => {
42+
yargs.showHelp();
43+
},
44+
});
45+
46+
module.exports = command;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const { MongoClient, ObjectId } = require("mongodb");
2+
const moment = require('moment')
3+
const Command = require('../../Command');
4+
const cmd = require('./base.cmd');
5+
const { objectIdFromDate } = require('./utils')
6+
7+
const offloadToCollection = async function(sourceDBObj, collection, targetDB, cutoffDate) {
8+
const sourceCollectionObj = sourceDBObj.collection(collection)
9+
const targetCollection = `archive-${collection}`
10+
11+
const cutoffDateObj = moment(cutoffDate)
12+
.add(1, 'days')
13+
.startOf("day").toDate()
14+
15+
if(!cutoffDateObj.isValid()){
16+
throw new Error('please enter a valid date in ISO 8601 format')
17+
}
18+
19+
const cutoffDateId = objectIdFromDate(cutoffDateObj)
20+
21+
var result = sourceCollectionObj.aggregate([
22+
{ $match: { _id: { $lte: ObjectId(cutoffDateId) } } },
23+
{ $merge: {
24+
into: {db: targetDB, coll: targetCollection},
25+
on: "_id",
26+
whenMatched: "keepExisting",
27+
whenNotMatched: "insert"
28+
}}
29+
])
30+
31+
await result.toArray()
32+
33+
if (!result.cursorState.killed){
34+
await sourceCollectionObj.deleteMany({_id: {$lte: ObjectId(cutoffDateId)}})
35+
}
36+
else {
37+
console.error("Cursor error. Archiving operation may not be completed.")
38+
console.error("The old logs were not deleted from the source collection.")
39+
}
40+
}
41+
42+
const command = new Command({
43+
command: 'offload-to-collection',
44+
parent: cmd,
45+
description: 'Archiving logs from one or more source collections to target collections.',
46+
webDocs: {
47+
category: 'Logs',
48+
title: 'Offload To Collection',
49+
},
50+
builder: yargs => yargs
51+
.option('targetDB', {
52+
alias: 'tdb',
53+
describe: "Target database name, if none inserted, db will be defined as target.",
54+
type: "string",
55+
})
56+
.example('codefresh offline-logs offload-to-collection --uri "mongodb://192.168.99.100:27017" --db logs --c logs foo --cod "2021-07-08" '),
57+
handler: async (argv) => {
58+
const {
59+
uri,
60+
db,
61+
collections,
62+
targetDB,
63+
cutoffDate,
64+
} = argv
65+
const client = new MongoClient(uri);
66+
try{
67+
await client.connect()
68+
const failedCollections = [];
69+
const sourceDBObj = client.db(db);
70+
const promises = collections.map( async (collection) => {
71+
try{
72+
await offloadToCollection(sourceDBObj, collection, targetDB || db, cutoffDate);
73+
} catch (error) {
74+
failedCollections.push(collection)
75+
}
76+
})
77+
await Promise.all(promises)
78+
79+
if (failedCollections.length){
80+
throw new Error(`failed to offload from collections: ${failedCollections.join(', ')}`)
81+
}
82+
} finally {
83+
client.close();
84+
}
85+
},
86+
})
87+
88+
module.exports = command;
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
2+
const { MongoClient, ObjectId } = require("mongodb");
3+
const moment = require('moment')
4+
const Command = require('../../Command');
5+
const cmd = require('./base.cmd');
6+
const {objectIdFromDate, writeLogsToFile, checkRemnant} = require('./utils')
7+
const { boolean } = require("yargs");
8+
9+
const offloadToFile = async function(database, collection, chunkDuration, cutoffDate, path) {
10+
const collectionObj = database.collection(collection);
11+
12+
if (chunkDuration <= 0){
13+
throw new Error('please enter a valid chunkDuration ( > 0)')
14+
}
15+
let chunkSize = chunkDuration
16+
17+
// the cutoff date will be the start of the next day from user input (archive will include the given cutoff date)
18+
const cutoffDateObj = moment(cutoffDate, moment.ISO_8601, true)
19+
.add(1, 'days')
20+
.startOf("day");
21+
if(!cutoffDateObj.isValid()){
22+
throw new Error('please enter a valid date in ISO 8601 format')
23+
}
24+
const cutoffDateId = objectIdFromDate(cutoffDateObj.toDate())
25+
26+
const minLog = await collectionObj
27+
.find({ _id: { $lt: ObjectId(cutoffDateId) } })
28+
.sort({ _id: 1 })
29+
.limit(1)
30+
.toArray();
31+
32+
if (minLog.length === 0) {
33+
console.info(`No logs to archive in Collection ${collection} from the given date.`);
34+
return;
35+
}
36+
37+
const minLogId = ObjectId(minLog[0]._id)
38+
const minDate = moment(
39+
minLogId.getTimestamp()
40+
).startOf("day").toDate();
41+
42+
for (
43+
let lowerBound = moment(minDate);
44+
lowerBound < cutoffDateObj;
45+
lowerBound.add( chunkSize, 'days' )
46+
) {
47+
//in case total days period is not devided with chunkSize, the last chunk size needs to be smaller.
48+
const remnant = checkRemnant(lowerBound, cutoffDateObj)
49+
50+
if (remnant < chunkSize && remnant > 0) {
51+
chunkSize = remnant;
52+
}
53+
const upperBound = moment(lowerBound).add( chunkSize, 'days' );
54+
55+
const lowerBoundId = objectIdFromDate(lowerBound.toDate())
56+
const upperBoundId = objectIdFromDate(upperBound.toDate())
57+
58+
const logsToArchive = await collectionObj
59+
.find({
60+
_id: {
61+
$gte: ObjectId(lowerBoundId),
62+
$lt: ObjectId(upperBoundId),
63+
},
64+
})
65+
.toArray();
66+
67+
if (logsToArchive.length > 0) {
68+
writeLogsToFile(upperBound, lowerBound, collection, logsToArchive, path)
69+
70+
await collectionObj.deleteMany({
71+
_id: {
72+
$gte: ObjectId(lowerBoundId),
73+
$lt: ObjectId(upperBoundId),
74+
},
75+
});
76+
}
77+
}
78+
}
79+
80+
const command = new Command({
81+
command: 'offload-to-file',
82+
parent: cmd,
83+
description: 'Archiving logs from one or more source collections to files by chunks of days.',
84+
webDocs: {
85+
category: 'Logs',
86+
title: 'Offload To File',
87+
},
88+
builder: yargs => yargs
89+
.option('chunkDuration', {
90+
alias: "chdur",
91+
describe:
92+
"Chunk size in days, each chunk will be archived into a different file.",
93+
default: 1,
94+
type: "number",
95+
})
96+
.option('path', {
97+
describe: "Directory path to which archive files will be saved.",
98+
default: ".",
99+
type: "string",
100+
})
101+
.example('codefresh offline-logs offload-to-file --uri "mongodb://192.168.99.100:27017" --db logs --collections logs foo --cod "2021-07-08" --chdur 3 --path "./" '),
102+
handler: async (argv) => {
103+
const {
104+
uri,
105+
cutoffDate,
106+
chunkDuration,
107+
path,
108+
db,
109+
collections,
110+
} = argv;
111+
const client = new MongoClient(uri);
112+
try {
113+
await client.connect();
114+
const failedCollections = [];
115+
const database = client.db(db);
116+
const promises = collections.map( async ( collection ) => {
117+
try {
118+
await offloadToFile( database, collection, chunkDuration, cutoffDate, path );
119+
} catch (e) {
120+
console.log(e)
121+
failedCollections.push(collection);
122+
}
123+
})
124+
await Promise.all(promises);
125+
126+
if (failedCollections.length) {
127+
throw new Error(`failed to offload from collections: ${failedCollections.join(', ')}`)
128+
}
129+
} finally {
130+
client.close();
131+
}
132+
},
133+
});
134+
135+
module.exports = command;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const moment = require('moment')
2+
const { writeLogsToFile, checkRemnant } = require('./utils');
3+
const fs = require('fs')
4+
5+
describe('offload-to-file', () => {
6+
describe('writeLogsToFile', () => {
7+
it('writes the input data to file', () => {
8+
fs.writeFileSync = jest.fn((path, data) => {
9+
return {path, data};
10+
});
11+
const upperBound = '2021-08-10';
12+
const lowerBound = '2021-08-01';
13+
const collection = 'logs';
14+
const logsToArchive = [1, 2, 3, 4];
15+
const expectedStr = JSON.stringify(logsToArchive)
16+
const path = 'some/path';
17+
writeLogsToFile(upperBound, lowerBound, collection, logsToArchive, path);
18+
expect(fs.writeFileSync.mock.calls.length).toBe(1);
19+
expect(fs.writeFileSync.mock.calls[0][0]).toContain(path);
20+
expect(fs.writeFileSync.mock.calls[0][1]).toBe(expectedStr);
21+
});
22+
it('creates the correct file name', () => {
23+
fs.writeFileSync = jest.fn((path, data) => {
24+
return {path, data};
25+
});
26+
const upperBound = '2021-08-10';
27+
const lowerBound = '2021-08-01';
28+
const collection = 'logs';
29+
const logsToArchive = [1, 2, 3, 4];
30+
const expectedPath = 'some/path/2021-08-01-2021-08-09-logs.json'
31+
const path = 'some/path';
32+
writeLogsToFile(upperBound, lowerBound, collection, logsToArchive, path);
33+
expect(fs.writeFileSync.mock.calls.length).toBe(1);
34+
expect(fs.writeFileSync.mock.calls[0][0]).toBe(expectedPath);
35+
});
36+
it('checks leap year date calculation', () => {
37+
fs.writeFileSync = jest.fn((path, data) => {
38+
return {path, data};
39+
});
40+
const upperBound = '2020-03-01';
41+
const lowerBound = '2020-02-01';
42+
const collection = 'logs';
43+
const logsToArchive = [1, 2, 3, 4];
44+
const expectedPath = 'some/path/2020-02-01-2020-02-29-logs.json'
45+
const path = 'some/path';
46+
writeLogsToFile(upperBound, lowerBound, collection, logsToArchive, path);
47+
expect(fs.writeFileSync.mock.calls.length).toBe(1);
48+
expect(fs.writeFileSync.mock.calls[0][0]).toBe(expectedPath);
49+
});
50+
});
51+
describe('checkRemnant', () => {
52+
it('checkes days remnant to make sure time range is not going off boundaries', () => {
53+
const lowerBound = '2020-08-01';
54+
const cutoffDateObj = moment('2020-08-01').add(1, 'days').startOf("day");
55+
const result = checkRemnant(lowerBound, cutoffDateObj);
56+
expect(result).toBe(1);
57+
});
58+
});
59+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const moment = require('moment');
2+
3+
const { join } = require('path');
4+
const fs = require('fs');
5+
6+
// converts date to objectId for filter purposes
7+
const objectIdFromDate = function (date) {
8+
return Math.floor(date.getTime() / 1000).toString(16) + "0000000000000000";
9+
};
10+
11+
const writeLogsToFile = function(upperBound, lowerBound, collection, logsToArchive, path) {
12+
const date = moment(upperBound).subtract(1, "days")
13+
const fileDateRange = `${moment(lowerBound).format('YYYY-MM-DD')}-${date.format('YYYY-MM-DD')}`
14+
const fileName = `${fileDateRange}-${collection}.json`;
15+
const absPath = join(path, fileName);
16+
const data = JSON.stringify(logsToArchive);
17+
fs.writeFileSync(absPath, data);
18+
};
19+
20+
const checkRemnant = function(lowerBound, cutoffDateObj) {
21+
return Math.ceil(cutoffDateObj.diff(lowerBound , "days" ));
22+
}
23+
24+
module.exports = {objectIdFromDate, writeLogsToFile, checkRemnant};

0 commit comments

Comments
 (0)