Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(log): add log module #4

Merged
merged 22 commits into from
Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Pull Request

on:
pull_request:
branches: [main]

env:
CACHE_VERSION: 1
DENO_DIR: .deno

concurrency:
group: ${{ github.workflow }}-${{ github.run_id }}
cancel-in-progress: true

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
# Configure the Github Actions runner to use Linux-style line endings before
# running the `actions/checkout` step due to a known issue with handling
# Windows-style line endings (CRLF)
# https://github.com/actions/checkout/issues/135
- name: Set git to use LF
if: matrix.os == 'windows-latest'
run: |
git config --global core.autocrlf false
git config --global core.eol lf

- name: Close repository
uses: actions/checkout@v2

- name: Setup deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x

- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ env.DENO_DIR }}
key: ${{ env.CACHE_VERSION }}-${{ hashFiles('lock.json') }}

- name: Run all tests
run: deno task test

- name: Generate test coverage report
run: deno coverage ./cov --lcov > cov.lcov

- name: Upload coverage
uses: codecov/codecov-action@v2
with:
name: ${{ matrix.os }}
files: cov.lcov

lint:
runs-on: ubuntu-latest
steps:
- name: Close repository
uses: actions/checkout@v2

- name: Setup deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x

- name: Check format
run: deno fmt --check

- name: Lint
run: deno lint
8 changes: 3 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,vscode,jetbrains,vim,nova
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,vscode,jetbrains,vim,nova
# Coverage
cov
*.lcov

### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
Expand Down Expand Up @@ -126,6 +127,3 @@ $RECYCLE.BIN/

# Windows shortcuts
*.lnk

# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,vscode,jetbrains,vim,nova

18 changes: 9 additions & 9 deletions cli.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { parse } from "https://deno.land/std@0.95.0/flags/mod.ts";
import { parse } from "flags/mod.ts";

import * as log from "./log/mod.ts";
import type { Command } from "./console/mod.ts";
import { default as logger } from "./log/logger.ts";
import { type Command } from "./console/mod.ts";

/**
* Initializes an Atlas application
*/
const init: Command = {
name: "init",
description: "Initializes an Atlas application",
handler: async (args) => {
handler: () => {
// TBD
await log.info("atlas init", args);
logger.debug(`atlas init`);
},
};

Expand All @@ -24,9 +24,9 @@ const start: Command = {
help: `
--port The port where to start the application listener
`,
handler: async (args) => {
handler: () => {
// TBD
await log.info("atlas start", args);
logger.debug(`atlas start`);
},
};

Expand All @@ -43,9 +43,9 @@ if (import.meta.main) {
try {
await commands.get(command)?.handler(args);
} catch (err) {
log.error(`Command '${command}' failed`, err.message);
logger.error(`command '${command}' failed with message: ${err.message}`);
}
} else {
log.error(`Command '${command}' not found`);
logger.error(`command '${command}' not found`);
}
}
2 changes: 1 addition & 1 deletion console/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ export type Command = {
/** Indicates whether the command should be shown in the commands list. */
hidden?: boolean;
/** The command handler function. */
handler: (args: unknown) => Promise<void>;
handler: (args: unknown) => Promise<void> | void;
};
25 changes: 25 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"fmt": {
"files": {
"exclude": [
".git",
".deno",
"cov"
]
}
},
"importMap": "import_map.json",
"lint": {
"files": {
"exclude": [
".git",
".deno",
"cov"
]
}
},
"tasks": {
"cache": "deno cache --reload --lock=lock.json --lock-write cli.ts log/mod.ts console/mod.ts",
"test": "deno test --unstable --allow-all --coverage=./cov"
}
}
7 changes: 7 additions & 0 deletions import_map.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"imports": {
"testing/": "https://deno.land/std@0.130.0/testing/",
"fmt/": "https://deno.land/std@0.130.0/fmt/",
"flags/": "https://deno.land/std@0.130.0/flags/"
}
}
5 changes: 5 additions & 0 deletions lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"https://deno.land/std@0.130.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
"https://deno.land/std@0.130.0/flags/mod.ts": "430cf2d1c26e00286373b2647ebdca637f7558505e88e9c108a4742cd184c916",
"https://deno.land/std@0.130.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37"
}
17 changes: 17 additions & 0 deletions log/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { LogMessage } from "./message.ts";

export abstract class LogHandler {
/** Formats a log message for the handler */
format(message: LogMessage): string {
return message.value;
}

/** Handles a log message */
handle(message: LogMessage): void {
const formatted = this.format(message);
return this.log(formatted);
}

/** Logs a message */
abstract log(message: string): void;
}
75 changes: 75 additions & 0 deletions log/handlers/console.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { assertEquals, assertStringIncludes } from "testing/asserts.ts";
import { LogLevel } from "../level.ts";
import { LogMessage } from "../message.ts";
import { ConsoleHandler } from "./console.ts";

class TestWriter implements Deno.WriterSync {
public buffer: Uint8Array = new Uint8Array();

writeSync(data: Uint8Array): number {
this.buffer = data;
return data.buffer.byteLength;
}
}

Deno.test("prints a timestamp for a message", () => {
const message = new LogMessage(LogLevel.DEBUG, "hello");
const writer = new TestWriter();
const handler = new ConsoleHandler({
color: false,
target: writer,
timestamp: true,
});

handler.handle(message);

assertEquals<string>(
new TextDecoder().decode(writer.buffer),
`${new Date(message.time).toISOString()} debug hello\n`,
);
});

Deno.test("formats a message to JSON", () => {
const message = new LogMessage(LogLevel.DEBUG, "hello");
const writer = new TestWriter();
const handler = new ConsoleHandler({
json: true,
target: writer,
});

handler.handle(message);

assertEquals<string>(
new TextDecoder().decode(writer.buffer),
`{"level":"debug","message":"hello"}\n`,
);
});

Deno.test("colorizes message level", () => {
const message = new LogMessage(LogLevel.DEBUG, "hello");
const writer = new TestWriter();
const handler = new ConsoleHandler({
color: true,
target: writer,
});

handler.handle(message);

// [90mdebug[39m
// ^^^^---------
assertStringIncludes(
new TextDecoder().decode(writer.buffer).slice(0, 5),
"[90m",
);

// [90mdebug[39m
// ---------^^^^
assertStringIncludes(
new TextDecoder().decode(writer.buffer).slice(11, 15),
"[39m",
);
});

// TODO(gabrielizaias): figure out how to mock stdout/stderr
// Deno.test("defaults to stdout for `debug`, `info`, `notice`, and `warning`", () => {});
// Deno.test("defaults to stderr for `error`, `critical`, `alert`, and `emergency`", () => {});
115 changes: 115 additions & 0 deletions log/handlers/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as colors from "fmt/colors.ts";
import { LogHandler } from "../handler.ts";
import { LogMessage } from "../message.ts";
import { getLevelValue, LogLevel } from "../level.ts";

type ConsoleHandlerOptions = {
/** Show colors on the console output. Defaults to `true` */
color?: boolean;

/** Add timestamp to the log message. Defaults to `false` */
timestamp?: boolean;

/** Format log message as JSON. Defaults to `false` */
json?: boolean;

/**
* Target for the log messages.
*
* | Default | Level |
* |:--------------|:----------------------------------------------|
* | `Deno.stdout` | `debug`, `info`, `notice`, and `warning` |
* | `Deno.stderr` | `error`, `critical`, `alert`, and `emergency` |
*/
target?: Deno.WriterSync;
};

export class ConsoleHandler extends LogHandler {
#color: boolean;
#json: boolean;
#target: Deno.WriterSync;
#timestamp: boolean;

constructor(options?: ConsoleHandlerOptions) {
super();
this.#color = options?.color ?? true;
this.#json = options?.json ?? false;
this.#target = options?.target ?? Deno.stdout;
this.#timestamp = options?.timestamp ?? false;
}

override handle(message: LogMessage): void {
if (
this.#target === Deno.stdout &&
getLevelValue(message.level) <= getLevelValue(LogLevel.ERROR)
) {
this.#target = Deno.stderr;
}

super.handle(message);
}

override format(message: LogMessage): string {
if (this.#json) {
return JSON.stringify({
...(this.#timestamp && { timestamp: message.time }),
level: message.level,
message: message.value,
});
}

const output = [];

if (this.#timestamp) {
if (this.#color) {
output.push(colors.gray(message.time));
} else {
output.push(message.time);
}
}

const level = this.#formatLevel(message.level);

output.push(level, message.value);

return output.join(" ");
}

log(message: string): void {
this.#target.writeSync(
new TextEncoder().encode(`${message}\n`),
);
}

#formatLevel(level: LogLevel): string {
if (this.#color) {
switch (level) {
case LogLevel.EMERGENCY:
return colors.bgRed(` ${level.toUpperCase()} `);

case LogLevel.ALERT:
return colors.red(level.toUpperCase());

case LogLevel.CRITICAL:
return colors.red(level);

case LogLevel.ERROR:
return colors.magenta(level);

case LogLevel.WARNING:
return colors.yellow(level);

case LogLevel.NOTICE:
return colors.cyan(level);

case LogLevel.INFO:
return colors.blue(level);

case LogLevel.DEBUG:
return colors.gray(level);
}
}

return level;
}
}
Loading