Skip to content

Commit 17615bd

Browse files
authored
Structured logs and error reporting (#9)
1 parent a96ae82 commit 17615bd

File tree

10 files changed

+814
-109
lines changed

10 files changed

+814
-109
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"devtool",
3434
"downlevel",
3535
"envalid",
36+
"envars",
3637
"eslintcache",
3738
"esmodules",
3839
"esnext",

README.md

+9-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ of your choice, such as [GraphQL.js](https://www.npmjs.com/package/graphql).
1919
- Stateless sessions implemented with JWT tokens and a session cookie (compatible with SSR)
2020
- Database schema migration, seeds, and REPL shell tooling
2121
- Transactional emails using Handlebars templates and instant email previews
22+
- Structured logs and error reporting to Google StackDriver
2223
- Pre-configured unit testing tooling powered by [Jest](https://jestjs.io/) and [Supertest](https://github.com/visionmedia/supertest)
2324
- Application bundling with Rollup as an optimization technique for serverless deployments
2425
- Rebuilds and restarts the app on changes when running locally
@@ -31,17 +32,22 @@ Be sure to join our [Discord channel](https://discord.com/invite/GrqQaSnvmr) for
3132

3233
## Tech Stack
3334

34-
- [Node.js](https://nodejs.org/) v14, [Yarn](https://yarnpkg.com/) package manager, [TypeScript](https://www.typescriptlang.org/), [Babel](https://babeljs.io/), [Rollup](https://rollupjs.org/)
35+
- [Node.js](https://nodejs.org/) v14, [Yarn](https://yarnpkg.com/) package manager,
36+
[TypeScript](https://www.typescriptlang.org/), [Babel](https://babeljs.io/),
37+
[Rollup](https://rollupjs.org/), [ESLint](https://eslint.org/),
38+
[Prettier](https://prettier.io/), [Jest](https://jestjs.io/)
3539
- [PostgreSQL](https://www.postgresql.org/), [Knex](https://knesjs.org/),
36-
[Express](https://expressjs.com/), [Handlebars](https://handlebarsjs.com/),
37-
[Simple OAuth2](https://github.com/lelylan/simple-oauth2).
40+
[Express](https://expressjs.com/), [Nodemailer](https://nodemailer.com/),
41+
[Email Templates](https://email-templates.js.org/), [Handlebars](https://handlebarsjs.com/),
42+
[Simple OAuth2](https://github.com/lelylan/simple-oauth2)
3843

3944
## Directory Structure
4045

4146
`├──`[`.build`](.build) — Compiled and bundled output (per Cloud Function)<br>
4247
`├──`[`.vscode`](.vscode) — VSCode settings including code snippets, recommended extensions etc.<br>
4348
`├──`[`api`](./api) — Cloud Function for handling API requests<br>
4449
`├──`[`auth`](./auth) — Authentication and session middleware<br>
50+
`├──`[`core`](./core) — Common application modules (email, logging, etc.)<br>
4551
`├──`[`db`](./db) — Database client for PostgreSQL using [Knex](https://knexjs.org/)<br>
4652
`├──`[`emails`](./emails) — Email templates for transactional emails using [Handlebars](https://handlebarsjs.com/)<br>
4753
`├──`[`env`](./env) — Environment variables for `local`, `dev`, `test`, and `prod`<br>

api/errors.ts

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { ErrorRequestHandler } from "express";
55
import { isHttpError } from "http-errors";
6+
import { reportError } from "../core";
67

78
/**
89
* Renders an error page.
@@ -13,6 +14,8 @@ export const handleError: ErrorRequestHandler = function (
1314
res,
1415
next // eslint-disable-line @typescript-eslint/no-unused-vars
1516
) {
17+
reportError(err, req);
18+
1619
const statusCode = isHttpError(err) ? err.statusCode : 500;
1720
res.status(statusCode);
1821

api/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { withViews } from "./views";
1010

1111
export const api = withViews(express());
1212

13+
api.enable("trust proxy");
14+
api.disable("x-powered-by");
15+
1316
// OAuth 2.0 authentication endpoints and user sessions
1417
api.use(auth);
1518

@@ -34,7 +37,7 @@ api.use(handleError);
3437
/**
3538
* Launch API for testing when in development mode.
3639
*
37-
* NOTE: This block will be removed in production build by Rollup.
40+
* NOTE: This block will be removed from production build by Rollup.
3841
*/
3942
if (process.env.NODE_ENV === "development") {
4043
const port = process.env.PORT ?? 8080;

api/email.ts core/email.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import env from "../env";
99
*
1010
* @see https://github.com/forwardemail/email-templates
1111
*/
12-
export default new Email({
12+
export const email = new Email({
1313
message: {
1414
from: `"${env.APP_NAME}" <${env.EMAIL_FROM}>`,
1515
},

core/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */
2+
/* SPDX-License-Identifier: MIT */
3+
4+
export * from "./email";
5+
export * from "./logging";

core/logging.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */
2+
/* SPDX-License-Identifier: MIT */
3+
4+
import { Logging } from "@google-cloud/logging";
5+
import { Request } from "express";
6+
import PrettyError from "pretty-error";
7+
import env from "../env";
8+
9+
// https://googleapis.dev/nodejs/logging/latest/
10+
const logging = new Logging();
11+
const log = logging.log("cloudfunctions.googleapis.com/cloud-functions");
12+
13+
// https://github.com/AriaMinaei/pretty-error#readme
14+
const pe = new PrettyError();
15+
16+
/**
17+
* Logs application errors to Google StackDriver when in production,
18+
* otherwise just print them to the console using PrettyError formatter.
19+
*/
20+
export function reportError(
21+
err: Error,
22+
req: Request,
23+
context?: Record<string, unknown>
24+
): void {
25+
if (!env.isProduction) {
26+
console.error(pe.render(err));
27+
return;
28+
}
29+
30+
// E.g. us-central-example.cloudfunctions.net
31+
const host = req.get("host") as string;
32+
33+
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
34+
log.error(
35+
log.entry(
36+
{
37+
resource: {
38+
type: "cloud_function",
39+
labels: {
40+
region: host.substring(0, host.indexOf("-", host.indexOf("-") + 1)),
41+
function_name: process.env.FUNCTION_TARGET as string,
42+
},
43+
},
44+
httpRequest: {
45+
requestMethod: req.method,
46+
requestUrl: req.originalUrl,
47+
userAgent: req.get("user-agent"),
48+
remoteIp: req.ip,
49+
referer: req.headers.referer,
50+
latency: null,
51+
},
52+
labels: {
53+
execution_id: req.get("function-execution-id") as string,
54+
},
55+
},
56+
{
57+
message: err.stack,
58+
context: {
59+
...context,
60+
user: req.user
61+
? { id: req.user.id, username: req.user.username }
62+
: null,
63+
},
64+
}
65+
)
66+
);
67+
}

package.json

+12-10
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
"postinstall": "husky install"
2626
},
2727
"dependencies": {
28+
"@google-cloud/logging": "^9.2.2",
2829
"chalk": "^4.1.1",
2930
"cookie": "^0.4.1",
3031
"email-templates": "^8.0.4",
3132
"envalid": "^7.1.0",
3233
"express": "^4.17.1",
33-
"express-handlebars": "^5.3.0",
34+
"express-handlebars": "^5.3.1",
3435
"google-auth-library": "^7.0.4",
3536
"got": "^11.8.2",
3637
"handlebars": "^4.7.7",
@@ -41,20 +42,22 @@
4142
"minimist": "^1.2.5",
4243
"nanoid": "^3.1.22",
4344
"pg": "^8.6.0",
44-
"simple-oauth2": "^4.2.0"
45+
"pretty-error": "^3.0.3",
46+
"simple-oauth2": "^4.2.0",
47+
"source-map-support": "^0.5.19"
4548
},
4649
"devDependencies": {
4750
"@babel/cli": "^7.13.16",
4851
"@babel/core": "^7.14.0",
4952
"@babel/plugin-proposal-class-properties": "^7.13.0",
50-
"@babel/preset-env": "^7.14.0",
53+
"@babel/preset-env": "^7.14.1",
5154
"@babel/preset-typescript": "^7.13.0",
5255
"@babel/register": "^7.13.16",
5356
"@jest/types": "^26.6.2",
5457
"@rollup/plugin-babel": "^5.3.0",
55-
"@rollup/plugin-commonjs": "^18.0.0",
58+
"@rollup/plugin-commonjs": "^18.1.0",
5659
"@rollup/plugin-json": "^4.1.0",
57-
"@rollup/plugin-node-resolve": "^11.2.1",
60+
"@rollup/plugin-node-resolve": "^13.0.0",
5861
"@rollup/plugin-replace": "^2.4.2",
5962
"@rollup/plugin-run": "^2.0.2",
6063
"@rollup/plugin-url": "^6.0.0",
@@ -68,11 +71,11 @@
6871
"@types/jsonwebtoken": "^8.5.1",
6972
"@types/lodash": "^4.14.168",
7073
"@types/minimist": "^1.2.1",
71-
"@types/node": "^14.14.43",
74+
"@types/node": "^14.14.44",
7275
"@types/simple-oauth2": "^4.1.0",
7376
"@types/supertest": "^2.0.11",
74-
"@typescript-eslint/eslint-plugin": "^4.22.0",
75-
"@typescript-eslint/parser": "^4.22.0",
77+
"@typescript-eslint/eslint-plugin": "^4.22.1",
78+
"@typescript-eslint/parser": "^4.22.1",
7679
"@yarnpkg/pnpify": "^3.0.0-rc.3",
7780
"babel-jest": "^26.6.3",
7881
"cross-spawn": "^7.0.3",
@@ -83,10 +86,9 @@
8386
"jest": "^26.6.3",
8487
"knex-types": "^0.2.0",
8588
"prettier": "^2.2.1",
86-
"rollup": "^2.46.0",
89+
"rollup": "^2.47.0",
8790
"rollup-plugin-copy": "^3.4.0",
8891
"rollup-plugin-delete": "^2.0.0",
89-
"source-map-support": "^0.5.19",
9092
"supertest": "^6.1.3",
9193
"typescript": "^4.2.4"
9294
},

scripts/deploy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ spawn(
4040
`--source=./.build`,
4141
`--timeout=30`,
4242
`--trigger-http`,
43-
`--set-env-vars=NODE_OPTIONS=--require ./.pnp.js`,
43+
`--set-env-vars=NODE_OPTIONS=-r ./.pnp.js -r source-map-support/register`,
4444
...Object.keys(env).map((key) => `--set-env-vars=${key}=${env[key]}`),
4545
],
4646
{ stdio: "inherit" }

0 commit comments

Comments
 (0)