Skip to content

Commit

Permalink
feat(server): Log server errors with pino.
Browse files Browse the repository at this point in the history
  • Loading branch information
SamTolmay committed Jul 19, 2023
1 parent efae9ed commit ed36f2f
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 124 deletions.
33 changes: 4 additions & 29 deletions packages/api/src/context/createApiContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,10 @@
import createAuthorize from './createAuthorize.js';
import createReadConfigFile from './createReadConfigFile.js';

function createApiContext({
authOptions,
buildDirectory,
config,
connections,
fileCache,
headers,
logger,
operators,
secrets,
session,
nextContext,
req,
}) {
const readConfigFile = createReadConfigFile({ buildDirectory, fileCache });
return {
authOptions,
authorize: createAuthorize({ session }),
config,
connections,
headers,
logger,
operators,
readConfigFile,
secrets,
user: session?.user,
nextContext,
req,
};
function createApiContext(context) {
context.readConfigFile = createReadConfigFile(context);
context.authorize = createAuthorize(context);
context.user = context?.session?.user;
}

export default createApiContext;
4 changes: 2 additions & 2 deletions packages/api/src/routes/request/evaluateOperators.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import { ServerParser } from '@lowdefy/operators';
import { RequestError } from '../../context/errors.js';

function evaluateOperators(
{ operators, secrets, user },
{ operators, secrets, session },
{ connectionConfig, payload, requestConfig }
) {
const operatorsParser = new ServerParser({
operators,
payload,
secrets,
user,
user: session?.user,
});
const { output: connectionProperties, errors: connectionErrors } = operatorsParser.parse({
input: connectionConfig.properties || {},
Expand Down
8 changes: 1 addition & 7 deletions packages/api/src/test/testContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ function testContext({
config = {},
connections = {},
headers = {},
host = 'host',
logger = {
debug: () => {},
error: () => {},
Expand All @@ -32,23 +31,18 @@ function testContext({
},
readConfigFile,
secrets = {},
setHeader,
session,
protocol = 'https',
} = {}) {
return {
authorize: createAuthorize({ session }),
config,
connections,
headers,
host,
logger,
operators,
protocol,
readConfigFile,
secrets,
setHeader,
user: session?.user,
session,
};
}

Expand Down
44 changes: 21 additions & 23 deletions packages/server/lib/server/apiWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,40 @@ import connections from '../../build/plugins/connections.js';
import createLogger from './log/createLogger.js';
import fileCache from './fileCache.js';
import getServerSession from './auth/getServerSession.js';
import logError from './log/logError.js';
import logRequest from './log/logRequest.js';
import operators from '../../build/plugins/operators/server.js';
import getAuthOptions from './auth/getAuthOptions.js';

function apiWrapper(handler) {
return async function wrappedHandler(req, res) {
let logger = console;
const context = {
// Important to give absolute path so Next can trace build files
rid: crypto.randomUUID(),
buildDirectory: path.join(process.cwd(), 'build'),
config,
connections,
fileCache,
headers: req?.headers,
logger: console,
operators,
req,
res,
};
try {
const rid = crypto.randomUUID();
logger = createLogger({ rid });
const authOptions = getAuthOptions({ logger });
const session = await getServerSession({ authOptions, req, res });
context.logger = createLogger({ rid: context.rid });
context.authOptions = getAuthOptions(context);
context.session = await getServerSession(context);
// Important to give absolute path so Next can trace build files
const context = createApiContext({
authOptions,
buildDirectory: path.join(process.cwd(), 'build'),
config,
connections,
fileCache,
headers: req.headers,
logger,
operators,
secrets: getSecretsFromEnv(),
session,
req,
});
context.secrets = getSecretsFromEnv();
createApiContext(context);
logRequest({ context });
// Await here so that if handler throws it is caught.
const response = await handler({ context, req, res });
// TODO: Log response time?
return response;
} catch (error) {
// TODO: Improve (logError function)
// TODO: Log cause
logger.error(error);
// TODO: What we do here?
logError({ error, context });

res.status(500).json({ name: error.name, message: error.message });
}
};
Expand Down
1 change: 1 addition & 0 deletions packages/server/lib/server/log/createLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import pino from 'pino';

// TODO: Pino does not serialize error.cause properties if the cause object is not an Error (or Error-like)
const logger = pino({
name: 'lowdefy_server',
level: process.env.LOWDEFY_LOG_LEVEL ?? 'info',
Expand Down
72 changes: 72 additions & 0 deletions packages/server/lib/server/log/logError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2020-2023 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

function logError({ context, error }) {
try {
const { headers = {}, user = {} } = context;

context.logger.error({
// TODO:
// app_name
// app_version
// lowdefy_version
// build_hash
// config_hash
err: error,
user: {
id: user.id,
roles: user.roles,
sub: user.sub,
session_id: user.session_id,
},
url: context.req.url,
method: context.req.method,
resolvedUrl: context.nextContext?.resolvedUrl,
hostname: context.req.hostname,
headers: {
'accept-language': headers['accept-language'],
'sec-ch-ua-mobile': headers['sec-ch-ua-mobile'],
'sec-ch-ua-platform': headers['sec-ch-ua-platform'],
'sec-ch-ua': headers['sec-ch-ua'],
'user-agent': headers['user-agent'],
host: headers.host,
referer: headers.referer,
// Non localhost headers
'x-forward-for': headers['x-forward-for'],
// Vercel headers
'x-vercel-id': headers['x-vercel-id'],
'x-real-ip': headers['x-real-ip'],
'x-vercel-ip-country': headers['x-vercel-ip-country'],
'x-vercel-ip-country-region': headers['x-vercel-ip-country-region'],
'x-vercel-ip-city': headers['x-vercel-ip-city'],
'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
// Cloudflare headers
'cf-connecting-ip': headers['cf-connecting-ip'],
'cf-ray': headers['cf-ray'],
'cf-ipcountry': headers['cf-ipcountry'],
'cf-visitor': headers['cf-visitor'],
},
});
} catch (e) {
console.error(error);
console.error('An error occurred while logging the error.');
console.error(e);
}
}

export default logError;
39 changes: 2 additions & 37 deletions packages/server/lib/server/log/logRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

// TODO: Better name needed here maybe?
function logRequest({ context, metadata = {} }) {
function logRequest({ context }) {
const { headers = {}, user = {} } = context;
context.logger.info({
// TODO:
Expand All @@ -42,7 +42,6 @@ function logRequest({ context, metadata = {} }) {
'user-agent': headers['user-agent'],
host: headers.host,
referer: headers.referer,
// Non localhost headers
'x-forward-for': headers['x-forward-for'],
// Vercel headers
'x-vercel-id': headers['x-vercel-id'],
Expand All @@ -53,49 +52,15 @@ function logRequest({ context, metadata = {} }) {
'x-vercel-ip-latitude': headers['x-vercel-ip-latitude'],
'x-vercel-ip-longitude': headers['x-vercel-ip-longitude'],
'x-vercel-ip-timezone': headers['x-vercel-ip-timezone'],
// TODO: Cloudflare headers
// Cloudflare headers
'cf-connecting-ip': headers['cf-connecting-ip'],
'cf-ray': headers['cf-ray'],
'cf-ipcountry': headers['cf-ipcountry'],
'cf-visitor': headers['cf-visitor'],
},
...metadata,
});
// TODO:
// Next local? nextContext.locale, nextContext.locales, nextContext.defaultLocale
// console.log('params', nextContext.params);
}

export default logRequest;

/*
User ID
Session ID
Event Type/Action
Timestamp
Source IP Address
User Agent
Resource ID/Identifier
Outcome/Status
Reason
Details/Additional Information
Targeted System
Page URL
Referrer URL
IP Address
Response Time
Error Messages
Device Information
Input Data
Behavioral Metrics
Error Level/Severity
Error Message
Error Code
HTTP Method
Stack Trace
Payload/Body
Application Version
Environment Information
*/
41 changes: 18 additions & 23 deletions packages/server/lib/server/serverSidePropsWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,36 @@ import config from '../../build/config.json';
import createLogger from './log/createLogger.js';
import fileCache from './fileCache.js';
import getServerSession from './auth/getServerSession.js';
import logError from './log/logError.js';
import logRequest from './log/logRequest.js';
import getAuthOptions from './auth/getAuthOptions.js';

// TODO: Merge serverSidePropsWrapper and apiWrapper?
function serverSidePropsWrapper(handler) {
return async function wrappedHandler(nextContext) {
let logger = console;
try {
const { req, res } = nextContext;
const rid = crypto.randomUUID();
logger = createLogger({ rid });
const authOptions = getAuthOptions({ logger });
const session = await getServerSession({ authOptions, req, res });
const context = {
// Important to give absolute path so Next can trace build files
const context = createApiContext({
authOptions,
buildDirectory: path.join(process.cwd(), 'build'),
config,
fileCache,
headers: req.headers,
logger,
session,
nextContext,
req,
});
rid: crypto.randomUUID(),
buildDirectory: path.join(process.cwd(), 'build'),
config,
fileCache,
headers: nextContext?.req?.headers,
logger: console,
nextContext,
req: nextContext?.req,
res: nextContext?.res,
};
try {
context.logger = createLogger({ rid: context.rid });
context.authOptions = getAuthOptions(context);
context.session = await getServerSession(context);
createApiContext(context);
logRequest({ context });
// Await here so that if handler throws it is caught.
const response = await handler({ context, nextContext });
// TODO: Log response time?
return response;
} catch (error) {
// TODO: Improve
// TODO: Log cause
logger.error(error);
// TODO: What we do here?
logError({ error, context });
throw error;
}
};
Expand Down
5 changes: 3 additions & 2 deletions packages/server/pages/404.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import Page from '../lib/client/Page.js';

export async function getStaticProps() {
// Important to give absolute path so Next can trace build files
const context = createApiContext({
const context = {
buildDirectory: path.join(process.cwd(), 'build'),
config,
fileCache,
logger: console, // TODO: pino or console or 🤷‍♂️?
});
};
createApiContext(context);

const [rootConfig, pageConfig] = await Promise.all([
getRootConfig(context),
Expand Down
1 change: 0 additions & 1 deletion packages/server/pages/[pageId].js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import Page from '../lib/client/Page.js';
async function getServerSidePropsHandler({ context, nextContext }) {
const { pageId } = nextContext.params;
const { logger } = context;
// throw new Error('Test', { cause: { a: 4, pageId } });
const [rootConfig, pageConfig] = await Promise.all([
getRootConfig(context),
getPageConfig(context, { pageId }),
Expand Down

0 comments on commit ed36f2f

Please sign in to comment.