-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from guardian/aa/cli
feat(cli): Add a CLI to build deep-links to Central ELK
- Loading branch information
Showing
11 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: Release CLI | ||
|
||
on: | ||
push: | ||
tags: | ||
- cli-v* | ||
|
||
jobs: | ||
release: | ||
runs-on: ubuntu-latest | ||
|
||
permissions: | ||
contents: write | ||
packages: write | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: denoland/setup-deno@v1 | ||
with: | ||
deno-version: v1.x | ||
- name: lint, test, compile | ||
working-directory: cli | ||
run: | | ||
deno fmt --check | ||
deno test | ||
deno task compile | ||
- name: release | ||
working-directory: cli/dist | ||
env: | ||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: | | ||
sha256sum devx-logs > checksum.txt | ||
gh release create ${{ github.ref }} * --generate-notes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# DevX Logs CLI | ||
|
||
A small tool to deep-link to Central ELK. | ||
|
||
## Installation via homebrew | ||
|
||
```bash | ||
brew tap guardian/homebrew-devtools | ||
brew install guardian/devtools/devx-logs | ||
|
||
# update | ||
brew upgrade devx-logs | ||
``` | ||
|
||
## Usage | ||
|
||
- Open the logs for Riff-Raff in PROD | ||
```bash | ||
devx-logs --space devx --stage PROD --app riff-raff | ||
``` | ||
- Display the URL for logs from Riff-Raff in PROD | ||
```bash | ||
devx-logs --space devx --stage PROD --app riff-raff --no-follow | ||
``` | ||
- Open the logs for Riff-Raff in PROD, where the level is INFO, and show the | ||
message and logger_name columns | ||
```bash | ||
devx-logs --space devx --stage PROD --app riff-raff --filter level=INFO --filter region=eu-west-1 --column message --column logger_name | ||
``` | ||
- Open the logs for the repository 'guardian/prism': | ||
```bash | ||
devx-logs --filter gu:repo.keyword=guardian/prism --column message --column gu:repo | ||
``` | ||
|
||
See all options via the `--help` flag: | ||
|
||
```bash | ||
devx-logs --help | ||
``` | ||
|
||
## Releasing | ||
|
||
Releasing is semi-automated. To release a new version, create a new tag with the | ||
`cli-v` prefix: | ||
|
||
```bash | ||
git tag cli-v0.0.1 | ||
``` | ||
|
||
And then push the tag: | ||
|
||
```bash | ||
git push --tags | ||
``` | ||
|
||
This will trigger [a GitHub Action](../.github/workflows/release-cli.yml), | ||
publishing a new version to GitHub releases. | ||
|
||
Once a new release is available, update the | ||
[Homebrew formula](https://github.com/guardian/homebrew-devtools/blob/main/Formula/devx-logs.rb) | ||
to point to the new version. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"tasks": { | ||
"compile": "deno compile --allow-run=open --target aarch64-apple-darwin --output dist/devx-logs main.ts", | ||
"demo": "deno run --allow-run=open main.ts --space devx --stack deploy --stage PROD --app riff-raff" | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/** | ||
* Wrap a string in single quotes so Kibana can parse it correctly | ||
*/ | ||
function wrapString(str: string): string { | ||
return `'${str}'`; | ||
} | ||
|
||
export function getLink( | ||
space: string, | ||
filters: Record<string, string>, | ||
columns: string[] = [], | ||
): string { | ||
const kibanaFilters = Object.entries(filters).map(([key, value]) => { | ||
return `(query:(match_phrase:(${wrapString(key)}:${wrapString(value)})))`; | ||
}); | ||
|
||
// The `#/` at the end is important for Kibana to correctly parse the query string | ||
// The `URL` object moves this to the end of the string, which breaks the link. | ||
const base = `https://logs.gutools.co.uk/s/${space}/app/discover#/`; | ||
|
||
const query = { | ||
...(kibanaFilters.length > 0 && { | ||
_g: `(filters:!(${kibanaFilters.join(",")}))`, | ||
}), | ||
...(columns.length > 0 && { | ||
_a: `(columns:!(${columns.map(wrapString).join(",")}))`, | ||
}), | ||
}; | ||
|
||
const queryString = Object.entries(query) | ||
.map(([key, value]) => `${key}=${value}`) | ||
.join("&"); | ||
|
||
return `${base}?${queryString}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts"; | ||
import { getLink } from "./elk.ts"; | ||
|
||
// NOTE: Each of these URLs should be opened in a browser to verify that they work as expected. | ||
|
||
Deno.test("getLink with simple input", () => { | ||
const got = getLink("devx", { app: "riff-raff", stage: "PROD" }); | ||
const want = | ||
"https://logs.gutools.co.uk/s/devx/app/discover#/?_g=(filters:!((query:(match_phrase:('app':'riff-raff'))),(query:(match_phrase:('stage':'PROD')))))"; | ||
assertEquals(got, want); | ||
}); | ||
|
||
Deno.test("getLink with columns", () => { | ||
const got = getLink("devx", { app: "riff-raff", stage: "PROD" }, [ | ||
"message", | ||
"level", | ||
]); | ||
const want = | ||
"https://logs.gutools.co.uk/s/devx/app/discover#/?_g=(filters:!((query:(match_phrase:('app':'riff-raff'))),(query:(match_phrase:('stage':'PROD')))))&_a=(columns:!('message','level'))"; | ||
assertEquals(got, want); | ||
}); | ||
|
||
/* | ||
Filters and columns with colon(:) input should get wrapped in single quotes(') so that Kibana can parse them correctly. | ||
That is, gu:repo should become 'gu:repo'. | ||
*/ | ||
Deno.test("getLink with colon(:) input", () => { | ||
const got = getLink("devx", { | ||
"gu:repo.keyword": "guardian/amigo", | ||
stage: "PROD", | ||
}, ["message", "gu:repo"]); | ||
const want = | ||
"https://logs.gutools.co.uk/s/devx/app/discover#/?_g=(filters:!((query:(match_phrase:('gu:repo.keyword':'guardian/amigo'))),(query:(match_phrase:('stage':'PROD')))))&_a=(columns:!('message','gu:repo'))"; | ||
assertEquals(got, want); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import type { Args } from "https://deno.land/[email protected]/flags/mod.ts"; | ||
import { parse } from "https://deno.land/[email protected]/flags/mod.ts"; | ||
import { getLink } from "./elk.ts"; | ||
import { parseFilters, removeUndefined } from "./transform.ts"; | ||
|
||
function parseArguments(args: string[]): Args { | ||
return parse(args, { | ||
boolean: ["follow"], | ||
negatable: ["follow"], | ||
string: ["space", "stack", "stage", "app"], | ||
collect: ["column", "filter"], | ||
stopEarly: false, | ||
"--": true, | ||
default: { | ||
follow: true, | ||
column: ["message"], | ||
space: "default", | ||
filter: [], | ||
}, | ||
}); | ||
} | ||
|
||
function printHelp(): void { | ||
console.log(`Usage: devx-logs [OPTIONS...]`); | ||
console.log("\nOptional flags:"); | ||
console.log(" --help Display this help and exit"); | ||
console.log(" --space The Kibana space to use"); | ||
console.log(" --stack The stack tag to filter by"); | ||
console.log(" --stage The stage tag to filter by"); | ||
console.log(" --app The app tag to filter by"); | ||
console.log( | ||
" --column Which columns to display. Multiple: true. Default: 'message'", | ||
); | ||
console.log( | ||
" --filter Additional filters to apply. Multiple: true. Format: key=value", | ||
); | ||
console.log(" --no-follow Don't open the link in the browser"); | ||
console.log("\nExample:"); | ||
console.log( | ||
" devx-logs --space devx --stack deploy --stage PROD --app riff-raff", | ||
); | ||
console.log("\nAdvanced example:"); | ||
console.log( | ||
" devx-logs --space devx --stack deploy --stage PROD --app riff-raff --filter level=INFO --filter region=eu-west-1 --column message --column logger_name", | ||
); | ||
} | ||
|
||
function main(inputArgs: string[]) { | ||
const args = parseArguments(inputArgs); | ||
|
||
if (args.help) { | ||
printHelp(); | ||
Deno.exit(0); | ||
} | ||
|
||
const { space, stack, stage, app, column, filter, follow } = args; | ||
|
||
const mergedFilters: Record<string, string | undefined> = { | ||
...parseFilters(filter), | ||
"stack.keyword": stack, | ||
"stage.keyword": stage, | ||
"app.keyword": app, | ||
}; | ||
|
||
const filters = removeUndefined(mergedFilters); | ||
const link = getLink(space, filters, column); | ||
|
||
console.log(link); | ||
|
||
if (follow) { | ||
new Deno.Command("open", { args: [link] }).spawn(); | ||
} | ||
} | ||
|
||
// Learn more at https://deno.land/manual/examples/module_metadata#concepts | ||
if (import.meta.main) { | ||
main(Deno.args); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/** | ||
* Turn an array of strings of form `key=value` into an object of form `{ key: value }` | ||
*/ | ||
export function parseFilters(filter: string[]): Record<string, unknown> { | ||
return filter.reduce((acc, curr) => { | ||
const [key, value] = curr.split("="); | ||
return { ...acc, [key]: value }; | ||
}, {}); | ||
} | ||
|
||
/** | ||
* Remove keys from a `Record` whose value is falsy | ||
*/ | ||
export function removeUndefined( | ||
obj: Record<string, string | undefined>, | ||
): Record<string, string> { | ||
return Object.entries(obj).filter(([, value]) => !!value).reduce( | ||
(acc, [key, value]) => ({ | ||
...acc, | ||
[key]: value, | ||
}), | ||
{}, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { assertEquals } from "https://deno.land/[email protected]/assert/assert_equals.ts"; | ||
import { parseFilters, removeUndefined } from "./transform.ts"; | ||
|
||
Deno.test("parseFilters", () => { | ||
const got = parseFilters(["stack=deploy", "stage=PROD", "app=riff-raff"]); | ||
const want = { | ||
stack: "deploy", | ||
stage: "PROD", | ||
app: "riff-raff", | ||
}; | ||
assertEquals(got, want); | ||
}); | ||
|
||
Deno.test("parseFilters without an = delimiter", () => { | ||
const got = parseFilters(["message"]); | ||
const want = { | ||
message: undefined, | ||
}; | ||
assertEquals(got, want); | ||
}); | ||
|
||
Deno.test("parseFilters without a value on the RHS of =", () => { | ||
const got = parseFilters(["name="]); | ||
const want = { | ||
name: "", | ||
}; | ||
assertEquals(got, want); | ||
}); | ||
|
||
Deno.test("removeUndefined", () => { | ||
const got = removeUndefined({ | ||
stack: "deploy", | ||
stage: undefined, | ||
app: "riff-raff", | ||
team: "", | ||
}); | ||
const want = { | ||
stack: "deploy", | ||
app: "riff-raff", | ||
}; | ||
assertEquals(got, want); | ||
}); | ||
|
||
Deno.test("removeUndefined where the RHS is 0", () => { | ||
const got = removeUndefined({ | ||
errors: "0", | ||
}); | ||
const want = { | ||
errors: "0", | ||
}; | ||
assertEquals(got, want); | ||
}); |