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

Improve logging in shell scripts #1293

Open
wants to merge 9 commits into
base: v5-0-0
Choose a base branch
from
9 changes: 6 additions & 3 deletions docs/blog/version-5.0-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,16 @@ Version 5.0 of [Foal](https://foalts.org/) is out!

## Shell scripts

- The `main` function of shell scripts now receives an instance of `ServiceManager` as second argument:
- The `main` function of shell scripts now receives an instance of `ServiceManager` as second argument and the logger as third argument:
```typescript
export async function main(args: any, services: ServiceManager) {
export async function main(args: any, services: ServiceManager, logger: Logger) {
// ...
}
```

- Log context are supported.
- When running a script, the script name as well as a script ID are added to the log context.
- At the end of script execution, as with an HTTP request, a log is printed to indicate whether the execution was successful or unsuccessful.
- Any error thrown in the `main` function is now logged with the framework logger.

## Removal of deprecated components

Expand Down
10 changes: 5 additions & 5 deletions docs/docs/authentication/user-class.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Go to `src/scripts/create-user.ts` and replace its content with the following li

```typescript
// 3p
import { hashPassword } from '@foal/core';
import { hashPassword, Logger, ServiceManager } from '@foal/core';

// App
import { User } from '../app/entities';
Expand All @@ -111,17 +111,17 @@ export const schema = {
type: 'object',
};

export async function main(args) {
export async function main(args: any, services: ServiceManager, logger: Logger) {
await dataSource.initialize();

try {
const user = new User();
user.email = args.email;
user.password = await hashPassword(args.password);

console.log(await user.save());
} catch (error: any) {
console.error(error.message);
await user.save();

logger.info(`User created: ${user.id}`)
} finally {
await dataSource.destroy();
}
Expand Down
43 changes: 18 additions & 25 deletions docs/docs/authorization/groups-and-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ npx foal generate script create-perm
Replace the content of the new created file `src/scripts/create-perm.ts` with the following:
```typescript
// 3p
import { Logger, ServiceManager } from '@foal/core';
import { Permission } from '@foal/typeorm';

// App
Expand All @@ -62,19 +63,17 @@ export const schema = {
type: 'object',
};

export async function main(args: { codeName: string, name: string }) {
export async function main(args: { codeName: string, name: string }, services: ServiceManager, logger: Logger) {
const permission = new Permission();
permission.codeName = args.codeName;
permission.name = args.name;

await dataSource.initialize();

try {
console.log(
await permission.save()
);
} catch (error: any) {
console.log(error.message);
await permission.save();

logger.info(`Permission created: ${permission.codeName}`);
} finally {
await dataSource.destroy();
}
Expand Down Expand Up @@ -131,6 +130,7 @@ npx foal generate script create-group
Replace the content of the new created file `src/scripts/create-group.ts` with the following:
```typescript
// 3p
import { Logger, ServiceManager } from '@foal/core';
import { Group, Permission } from '@foal/typeorm';

// App
Expand All @@ -147,7 +147,7 @@ export const schema = {
type: 'object',
};

export async function main(args: { codeName: string, name: string, permissions: string[] }) {
export async function main(args: { codeName: string, name: string, permissions: string[] }, services: ServiceManager, logger: Logger) {
const group = new Group();
group.permissions = [];
group.codeName = args.codeName;
Expand All @@ -159,17 +159,14 @@ export async function main(args: { codeName: string, name: string, permissions:
for (const codeName of args.permissions) {
const permission = await Permission.findOneBy({ codeName });
if (!permission) {
console.log(`No permission with the code name "${codeName}" was found.`);
return;
throw new Error(`No permission with the code name "${codeName}" was found.`);
}
group.permissions.push(permission);
}

console.log(
await group.save()
);
} catch (error: any) {
console.log(error.message);
await group.save();

logger.info(`Group created: ${group.codeName}`);
} finally {
await dataSource.destroy();
}
Expand Down Expand Up @@ -227,7 +224,7 @@ Replace the content of the new created file `src/scripts/create-user.ts` with th

```typescript
// 3p
import { hashPassword } from '@foal/core';
import { hashPassword, Logger, ServiceManager } from '@foal/core';
import { Group, Permission } from '@foal/typeorm';

// App
Expand All @@ -246,7 +243,7 @@ export const schema = {
type: 'object',
};

export async function main(args) {
export async function main(args: any, services: ServiceManager, logger: Logger) {
const user = new User();
user.userPermissions = [];
user.groups = [];
Expand All @@ -258,27 +255,23 @@ export async function main(args) {
for (const codeName of args.userPermissions as string[]) {
const permission = await Permission.findOneBy({ codeName });
if (!permission) {
console.log(`No permission with the code name "${codeName}" was found.`);
return;
throw new Error(`No permission with the code name "${codeName}" was found.`);
}
user.userPermissions.push(permission);
}

for (const codeName of args.groups as string[]) {
const group = await Group.findOneBy({ codeName });
if (!group) {
console.log(`No group with the code name "${codeName}" was found.`);
return;
throw new Error(`No group with the code name "${codeName}" was found.`);
}
user.groups.push(group);
}

try {
console.log(
await user.save()
);
} catch (error: any) {
console.log(error.message);
await user.save();

logger.info(`User created: ${user.id}`);
} finally {
await dataSource.destroy();
}
Expand Down
95 changes: 66 additions & 29 deletions docs/docs/cli/shell-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,99 @@
title: Shell Scripts
---

Shell scripts are an easy way of executing a piece of code from the command line. They can be used in a variety of scenarios and can be a simple code snippet or be more complex and call application services.

Sometimes we have to execute some tasks from the command line. These tasks can serve different purposes such as populating a database (user creation, etc) for instance. They often need to access some of the app classes and functions. This is when shell scripts come into play.
## Structure

## Create Scripts

A shell script is just a TypeScript file located in the `src/scripts`. It must export a `main` function that is then called when running the script.

Let's create a new one with the command line: `npx foal g script display-users`. A new file with a default template should appear in you `src/scripts` directory.

## Write Scripts

Remove the content of `src/scripts/display-users.ts` and replace it with the code below.
A shell script file is divided into two parts: a `main` function, which contains the code to be executed, and a `schema`, which parses and validates the arguments given on the command line and passes them on to the `main` function. The file must be located in the `src/scripts` directory.

*Example: src/scripts/create-user.ts*
```typescript
// 3p
import { ServiceManager } from '@foal/core';
import { Logger, ServiceManager } from '@foal/core';

// App
import { dataSource } from '../db';
import { User } from '../app/entities';
import { Logger } from '../app/services';

export async function main(args: any, services: ServiceManager) {
export const schema = {
type: 'object',
properties: {
email: { type: 'string' },
},
required: ['email'],
additionalProperties: false
}

export async function main(args: { email: string }) {
await dataSource.initialize();

try {
const users = await User.find();
const logger = services.get(Logger);
logger.log(users);
const user = new User();
user.email = args.email;

await user.save();
} finally {
dataSource.destroy();
}
}

```

As you can see, we can easily establish a connection to the database in the script as well as import some of the app components (the `User` in this case).
## Generating, Building and Running Shell Scripts

Encapsulating your code in a `main` function without calling it directly in the file has several benefits:
- You can import and test your `main` function in a separate file.
- Using a function lets you easily use async/await keywords when dealing with asynchronous code.
To generate a new script, you can use the CLI `generate` command:

## Build and Run Scripts

To run a script you first need to build it.
```bash
npx foal generate script create-user
# or
npx foal g script create-user
```

```sh
If you need to build the script once, run this command:
```bash
npm run build
```

Then you can execute it with this command:
If you need to build and watch it in dev mode, use this command:
```bash
npm run dev
```

```shell
npx foal run my-script
Then you can run the script as follows:
```bash
npx foal run create-user email=foo@foalts.org
```

> You can also provide additionnal arguments to your script (for example: `npx foal run my-script foo=1 bar='[ 3, 4 ]'`). The default template in the generated scripts shows you how to handle such behavior.
## Accessing Services

If you wish to access a service, you can use the `ServiceManager` instance passed as second argument to the `main` function.

> If you want your script to recompile each time you save the file, you can run `npm run dev` in a separate terminal.
Example

```typescript
import { ServiceManager } from '@foal/core';

import { MyService } from '../app/services';

export function main(args: any, services: ServiceManager) {
const myService = services.get(MyService);

// Do something with myService.
}
```

## Logging

When a script is executed, the script name as well as a script ID are added to the log context. Like the request ID in an HTTP request, the script ID is added as a parameter to every log printed during script execution, including any errors. In this way, it is possible to aggregate all logs from a single script execution in a logging program.

If you wish to access the logger in the script, it is passed as the third argument to the main function.

```typescript
import { Logger, ServiceManager } from '@foal/core';


export function main(args: any, services: ServiceManager, logger: Logger) {
logger.info('Hello world!');
}
```
7 changes: 4 additions & 3 deletions docs/docs/common/async-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,20 @@ export function main(args: any) {
*scripts/schedule-jobs.ts*
```typescript
// 3p
import { Logger, ServiceManager } from '@foal/core';
import { scheduleJob } from 'node-schedule';
import { main as fetchMetrics } from './fetch-metrics';

export async function main(args: any) {
console.log('Scheduling the job...');
export async function main(args: any, services: ServiceManager, logger: Logger) {
logger.info('Scheduling the job...');

// Run the fetch-metrics script every day at 10:00 AM.
scheduleJob(
{ hour: 10, minute: 0 },
() => fetchMetrics(args)
);

console.log('Job scheduled!');
logger.info('Job scheduled!');
}

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Open the file and replace its content with the following:

```typescript
// 3p
import { hashPassword } from '@foal/core';
import { hashPassword, Logger, ServiceManager } from '@foal/core';

// App
import { User } from '../app/entities';
Expand All @@ -31,7 +31,7 @@ export const schema = {
type: 'object',
};

export async function main(args: { email: string, password: string, name?: string }) {
export async function main(args: { email: string, password: string, name?: string }, services: ServiceManager, logger: Logger) {
const user = new User();
user.email = args.email;
user.password = await hashPassword(args.password);
Expand All @@ -41,9 +41,9 @@ export async function main(args: { email: string, password: string, name?: strin
await dataSource.initialize();

try {
console.log(await user.save());
} catch (error: any) {
console.log(error.message);
await user.save();

logger.info(`User created: ${user.id}`);
} finally {
await dataSource.destroy();
}
Expand Down Expand Up @@ -87,6 +87,7 @@ npx foal generate script create-story
Open the `create-story.ts` file and replace its content.

```typescript
import { Logger, ServiceManager } from '@foal/core';
import { Story, User } from '../app/entities';
import { dataSource } from '../db';

Expand All @@ -101,7 +102,7 @@ export const schema = {
type: 'object',
};

export async function main(args: { author: string, title: string, link: string }) {
export async function main(args: { author: string, title: string, link: string }, services: ServiceManager, logger: Logger) {
await dataSource.initialize();

const user = await User.findOneByOrFail({ email: args.author });
Expand All @@ -112,9 +113,9 @@ export async function main(args: { author: string, title: string, link: string }
story.link = args.link;

try {
console.log(await story.save());
} catch (error: any) {
console.error(error);
await store.save();

logger.info(`Story created: ${story.id}`);
} finally {
await dataSource.destroy();
}
Expand Down
Loading
Loading