feat(reporter-pino): first version of the package
This commit is contained in:
parent
d916043061
commit
3619d86750
6 changed files with 433 additions and 0 deletions
64
packages/reporter-pino/README.md
Normal file
64
packages/reporter-pino/README.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# @emigrate/reporter-pino
|
||||
|
||||
A [Pino](https://getpino.io/#/) reporter for Emigrate which logs the migration progress using line delimited JSON by default.
|
||||
Which is great both in production environments and for piping the output to other tools.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the reporter in your project, alongside the Emigrate CLI:
|
||||
|
||||
```bash
|
||||
npm install --save-dev @emigrate/cli @emigrate/reporter-pino
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### With default options
|
||||
|
||||
Configure the reporter in your `emigrate.config.js` file:
|
||||
|
||||
```js
|
||||
import reporterPino from '@emigrate/reporter-pino';
|
||||
|
||||
export default {
|
||||
directory: 'migrations',
|
||||
reporter: reporterPino,
|
||||
};
|
||||
```
|
||||
|
||||
Or simply:
|
||||
|
||||
```js
|
||||
export default {
|
||||
directory: 'migrations',
|
||||
reporter: 'pino', // the @emigrate/reporter- prefix is optional
|
||||
};
|
||||
```
|
||||
|
||||
Or use the CLI option `--reporter` (or `-r`):
|
||||
|
||||
```bash
|
||||
emigrate up --reporter pino # the @emigrate/reporter- prefix is optional
|
||||
```
|
||||
|
||||
### With custom options
|
||||
|
||||
Configure the reporter in your `emigrate.config.js` file:
|
||||
|
||||
```js
|
||||
import { createPinoReporter } from '@emigrate/reporter-pino';
|
||||
|
||||
export default {
|
||||
directory: 'migrations',
|
||||
reporter: createPinoReporter({
|
||||
level: 'error', // default is 'info'
|
||||
errorKey: 'err', // default is 'error'
|
||||
}),
|
||||
};
|
||||
```
|
||||
|
||||
The log level can also be set using the `LOG_LEVEL` environment variable:
|
||||
|
||||
```bash
|
||||
LOG_LEVEL=error emigrate up -r pino
|
||||
```
|
||||
47
packages/reporter-pino/package.json
Normal file
47
packages/reporter-pino/package.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "@emigrate/reporter-pino",
|
||||
"version": "0.0.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "A Pino reporter for Emigrate for logging the migration process.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.js",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --pretty",
|
||||
"build:watch": "tsc --pretty --watch",
|
||||
"lint": "xo --cwd=../.. $(pwd)"
|
||||
},
|
||||
"keywords": [
|
||||
"emigrate",
|
||||
"emigrate-reporter",
|
||||
"plugin",
|
||||
"migrations",
|
||||
"reporter"
|
||||
],
|
||||
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
|
||||
"homepage": "https://github.com/aboviq/emigrate/tree/main/packages/reporter-pino#readme",
|
||||
"repository": "https://github.com/aboviq/emigrate/tree/main/packages/reporter-pino",
|
||||
"bugs": "https://github.com/aboviq/emigrate/issues",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emigrate/plugin-tools": "workspace:*",
|
||||
"pino": "8.16.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emigrate/tsconfig": "workspace:*"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
180
packages/reporter-pino/src/index.ts
Normal file
180
packages/reporter-pino/src/index.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import process from 'node:process';
|
||||
import { pino, levels, type Logger } from 'pino';
|
||||
import {
|
||||
type Awaitable,
|
||||
type MigrationMetadata,
|
||||
type MigrationMetadataFinished,
|
||||
type ReporterInitParameters,
|
||||
type EmigrateReporter,
|
||||
} from '@emigrate/plugin-tools/types';
|
||||
|
||||
type PinoReporterOptions = {
|
||||
level?: string;
|
||||
/**
|
||||
* Customize the key used for logging errors
|
||||
*
|
||||
* @default 'error'
|
||||
* @see https://getpino.io/#/docs/api?id=errorkey-string
|
||||
*/
|
||||
errorKey?: string;
|
||||
};
|
||||
|
||||
class PinoReporter implements Required<EmigrateReporter> {
|
||||
#logger!: Logger;
|
||||
#migrations?: MigrationMetadata[];
|
||||
#command!: ReporterInitParameters['command'];
|
||||
|
||||
constructor(private readonly options: PinoReporterOptions) {
|
||||
if (!options.level || !levels.values[options.level]) {
|
||||
options.level = 'info';
|
||||
}
|
||||
}
|
||||
|
||||
get logLevel(): string {
|
||||
if (this.options.level && levels.values[this.options.level]) {
|
||||
return this.options.level;
|
||||
}
|
||||
|
||||
return 'info';
|
||||
}
|
||||
|
||||
get errorKey(): string {
|
||||
return this.options.errorKey ?? 'error';
|
||||
}
|
||||
|
||||
onInit({ command, ...parameters }: ReporterInitParameters): Awaitable<void> {
|
||||
this.#command = command;
|
||||
this.#logger = pino({
|
||||
name: 'emigrate',
|
||||
level: this.logLevel,
|
||||
errorKey: this.errorKey,
|
||||
base: {
|
||||
scope: command,
|
||||
},
|
||||
});
|
||||
|
||||
this.#logger.info({ parameters }, `Emigrate "${command}" initialized${parameters.dry ? ' (dry-run)' : ''}`);
|
||||
}
|
||||
|
||||
onCollectedMigrations(migrations: MigrationMetadata[]): Awaitable<void> {
|
||||
this.#migrations = migrations;
|
||||
}
|
||||
|
||||
onLockedMigrations(lockedMigrations: MigrationMetadata[]): Awaitable<void> {
|
||||
const migrations = this.#migrations ?? [];
|
||||
|
||||
if (migrations.length === 0) {
|
||||
this.#logger.info('No pending migrations found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (migrations.length === lockedMigrations.length) {
|
||||
this.#logger.info(
|
||||
{ migrationCount: lockedMigrations.length },
|
||||
`${lockedMigrations.length} pending migrations to run`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const nonLockedMigrations = migrations.filter(
|
||||
(migration) => !lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name),
|
||||
);
|
||||
const failedMigrations = nonLockedMigrations.filter(
|
||||
(migration) => 'status' in migration && migration.status === 'failed',
|
||||
);
|
||||
const unlockableCount = nonLockedMigrations.length - failedMigrations.length;
|
||||
const parts = [
|
||||
`${lockedMigrations.length} of ${migrations.length} pending migrations to run`,
|
||||
unlockableCount > 0 ? `(${unlockableCount} locked)` : '',
|
||||
failedMigrations.length > 0 ? `(${failedMigrations.length} failed)` : '',
|
||||
].filter(Boolean);
|
||||
|
||||
this.#logger.info({ migrationCount: lockedMigrations.length }, parts.join(' '));
|
||||
}
|
||||
|
||||
onNewMigration(migration: MigrationMetadata, content: string): Awaitable<void> {
|
||||
this.#logger.info({ migration, content }, `Created new migration file: ${migration.name}`);
|
||||
}
|
||||
|
||||
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void> {
|
||||
this.#logger.debug({ migration }, `Removing migration: ${migration.name}`);
|
||||
}
|
||||
|
||||
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||
this.#logger.info({ migration }, `Successfully removed migration: ${migration.name}`);
|
||||
}
|
||||
|
||||
onMigrationRemoveError(migration: MigrationMetadataFinished, error: Error): Awaitable<void> {
|
||||
this.#logger.error({ migration, [this.errorKey]: error }, `Failed to remove migration: ${migration.name}`);
|
||||
}
|
||||
|
||||
onMigrationStart(migration: MigrationMetadata): Awaitable<void> {
|
||||
this.#logger.info({ migration }, `${migration.name} (running)`);
|
||||
}
|
||||
|
||||
onMigrationSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||
this.#logger.info({ migration }, `${migration.name} (${migration.status})`);
|
||||
}
|
||||
|
||||
onMigrationError(migration: MigrationMetadataFinished, error: Error): Awaitable<void> {
|
||||
this.#logger.error({ migration, [this.errorKey]: error }, `${migration.name} (${migration.status})`);
|
||||
}
|
||||
|
||||
onMigrationSkip(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||
this.#logger.info({ migration }, `${migration.name} (${migration.status})`);
|
||||
}
|
||||
|
||||
onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): Awaitable<void> {
|
||||
const total = migrations.length;
|
||||
let done = 0;
|
||||
let failed = 0;
|
||||
let skipped = 0;
|
||||
let pending = 0;
|
||||
|
||||
for (const migration of migrations) {
|
||||
const status = 'status' in migration ? migration.status : undefined;
|
||||
switch (status) {
|
||||
case 'done': {
|
||||
done++;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'failed': {
|
||||
failed++;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'skipped': {
|
||||
skipped++;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'pending': {
|
||||
pending++;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.#logger.error(
|
||||
{ failed, done, skipped, pending, total, [this.errorKey]: error },
|
||||
`Emigrate "${this.#command}" failed`,
|
||||
);
|
||||
} else {
|
||||
this.#logger.info({ failed, done, skipped, pending, total }, `Emigrate "${this.#command}" finished successfully`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createPinoReporter = (options: PinoReporterOptions = {}): EmigrateReporter => {
|
||||
return new PinoReporter(options);
|
||||
};
|
||||
|
||||
export default createPinoReporter({
|
||||
level: process.env['LOG_LEVEL'],
|
||||
});
|
||||
8
packages/reporter-pino/tsconfig.json
Normal file
8
packages/reporter-pino/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@emigrate/tsconfig/build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue