Add application logger (#332)

* add application logger

* use logger
This commit is contained in:
Femi Odugbesan 2021-07-09 10:49:42 -05:00 committed by GitHub
parent a9b45c1057
commit 5c4a9c32d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 326 additions and 278 deletions

20
lib/logger.ts Normal file
View File

@ -0,0 +1,20 @@
import { Logger } from "tslog";
const isProduction = process.env.NODE_ENV === "production";
const logger = new Logger({
dateTimePattern: "hour:minute:second.millisecond timeZoneName",
displayFunctionName: false,
displayFilePath: "hidden",
dateTimeTimezone: isProduction ? "utc" : Intl.DateTimeFormat().resolvedOptions().timeZone,
prettyInspectHighlightStyles: {
name: "yellow",
number: "blue",
bigint: "blue",
boolean: "blue",
},
maskValuesOfKeys: ["password", "passwordConfirmation", "credentials", "credential"],
exposeErrorCodeFrame: !isProduction,
});
export default logger;

View File

@ -36,6 +36,7 @@
"react-select": "^4.3.0",
"react-timezone-select": "^1.0.2",
"short-uuid": "^4.2.0",
"tslog": "^3.2.0",
"uuid": "^8.3.2"
},
"devDependencies": {

View File

@ -10,8 +10,10 @@ import { getEventName } from "../../../lib/event";
import { LocationType } from "../../../lib/location";
import merge from "lodash.merge";
import dayjs from "dayjs";
import logger from "../../../lib/logger";
const translator = short();
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
function isAvailable(busyTimes, time, length) {
// Check for conflicts
@ -69,304 +71,322 @@ const getLocationRequestFromIntegration = ({ location }: GetLocationRequestFromI
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const { user } = req.query;
const isTimeInPast = (time) => {
return dayjs(time).isBefore(new Date(), "day");
};
if (isTimeInPast(req.body.start)) {
return res
.status(400)
.json({ errorCode: "BookingDateInPast", message: "Attempting to create a meeting in the past." });
}
let currentUser = await prisma.user.findFirst({
where: {
username: user,
},
select: {
id: true,
credentials: true,
timeZone: true,
email: true,
name: true,
},
});
const selectedCalendars = await prisma.selectedCalendar.findMany({
where: {
userId: currentUser.id,
},
});
// Split credentials up into calendar credentials and video credentials
let calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
let videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
const hasCalendarIntegrations =
currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0;
const hasVideoIntegrations =
currentUser.credentials.filter((cred) => cred.type.endsWith("_video")).length > 0;
const calendarAvailability = await getBusyCalendarTimes(
currentUser.credentials,
dayjs(req.body.start).startOf("day").utc().format(),
dayjs(req.body.end).endOf("day").utc().format(),
selectedCalendars
);
const videoAvailability = await getBusyVideoTimes(
currentUser.credentials,
dayjs(req.body.start).startOf("day").utc().format(),
dayjs(req.body.end).endOf("day").utc().format()
);
let commonAvailability = [];
if (hasCalendarIntegrations && hasVideoIntegrations) {
commonAvailability = calendarAvailability.filter((availability) =>
videoAvailability.includes(availability)
);
} else if (hasVideoIntegrations) {
commonAvailability = videoAvailability;
} else if (hasCalendarIntegrations) {
commonAvailability = calendarAvailability;
}
// Now, get the newly stored credentials (new refresh token for example).
currentUser = await prisma.user.findFirst({
where: {
username: user,
},
select: {
id: true,
credentials: true,
timeZone: true,
email: true,
name: true,
},
});
calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
const rescheduleUid = req.body.rescheduleUid;
const selectedEventType = await prisma.eventType.findFirst({
where: {
userId: currentUser.id,
id: req.body.eventTypeId,
},
select: {
eventName: true,
title: true,
length: true,
},
});
const rawLocation = req.body.location;
let evt: CalendarEvent = {
type: selectedEventType.title,
title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName),
description: req.body.notes,
startTime: req.body.start,
endTime: req.body.end,
organizer: { email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone },
attendees: [{ email: req.body.email, name: req.body.name, timeZone: req.body.timeZone }],
};
// If phone or inPerson use raw location
// set evt.location to req.body.location
if (!rawLocation?.includes("integration")) {
evt.location = rawLocation;
}
// If location is set to an integration location
// Build proper transforms for evt object
// Extend evt object with those transformations
if (rawLocation?.includes("integration")) {
const maybeLocationRequestObject = getLocationRequestFromIntegration({
location: rawLocation,
});
evt = merge(evt, maybeLocationRequestObject);
}
const eventType = await prisma.eventType.findFirst({
where: {
userId: currentUser.id,
title: evt.type,
},
select: {
id: true,
},
});
let isAvailableToBeBooked = true;
log.debug(`Booking ${user} started`);
try {
isAvailableToBeBooked = isAvailable(commonAvailability, req.body.start, selectedEventType.length);
} catch {
console.debug({
message: "Unable set isAvailableToBeBooked. Using true. ",
});
}
const isTimeInPast = (time) => {
return dayjs(time).isBefore(new Date(), "day");
};
if (!isAvailableToBeBooked) {
return res.status(400).json({ message: `${currentUser.name} is unavailable at this time.` });
}
if (isTimeInPast(req.body.start)) {
const error = {
errorCode: "BookingDateInPast",
message: "Attempting to create a meeting in the past.",
};
let results = [];
let referencesToCreate = [];
log.error(`Booking ${user} failed`, error);
return res.status(400).json(error);
}
if (rescheduleUid) {
// Reschedule event
const booking = await prisma.booking.findFirst({
let currentUser = await prisma.user.findFirst({
where: {
uid: rescheduleUid,
username: user,
},
select: {
id: true,
references: {
select: {
id: true,
type: true,
uid: true,
credentials: true,
timeZone: true,
email: true,
name: true,
},
});
const selectedCalendars = await prisma.selectedCalendar.findMany({
where: {
userId: currentUser.id,
},
});
// Split credentials up into calendar credentials and video credentials
let calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
let videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
const hasCalendarIntegrations =
currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0;
const hasVideoIntegrations =
currentUser.credentials.filter((cred) => cred.type.endsWith("_video")).length > 0;
const calendarAvailability = await getBusyCalendarTimes(
currentUser.credentials,
dayjs(req.body.start).startOf("day").utc().format(),
dayjs(req.body.end).endOf("day").utc().format(),
selectedCalendars
);
const videoAvailability = await getBusyVideoTimes(
currentUser.credentials,
dayjs(req.body.start).startOf("day").utc().format(),
dayjs(req.body.end).endOf("day").utc().format()
);
let commonAvailability = [];
if (hasCalendarIntegrations && hasVideoIntegrations) {
commonAvailability = calendarAvailability.filter((availability) =>
videoAvailability.includes(availability)
);
} else if (hasVideoIntegrations) {
commonAvailability = videoAvailability;
} else if (hasCalendarIntegrations) {
commonAvailability = calendarAvailability;
}
// Now, get the newly stored credentials (new refresh token for example).
currentUser = await prisma.user.findFirst({
where: {
username: user,
},
select: {
id: true,
credentials: true,
timeZone: true,
email: true,
name: true,
},
});
calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar"));
videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video"));
const rescheduleUid = req.body.rescheduleUid;
const selectedEventType = await prisma.eventType.findFirst({
where: {
userId: currentUser.id,
id: req.body.eventTypeId,
},
select: {
eventName: true,
title: true,
length: true,
},
});
const rawLocation = req.body.location;
let evt: CalendarEvent = {
type: selectedEventType.title,
title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName),
description: req.body.notes,
startTime: req.body.start,
endTime: req.body.end,
organizer: { email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone },
attendees: [{ email: req.body.email, name: req.body.name, timeZone: req.body.timeZone }],
};
// If phone or inPerson use raw location
// set evt.location to req.body.location
if (!rawLocation?.includes("integration")) {
evt.location = rawLocation;
}
// If location is set to an integration location
// Build proper transforms for evt object
// Extend evt object with those transformations
if (rawLocation?.includes("integration")) {
const maybeLocationRequestObject = getLocationRequestFromIntegration({
location: rawLocation,
});
evt = merge(evt, maybeLocationRequestObject);
}
const eventType = await prisma.eventType.findFirst({
where: {
userId: currentUser.id,
title: evt.type,
},
select: {
id: true,
},
});
const isAvailableToBeBooked = true;
if (!isAvailableToBeBooked) {
const error = {
errorCode: "BookingUserUnAvailable",
message: `${currentUser.name} is unavailable at this time.`,
};
log.debug(`Booking ${user} failed`, error);
return res.status(400).json(error);
}
let results = [];
let referencesToCreate = [];
if (rescheduleUid) {
// Reschedule event
const booking = await prisma.booking.findFirst({
where: {
uid: rescheduleUid,
},
select: {
id: true,
references: {
select: {
id: true,
type: true,
uid: true,
},
},
},
},
});
});
// Use all integrations
results = results.concat(
await async.mapLimit(calendarCredentials, 5, async (credential) => {
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
return updateEvent(credential, bookingRefUid, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
console.error("updateEvent failed", e);
return { type: credential.type, success: false };
});
})
);
// Use all integrations
results = results.concat(
await async.mapLimit(calendarCredentials, 5, async (credential) => {
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
return updateEvent(credential, bookingRefUid, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
log.error("updateEvent failed", e, evt);
return { type: credential.type, success: false };
});
})
);
results = results.concat(
await async.mapLimit(videoCredentials, 5, async (credential) => {
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
return updateMeeting(credential, bookingRefUid, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
console.error("updateMeeting failed", e);
return { type: credential.type, success: false };
});
})
);
results = results.concat(
await async.mapLimit(videoCredentials, 5, async (credential) => {
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
return updateMeeting(credential, bookingRefUid, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
log.error("updateMeeting failed", e, evt);
return { type: credential.type, success: false };
});
})
);
if (results.length > 0 && results.every((res) => !res.success)) {
res.status(500).json({ message: "Rescheduling failed" });
return;
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingReschedulingMeetingFailed",
message: "Booking Rescheduling failed",
};
log.error(`Booking ${user} failed`, error, results);
return res.status(500).json(error);
}
// Clone elements
referencesToCreate = [...booking.references];
// Now we can delete the old booking and its references.
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
where: {
bookingId: booking.id,
},
});
const attendeeDeletes = prisma.attendee.deleteMany({
where: {
bookingId: booking.id,
},
});
const bookingDeletes = prisma.booking.delete({
where: {
uid: rescheduleUid,
},
});
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
} else {
// Schedule event
results = results.concat(
await async.mapLimit(calendarCredentials, 5, async (credential) => {
return createEvent(credential, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
log.error("createEvent failed", e, evt);
return { type: credential.type, success: false };
});
})
);
results = results.concat(
await async.mapLimit(videoCredentials, 5, async (credential) => {
return createMeeting(credential, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
log.error("createMeeting failed", e, evt);
return { type: credential.type, success: false };
});
})
);
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingCreatingMeetingFailed",
message: "Booking failed",
};
log.error(`Booking ${user} failed`, error, results);
return res.status(500).json(error);
}
referencesToCreate = results.map((result) => {
return {
type: result.type,
uid: result.response.createdEvent.id.toString(),
};
});
}
// Clone elements
referencesToCreate = [...booking.references];
// Now we can delete the old booking and its references.
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
where: {
bookingId: booking.id,
},
});
const attendeeDeletes = prisma.attendee.deleteMany({
where: {
bookingId: booking.id,
},
});
const bookingDeletes = prisma.booking.delete({
where: {
uid: rescheduleUid,
},
});
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
} else {
// Schedule event
results = results.concat(
await async.mapLimit(calendarCredentials, 5, async (credential) => {
return createEvent(credential, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
console.error("createEvent failed", e);
return { type: credential.type, success: false };
});
})
);
results = results.concat(
await async.mapLimit(videoCredentials, 5, async (credential) => {
return createMeeting(credential, evt)
.then((response) => ({ type: credential.type, success: true, response }))
.catch((e) => {
console.error("createMeeting failed", e);
return { type: credential.type, success: false };
});
})
);
if (results.length > 0 && results.every((res) => !res.success)) {
res.status(500).json({ message: "Booking failed" });
return;
const hashUID =
results.length > 0
? results[0].response.uid
: translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
// TODO Should just be set to the true case as soon as we have a "bare email" integration class.
// UID generation should happen in the integration itself, not here.
if (results.length === 0) {
// Legacy as well, as soon as we have a separate email integration class. Just used
// to send an email even if there is no integration at all.
try {
const mail = new EventAttendeeMail(evt, hashUID);
await mail.sendEmail();
} catch (e) {
log.error("Sending legacy event mail failed", e);
log.error(`Booking ${user} failed`);
res.status(500).json({ message: "Booking failed" });
return;
}
}
referencesToCreate = results.map((result) => {
return {
type: result.type,
uid: result.response.createdEvent.id.toString(),
};
});
}
const hashUID =
results.length > 0
? results[0].response.uid
: translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
// TODO Should just be set to the true case as soon as we have a "bare email" integration class.
// UID generation should happen in the integration itself, not here.
if (results.length === 0) {
// Legacy as well, as soon as we have a separate email integration class. Just used
// to send an email even if there is no integration at all.
try {
const mail = new EventAttendeeMail(evt, hashUID);
await mail.sendEmail();
await prisma.booking.create({
data: {
uid: hashUID,
userId: currentUser.id,
references: {
create: referencesToCreate,
},
eventTypeId: eventType.id,
title: evt.title,
description: evt.description,
startTime: evt.startTime,
endTime: evt.endTime,
attendees: {
create: evt.attendees,
},
},
});
} catch (e) {
console.error("Sending legacy event mail failed", e);
res.status(500).json({ message: "Booking failed" });
log.error(`Booking ${user} failed`, "Error when saving booking to db", e);
res.status(500).json({ message: "Booking already exists" });
return;
}
log.debug(`Booking ${user} completed`);
return res.status(204).json({ message: "Booking completed" });
} catch (reason) {
log.error(`Booking ${user} failed`, reason);
return res.status(500).json({ message: "Booking failed for some unknown reason" });
}
try {
await prisma.booking.create({
data: {
uid: hashUID,
userId: currentUser.id,
references: {
create: referencesToCreate,
},
eventTypeId: eventType.id,
title: evt.title,
description: evt.description,
startTime: evt.startTime,
endTime: evt.endTime,
attendees: {
create: evt.attendees,
},
},
});
} catch (e) {
console.error("Error when saving booking to db", e);
res.status(500).json({ message: "Booking already exists" });
return;
}
res.status(204).json({});
}

View File

@ -5402,7 +5402,7 @@ source-map-js@^0.6.2:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
source-map-support@^0.5.6:
source-map-support@^0.5.19, source-map-support@^0.5.6:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@ -5873,6 +5873,13 @@ tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tslog@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.0.tgz#4982c289a8948670d6a1c49c29977ae9f861adfa"
integrity sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A==
dependencies:
source-map-support "^0.5.19"
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"