Skip to content

Commit

Permalink
Merge pull request #15 from bcelenza/fixes/misc
Browse files Browse the repository at this point in the history
fix(format): Misc fixes to code format
  • Loading branch information
bcelenza authored Feb 5, 2023
2 parents 8ae6dbf + a41a15e commit 69c10fa
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 107 deletions.
158 changes: 89 additions & 69 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,62 @@ import {
GetQueryResultsCommandOutput,
QueryStatus,
StartQueryCommand,
StopQueryCommand,
} from '@aws-sdk/client-cloudwatch-logs';
import { Parser as CsvParser } from 'json2csv';
import json2md from 'json2md';
import * as yargs from 'yargs';
StopQueryCommand
} from "@aws-sdk/client-cloudwatch-logs";
import { Parser as CsvParser } from "json2csv";
import json2md from "json2md";
import * as yargs from "yargs";

import * as logs from './logs';
import * as time from './time';
import * as util from './util';
import * as logs from "./logs";
import * as time from "./time";
import * as util from "./util";

const args = yargs
.strictOptions()
.option('log-group', {
alias: 'l',
type: 'string',
description: 'The name of the log group',
.option("log-group", {
alias: "l",
type: "string",
description: "The name of the log group",
})
.option('format', {
alias: 'f',
type: 'string',
choices: ['csv', 'json', 'md', 'markdown'],
default: 'csv',
description: 'The format of the results',
.option("format", {
alias: "f",
type: "string",
choices: ["csv", "json", "md", "markdown"],
default: "csv",
description: "The format of the results",
})
.option('start', {
alias: 's',
type: 'string',
default: '1h',
description: 'When to start the search (duration or ISO 8601 format)',
.option("start", {
alias: "s",
type: "string",
default: "1h",
description: "When to start the search (duration or ISO 8601 format)",
})
.option('end', {
alias: 'e',
type: 'string',
default: 'now',
description: 'When to end the search (duration or ISO 8601 format)',
.option("end", {
alias: "e",
type: "string",
default: "now",
description: "When to end the search (duration or ISO 8601 format)",
})
.option('message-only', {
alias: 'm',
type: 'boolean',
.option("message-only", {
alias: "m",
type: "boolean",
default: false,
description: 'Return just the message in plain-text format (no CSV)',
description: "Return just the message in plain-text format (no CSV)",
})
.demandOption('log-group', 'At least one log group is required.')
.demandOption("log-group", "At least one log group is required.")
.help()
.alias('help', 'h').argv;
.alias("help", "h").argv;

let runningQueryId: string | undefined;
const logsClient = new CloudWatchLogsClient({});
process.on('SIGINT', async () => {
process.on("SIGINT", async () => {
if (runningQueryId) {
console.error(`Cancelling running query with ID ${runningQueryId}`);
await logsClient.send(new StopQueryCommand({
queryId: runningQueryId
}))
await logsClient.send(
new StopQueryCommand({
queryId: runningQueryId,
})
);
process.exit(0);
}
});
Expand All @@ -66,28 +68,37 @@ process.on('SIGINT', async () => {
// Parse arguments
const logGroups = args.logGroup;
if (!logGroups) {
throw new Error('A log group is required (specify with -l or --log-group)');
throw new Error("A log group is required (specify with -l or --log-group)");
}

const inputLogGroupNames: string[] = Array.isArray(logGroups) ? logGroups : [logGroups];
const inputLogGroupNames: string[] = Array.isArray(logGroups)
? logGroups
: [logGroups];

const startTime = time.parseTimeOrDuration(args.start);
const endTime = time.parseTimeOrDuration(args.end);

const query = args._[0];
if (!query) {
throw new Error('No query provided');
throw new Error("No query provided");
}
const queryString = query.toString();

// Expand log group names
const logGroupNames = await logs.expandLogGroups(inputLogGroupNames, logsClient)
const logGroupNames = await logs.expandLogGroups(
inputLogGroupNames,
logsClient
);
if (logGroupNames.size === 0) {
throw new Error('No explicit or matching log groups provided.');
throw new Error("No explicit or matching log groups provided.");
}

console.error(`Querying between ${new Date(startTime).toISOString()} and ${new Date(endTime).toISOString()}
for ${logGroupNames.size} log group(s): ${JSON.stringify([...logGroupNames])}\n`);
console.error(`Querying between ${new Date(
startTime
).toISOString()} and ${new Date(endTime).toISOString()}
for ${logGroupNames.size} log group(s): ${JSON.stringify([
...logGroupNames,
])}\n`);

// Execute the query
const startOutput = await logsClient.send(
Expand All @@ -114,19 +125,25 @@ process.on('SIGINT', async () => {
})
);
const stats = queryResults.statistics!;
const megabytesScanned = ((stats.bytesScanned || 0) / 1024 / 1024).toFixed(2);
process.stderr.write(`\r ${stats.recordsScanned} records (${megabytesScanned} MB) scanned, ${stats.recordsMatched} matched`);
const megabytesScanned = ((stats.bytesScanned || 0) / 1024 / 1024).toFixed(
2
);
process.stderr.write(
`\r ${stats.recordsScanned} records (${megabytesScanned} MB) scanned, ${stats.recordsMatched} matched`
);
} while (
queryResults.status === QueryStatus.Scheduled ||
queryResults.status === QueryStatus.Running
);
runningQueryId = undefined;
process.stderr.write('\n\n');
process.stderr.write("\n\n");

// If the command did not complete successfully, error with output
if (queryResults.status !== QueryStatus.Complete) {
throw new Error(
`Query failed with status ${queryResults.status}: ${JSON.stringify(queryResults)}`
`Query failed with status ${queryResults.status}: ${JSON.stringify(
queryResults
)}`
);
}

Expand All @@ -135,46 +152,49 @@ process.on('SIGINT', async () => {
const mappedResults = results.map((r) => {
const result: { [key: string]: string } = {};
for (const item of r) {
const field = item.field || '<no name>';
if (item.field === '@ptr') {
const field = item.field || "<no name>";
if (item.field === "@ptr") {
continue;
}

result[field] = item.value || '';
result[field] = item.value || "";
}
return result;
});

if(mappedResults.length === 0) {
console.error('No results returned.');
if (mappedResults.length === 0) {
console.error("No results returned.");
return;
}

// Output the results in the desired format
if (args['message-only']) {
mappedResults.forEach((r) => console.log(util.unescapeValue(r['@message'])));
if (args["message-only"]) {
mappedResults.forEach((r) =>
console.log(util.unescapeValue(r["@message"]))
);
} else {
switch (args.format) {
case 'csv':
case "csv":
console.log(new CsvParser().parse(mappedResults));
break;
case 'json':
case "json":
console.log(JSON.stringify(mappedResults));
break;
case 'md':
case 'markdown':
console.log(json2md([
{
table: {
headers: Object.keys(mappedResults[0]),
rows: mappedResults.map(r => Object.values(r)),
}
}
]));
case "md":
case "markdown":
console.log(
json2md([
{
table: {
headers: Object.keys(mappedResults[0]),
rows: mappedResults.map((r) => Object.values(r)),
},
},
])
);
break;
}
}

})().catch((e: Error) => {
console.error(`Error: ${e.message}`);
process.exit(1);
Expand Down
75 changes: 45 additions & 30 deletions src/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,59 @@ import {
DescribeLogGroupsCommandOutput
} from "@aws-sdk/client-cloudwatch-logs";

const getLogGroupsByPrefix = async(logGroupPrefix: string, client: CloudWatchLogsClient): Promise<string[]> => {
const logGroups: string[] = [];

let response: DescribeLogGroupsCommandOutput = {
$metadata: {}
};
do {
response = await client.send(new DescribeLogGroupsCommand({
logGroupNamePrefix: logGroupPrefix,
nextToken: response.nextToken
}));

const matchingLogGroups = response.logGroups;
if (!matchingLogGroups) {
throw new Error(`No log groups found for prefix ${logGroupPrefix}`);
}
const getLogGroupsByPrefix = async (
logGroupPrefix: string,
client: CloudWatchLogsClient
): Promise<string[]> => {
const logGroups: string[] = [];

let response: DescribeLogGroupsCommandOutput = {
$metadata: {},
};
do {
response = await client.send(
new DescribeLogGroupsCommand({
logGroupNamePrefix: logGroupPrefix,
nextToken: response.nextToken,
})
);

const matchingLogGroups = response.logGroups;
if (!matchingLogGroups) {
throw new Error(`No log groups found for prefix ${logGroupPrefix}`);
}

logGroups.push(...matchingLogGroups.map(g => g.logGroupName!));
} while (response.nextToken)
logGroups.push(...matchingLogGroups.map((g) => g.logGroupName!));
} while (response.nextToken);

return logGroups;
}
return logGroups;
};

export const expandLogGroups = async(inputLogGroupNames: string[], client: CloudWatchLogsClient): Promise<Set<string>> => {
export const expandLogGroups = async (
inputLogGroupNames: string[],
client: CloudWatchLogsClient
): Promise<Set<string>> => {
const logGroupNames = new Set<string>();
for (const inputName of inputLogGroupNames) {
if (inputName.includes('*')) {
if (inputName.includes("*")) {
// ensure only one asterisk is provided at the end
if (!inputName.endsWith('*') || (inputName.match(/\*/g) || []).length > 1) {
throw new Error(`Could not match log group by name ${inputName}: only prefix matching is supported by CloudWatch.`);
}

const matchingLogGroups = await getLogGroupsByPrefix(inputName.replace(/\*/g, ''), client);
matchingLogGroups.forEach(g => logGroupNames.add(g));
if (
!inputName.endsWith("*") ||
(inputName.match(/\*/g) || []).length > 1
) {
throw new Error(
`Could not match log group by name ${inputName}: only prefix matching is supported by CloudWatch.`
);
}

const matchingLogGroups = await getLogGroupsByPrefix(
inputName.replace(/\*/g, ""),
client
);
matchingLogGroups.forEach((g) => logGroupNames.add(g));
} else {
logGroupNames.add(inputName);
}
}
return logGroupNames;
}

};
10 changes: 6 additions & 4 deletions src/time.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import parse from 'parse-duration';
import parse from "parse-duration";

// Attempts to parse a time as a duration or ISO 8601 format, returning a unix timestamp
export const parseTimeOrDuration = (timeOrDuration: string): number => {
// special case: `now` is now
if (timeOrDuration === 'now') {
if (timeOrDuration === "now") {
return new Date().getTime();
}

// check for ISO 8601 format
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(timeOrDuration)) {
const time = Date.parse(timeOrDuration);
if (!time) {
throw new Error(`Unable to parse time as ISO 8601 format: ${timeOrDuration}`);
throw new Error(
`Unable to parse time as ISO 8601 format: ${timeOrDuration}`
);
}
return time;
}

// otherwise, try to parse as a duration
const duration = parse(timeOrDuration, 'ms');
const duration = parse(timeOrDuration, "ms");
if (!duration) {
throw new Error(`Unable to parse time as a duration: ${timeOrDuration}`);
}
Expand Down
9 changes: 5 additions & 4 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const unescapeValue = (v: string): string => {
return v.replace(new RegExp('\\\\"', 'g'),'"')
.replace(new RegExp('\\\\n', 'g'), '\n')
.trim();
}
return v
.replace(new RegExp('\\\\"', "g"), '"')
.replace(new RegExp("\\\\n", "g"), "\n")
.trim();
};

0 comments on commit 69c10fa

Please sign in to comment.