From 83989442b1513f784812f52da30a1f0bc1a25541 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 12 Dec 2023 20:03:46 +0000 Subject: [PATCH 01/11] feat(cli): Add a CLI to build deep-links to Central ELK Adds a (Deno) CLI tool to build a deep-link to Central ELK, and open it in user's browser. --- .github/workflows/ci.yaml | 12 ++++ .gitignore | 1 + cli/README.md | 33 ++++++++++ cli/deno.json | 6 ++ cli/deno.lock | 18 ++++++ cli/index.ts | 126 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+) create mode 100644 .gitignore create mode 100644 cli/README.md create mode 100644 cli/deno.json create mode 100644 cli/deno.lock create mode 100644 cli/index.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 906241f..b767efd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,18 @@ on: - main jobs: + cli: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + - name: lint + compile + working-directory: cli + run: | + deno fmt --check + deno task compile test: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..0244177 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,33 @@ +# DevX Logs CLI + +A small tool to deep-link to Central ELK. + +## Installation + +TODO. + +## 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 +``` diff --git a/cli/deno.json b/cli/deno.json new file mode 100644 index 0000000..d15e13a --- /dev/null +++ b/cli/deno.json @@ -0,0 +1,6 @@ +{ + "tasks": { + "compile": "deno compile --allow-run=open --output dist/devx-logs index.ts", + "demo": "deno run --allow-run=open index.ts --space devx --stack deploy --stage PROD --app riff-raff" + } +} diff --git a/cli/deno.lock b/cli/deno.lock new file mode 100644 index 0000000..0ec1502 --- /dev/null +++ b/cli/deno.lock @@ -0,0 +1,18 @@ +{ + "version": "3", + "redirects": { + "https://deno.land/x/is_docker/mod.ts": "https://deno.land/x/is_docker@v2.0.0/mod.ts", + "https://deno.land/x/open/index.ts": "https://deno.land/x/open@v0.0.6/index.ts" + }, + "remote": { + "https://deno.land/std@0.106.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", + "https://deno.land/std@0.106.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", + "https://deno.land/std@0.106.0/path/posix.ts": "b81974c768d298f8dcd2c720229639b3803ca4a241fa9a355c762fa2bc5ef0c1", + "https://deno.land/std@0.200.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.200.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.200.0/flags/mod.ts": "a5ac18af6583404f21ea03771f8816669d901e0ff4374020870334d6f61d73d5", + "https://deno.land/x/is_docker@v2.0.0/mod.ts": "4c8753346f4afbb6c251d7984a609aa84055559cf713fba828939a5d39c95cd0", + "https://deno.land/x/is_wsl@v1.1.0/mod.ts": "30996b09376652df7a4d495320e918154906ab94325745c1399e13e658dca5da", + "https://deno.land/x/open@v0.0.6/index.ts": "c7484a7bf2628236f33bbe354520e651811faf1a7cbc3c3f80958ce81b4c42ef" + } +} diff --git a/cli/index.ts b/cli/index.ts new file mode 100644 index 0000000..ed2e9a6 --- /dev/null +++ b/cli/index.ts @@ -0,0 +1,126 @@ +import type { Args } from "https://deno.land/std@0.200.0/flags/mod.ts"; +import { parse } from "https://deno.land/std@0.200.0/flags/mod.ts"; +import { open } from "https://deno.land/x/open@v0.0.6/index.ts"; + +export function getLink( + space: string, + filters: Record, + columns: string[], +): string { + const kibanaFilters = Object.entries(filters).map(([key, value]) => { + return `(query:(match_phrase:(${key}:'${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.join(",")}))`, + }), + }; + + const queryString = Object.entries(query) + .map(([key, value]) => `${key}=${value}`) + .join("&"); + + return `${base}?${queryString}`; +} + +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 escapeColon(str: string): string { + return str.includes(":") ? `'${str}'` : str; +} + +function parseFilters(filter: string[]): Record { + return filter.reduce((acc, curr) => { + const [key, value] = curr.split("="); + return { ...acc, [escapeColon(key)]: value }; + }, {}); +} + +function removeUndefined( + obj: Record, +): Record { + return Object.entries(obj).filter(([, value]) => !!value).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value, + }), + {}, + ); +} + +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", + ); +} + +async 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 = { + ...parseFilters(filter), + "stack.keyword": stack, + "stage.keyword": stage, + "app.keyword": app, + }; + + const filters = removeUndefined(mergedFilters); + const link = getLink(space, filters, column.map(escapeColon)); + + console.log(link); + + if (follow) { + await open(link); + } +} + +await main(Deno.args); From 54d8357ec5e3af3f9f6f1352e27f5b074449d682 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 12 Dec 2023 20:51:51 +0000 Subject: [PATCH 02/11] fix(cli): Compile for M1 Macs --- cli/deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/deno.json b/cli/deno.json index d15e13a..f1b2bc2 100644 --- a/cli/deno.json +++ b/cli/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "compile": "deno compile --allow-run=open --output dist/devx-logs index.ts", + "compile": "deno compile --allow-run=open --target aarch64-apple-darwin --output dist/devx-logs index.ts", "demo": "deno run --allow-run=open index.ts --space devx --stack deploy --stage PROD --app riff-raff" } } From d4e9805a19a1fdb61852b98fd5fb0f8fa8f5a2f9 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 12 Dec 2023 23:27:56 +0000 Subject: [PATCH 03/11] cd(cli): Release the CLI upon new tag --- .github/workflows/release-cli.yml | 32 +++++++++++++++++++++++++++++++ cli/README.md | 22 +++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 .github/workflows/release-cli.yml diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 0000000..6961cc5 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,32 @@ +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 + compile + working-directory: cli + run: | + deno fmt --check + 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 diff --git a/cli/README.md b/cli/README.md index 0244177..104c09a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -31,3 +31,25 @@ 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. From a60705e4abbb2405bbb51bf6b1dace75d0eb1a0f Mon Sep 17 00:00:00 2001 From: akash1810 Date: Wed, 13 Dec 2023 10:58:37 +0000 Subject: [PATCH 04/11] docs(cli): Add installation instructions Requires https://github.com/guardian/homebrew-devtools/pull/96. --- cli/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index 104c09a..278cd95 100644 --- a/cli/README.md +++ b/cli/README.md @@ -2,9 +2,15 @@ A small tool to deep-link to Central ELK. -## Installation +## Installation via homebrew -TODO. +```bash +brew tap guardian/homebrew-devtools +brew install guardian/devtools/devx-logs + +# update +brew upgrade devx-logs +``` ## Usage From 9131b16bf6b7f56b080085c793e63619819aedbb Mon Sep 17 00:00:00 2001 From: akash1810 Date: Wed, 13 Dec 2023 17:45:56 +0000 Subject: [PATCH 05/11] test(cli): Unit test the core logic --- .github/workflows/ci.yaml | 3 +- .github/workflows/release-cli.yml | 3 +- cli/deno.lock | 32 ++++++++++++++++++ cli/elk.test.ts | 35 +++++++++++++++++++ cli/elk.ts | 31 +++++++++++++++++ cli/index.ts | 56 ++----------------------------- cli/transform.test.ts | 42 +++++++++++++++++++++++ cli/transform.ts | 24 +++++++++++++ 8 files changed, 171 insertions(+), 55 deletions(-) create mode 100644 cli/elk.test.ts create mode 100644 cli/elk.ts create mode 100644 cli/transform.test.ts create mode 100644 cli/transform.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b767efd..038ecd8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,10 +18,11 @@ jobs: - uses: denoland/setup-deno@v1 with: deno-version: v1.x - - name: lint + compile + - name: lint, test, compile working-directory: cli run: | deno fmt --check + deno test deno task compile test: runs-on: ubuntu-latest diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 6961cc5..b8a388a 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -18,10 +18,11 @@ jobs: - uses: denoland/setup-deno@v1 with: deno-version: v1.x - - name: lint + compile + - name: lint, test, compile working-directory: cli run: | deno fmt --check + deno test deno task compile - name: release working-directory: cli/dist diff --git a/cli/deno.lock b/cli/deno.lock index 0ec1502..6bb787d 100644 --- a/cli/deno.lock +++ b/cli/deno.lock @@ -11,6 +11,38 @@ "https://deno.land/std@0.200.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.200.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", "https://deno.land/std@0.200.0/flags/mod.ts": "a5ac18af6583404f21ea03771f8816669d901e0ff4374020870334d6f61d73d5", + "https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", + "https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99", + "https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2", "https://deno.land/x/is_docker@v2.0.0/mod.ts": "4c8753346f4afbb6c251d7984a609aa84055559cf713fba828939a5d39c95cd0", "https://deno.land/x/is_wsl@v1.1.0/mod.ts": "30996b09376652df7a4d495320e918154906ab94325745c1399e13e658dca5da", "https://deno.land/x/open@v0.0.6/index.ts": "c7484a7bf2628236f33bbe354520e651811faf1a7cbc3c3f80958ce81b4c42ef" diff --git a/cli/elk.test.ts b/cli/elk.test.ts new file mode 100644 index 0000000..d58c9b2 --- /dev/null +++ b/cli/elk.test.ts @@ -0,0 +1,35 @@ +import { assertEquals } from "https://deno.land/std@0.208.0/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); +}); diff --git a/cli/elk.ts b/cli/elk.ts new file mode 100644 index 0000000..3564f70 --- /dev/null +++ b/cli/elk.ts @@ -0,0 +1,31 @@ +function escapeColon(str: string): string { + return str.includes(":") ? `'${str}'` : str; +} +export function getLink( + space: string, + filters: Record, + columns: string[] = [], +): string { + const kibanaFilters = Object.entries(filters).map(([key, value]) => { + return `(query:(match_phrase:(${escapeColon(key)}:'${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(escapeColon).join(",")}))`, + }), + }; + + const queryString = Object.entries(query) + .map(([key, value]) => `${key}=${value}`) + .join("&"); + + return `${base}?${queryString}`; +} diff --git a/cli/index.ts b/cli/index.ts index ed2e9a6..2713fa6 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,35 +1,8 @@ import type { Args } from "https://deno.land/std@0.200.0/flags/mod.ts"; import { parse } from "https://deno.land/std@0.200.0/flags/mod.ts"; import { open } from "https://deno.land/x/open@v0.0.6/index.ts"; - -export function getLink( - space: string, - filters: Record, - columns: string[], -): string { - const kibanaFilters = Object.entries(filters).map(([key, value]) => { - return `(query:(match_phrase:(${key}:'${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.join(",")}))`, - }), - }; - - const queryString = Object.entries(query) - .map(([key, value]) => `${key}=${value}`) - .join("&"); - - return `${base}?${queryString}`; -} +import { getLink } from "./elk.ts"; +import { parseFilters, removeUndefined } from "./transform.ts"; function parseArguments(args: string[]): Args { return parse(args, { @@ -48,29 +21,6 @@ function parseArguments(args: string[]): Args { }); } -function escapeColon(str: string): string { - return str.includes(":") ? `'${str}'` : str; -} - -function parseFilters(filter: string[]): Record { - return filter.reduce((acc, curr) => { - const [key, value] = curr.split("="); - return { ...acc, [escapeColon(key)]: value }; - }, {}); -} - -function removeUndefined( - obj: Record, -): Record { - return Object.entries(obj).filter(([, value]) => !!value).reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: value, - }), - {}, - ); -} - function printHelp(): void { console.log(`Usage: devx-logs [OPTIONS...]`); console.log("\nOptional flags:"); @@ -114,7 +64,7 @@ async function main(inputArgs: string[]) { }; const filters = removeUndefined(mergedFilters); - const link = getLink(space, filters, column.map(escapeColon)); + const link = getLink(space, filters, column); console.log(link); diff --git a/cli/transform.test.ts b/cli/transform.test.ts new file mode 100644 index 0000000..852e40e --- /dev/null +++ b/cli/transform.test.ts @@ -0,0 +1,42 @@ +import { assertEquals } from "https://deno.land/std@0.208.0/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); +}); diff --git a/cli/transform.ts b/cli/transform.ts new file mode 100644 index 0000000..3014e29 --- /dev/null +++ b/cli/transform.ts @@ -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 { + 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, +): Record { + return Object.entries(obj).filter(([, value]) => !!value).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value, + }), + {}, + ); +} From 1ee363e6a5792db3fc638588a23357cdf10ed812 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Wed, 13 Dec 2023 18:05:21 +0000 Subject: [PATCH 06/11] refactor(cli): Remove dependency https://deno.land/x/open@v0.0.6 --- cli/deno.lock | 12 +----------- cli/index.ts | 7 +++---- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/cli/deno.lock b/cli/deno.lock index 6bb787d..a89f022 100644 --- a/cli/deno.lock +++ b/cli/deno.lock @@ -1,13 +1,6 @@ { "version": "3", - "redirects": { - "https://deno.land/x/is_docker/mod.ts": "https://deno.land/x/is_docker@v2.0.0/mod.ts", - "https://deno.land/x/open/index.ts": "https://deno.land/x/open@v0.0.6/index.ts" - }, "remote": { - "https://deno.land/std@0.106.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", - "https://deno.land/std@0.106.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", - "https://deno.land/std@0.106.0/path/posix.ts": "b81974c768d298f8dcd2c720229639b3803ca4a241fa9a355c762fa2bc5ef0c1", "https://deno.land/std@0.200.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.200.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", "https://deno.land/std@0.200.0/flags/mod.ts": "a5ac18af6583404f21ea03771f8816669d901e0ff4374020870334d6f61d73d5", @@ -42,9 +35,6 @@ "https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", "https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", "https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", - "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2", - "https://deno.land/x/is_docker@v2.0.0/mod.ts": "4c8753346f4afbb6c251d7984a609aa84055559cf713fba828939a5d39c95cd0", - "https://deno.land/x/is_wsl@v1.1.0/mod.ts": "30996b09376652df7a4d495320e918154906ab94325745c1399e13e658dca5da", - "https://deno.land/x/open@v0.0.6/index.ts": "c7484a7bf2628236f33bbe354520e651811faf1a7cbc3c3f80958ce81b4c42ef" + "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2" } } diff --git a/cli/index.ts b/cli/index.ts index 2713fa6..0f15d14 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,6 +1,5 @@ import type { Args } from "https://deno.land/std@0.200.0/flags/mod.ts"; import { parse } from "https://deno.land/std@0.200.0/flags/mod.ts"; -import { open } from "https://deno.land/x/open@v0.0.6/index.ts"; import { getLink } from "./elk.ts"; import { parseFilters, removeUndefined } from "./transform.ts"; @@ -46,7 +45,7 @@ function printHelp(): void { ); } -async function main(inputArgs: string[]) { +function main(inputArgs: string[]) { const args = parseArguments(inputArgs); if (args.help) { @@ -69,8 +68,8 @@ async function main(inputArgs: string[]) { console.log(link); if (follow) { - await open(link); + new Deno.Command("open", { args: [link] }).spawn(); } } -await main(Deno.args); +main(Deno.args); From 56a84ba78801a3c76b252b898fdc1c902db107e9 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Wed, 13 Dec 2023 19:26:44 +0000 Subject: [PATCH 07/11] refactor(cli): Simplify URL building by wrapping every part --- cli/elk.test.ts | 6 +++--- cli/elk.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cli/elk.test.ts b/cli/elk.test.ts index d58c9b2..b0bbdfa 100644 --- a/cli/elk.test.ts +++ b/cli/elk.test.ts @@ -6,7 +6,7 @@ import { getLink } from "./elk.ts"; 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')))))"; + "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); }); @@ -16,7 +16,7 @@ Deno.test("getLink with columns", () => { "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))"; + "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); }); @@ -30,6 +30,6 @@ Deno.test("getLink with colon(:) input", () => { 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'))"; + "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); }); diff --git a/cli/elk.ts b/cli/elk.ts index 3564f70..97c0123 100644 --- a/cli/elk.ts +++ b/cli/elk.ts @@ -1,13 +1,17 @@ -function escapeColon(str: string): string { - return str.includes(":") ? `'${str}'` : str; +/** + * 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, columns: string[] = [], ): string { const kibanaFilters = Object.entries(filters).map(([key, value]) => { - return `(query:(match_phrase:(${escapeColon(key)}:'${value}')))`; + return `(query:(match_phrase:(${wrapString(key)}:${wrapString(value)})))`; }); // The `#/` at the end is important for Kibana to correctly parse the query string @@ -19,7 +23,7 @@ export function getLink( _g: `(filters:!(${kibanaFilters.join(",")}))`, }), ...(columns.length > 0 && { - _a: `(columns:!(${columns.map(escapeColon).join(",")}))`, + _a: `(columns:!(${columns.map(wrapString).join(",")}))`, }), }; From 5cf85ac6d676457d8581656ef765a730cadb7cdf Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 14 Dec 2023 00:06:25 +0000 Subject: [PATCH 08/11] style(cli): Follow Deno style for filenames See https://docs.deno.com/runtime/manual/references/contributing/style_guide#do-not-use-the-filename-indextsindexjs. --- cli/deno.json | 4 ++-- cli/{index.ts => main.ts} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename cli/{index.ts => main.ts} (100%) diff --git a/cli/deno.json b/cli/deno.json index f1b2bc2..94ccffd 100644 --- a/cli/deno.json +++ b/cli/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "compile": "deno compile --allow-run=open --target aarch64-apple-darwin --output dist/devx-logs index.ts", - "demo": "deno run --allow-run=open index.ts --space devx --stack deploy --stage PROD --app riff-raff" + "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" } } diff --git a/cli/index.ts b/cli/main.ts similarity index 100% rename from cli/index.ts rename to cli/main.ts From 30b25677b4cb74ad5a55caa01516930ee6919449 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 14 Dec 2023 00:09:16 +0000 Subject: [PATCH 09/11] style(cli): Follow Deno style for test filenames See https://docs.deno.com/runtime/manual/references/contributing/style_guide#each-module-should-come-with-a-test-module. --- cli/{elk.test.ts => elk_test.ts} | 0 cli/{transform.test.ts => transform_test.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename cli/{elk.test.ts => elk_test.ts} (100%) rename cli/{transform.test.ts => transform_test.ts} (100%) diff --git a/cli/elk.test.ts b/cli/elk_test.ts similarity index 100% rename from cli/elk.test.ts rename to cli/elk_test.ts diff --git a/cli/transform.test.ts b/cli/transform_test.ts similarity index 100% rename from cli/transform.test.ts rename to cli/transform_test.ts From 87f929b87d4438cbc3eec35030d081ce011cff2c Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 14 Dec 2023 21:32:55 +0000 Subject: [PATCH 10/11] fix(cli): Run main function only when running file directly This'll allow `main.ts` to be unit tested. --- cli/main.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/main.ts b/cli/main.ts index 0f15d14..b38ea6e 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -72,4 +72,7 @@ function main(inputArgs: string[]) { } } -main(Deno.args); +// Learn more at https://deno.land/manual/examples/module_metadata#concepts +if (import.meta.main) { + main(Deno.args); +} From 824038db8ba5d877ebe1b0c09ae6c2a994978ba7 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 19 Dec 2023 11:17:36 +0000 Subject: [PATCH 11/11] test: Unit test `removeUndefined` with "0" input --- cli/transform_test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cli/transform_test.ts b/cli/transform_test.ts index 852e40e..ae0aad0 100644 --- a/cli/transform_test.ts +++ b/cli/transform_test.ts @@ -40,3 +40,13 @@ Deno.test("removeUndefined", () => { }; assertEquals(got, want); }); + +Deno.test("removeUndefined where the RHS is 0", () => { + const got = removeUndefined({ + errors: "0", + }); + const want = { + errors: "0", + }; + assertEquals(got, want); +});