diff --git a/packages/attestation-service/README.md b/packages/attestation-service/README.md index 84893323b1a..b1d0178a690 100644 --- a/packages/attestation-service/README.md +++ b/packages/attestation-service/README.md @@ -19,6 +19,18 @@ You can use the following environment variables to configure the attestation ser - `NEXMO_SECRET` - The API secret to the Nexmo API - `NEXMO_BLACKLIST` - A comma-sperated list of country codes you do not want to serve +`twilio` + +- `TWILIO_ACCOUNT_SID` - The SID of your Twilio account +- `TWILIO_MESSAGE_SERVICE_SID` - The SID of the messaging service you want to use. The messaging service should have at least 1 phone number associated with it. +- `TWILIO_AUTH_TOKEN` - The auth token for your Twilio account + +### Operations + +This service uses `bunyan` for structured logging with JSON lines. You can pipe STDOUT to `yarn run bunyan` for a more human friendly output. The `LOG_LEVEL` environment variable can specify desired log levels. With `LOG_FORMAT=stackdriver` you can get stackdriver specific format to recover information such as error traces etc. + +This service exposes prometheus metrics under `/metrics`. + ### Running locally After checking out the source, you should create a local sqlite database by running: diff --git a/packages/attestation-service/config/.env.development b/packages/attestation-service/config/.env.development index ef15b10adb5..76186201b78 100644 --- a/packages/attestation-service/config/.env.development +++ b/packages/attestation-service/config/.env.development @@ -11,3 +11,7 @@ TWILIO_ACCOUNT_SID=x TWILIO_MESSAGING_SERVICE_SID=x TWILIO_AUTH_TOKEN=x TWILIO_BLACKLIST= +# Options: default, stackdriver +LOG_FORMAT= +# Options: fatal, error, warn, info, debug, trace +LOG_LEVEL= diff --git a/packages/attestation-service/index.d.ts b/packages/attestation-service/index.d.ts index ce576a76194..63299f7e48e 100644 --- a/packages/attestation-service/index.d.ts +++ b/packages/attestation-service/index.d.ts @@ -1 +1,2 @@ declare module 'nexmo' +declare module 'express-request-id' diff --git a/packages/attestation-service/package.json b/packages/attestation-service/package.json index e82ad710aaa..9ee640d9a62 100644 --- a/packages/attestation-service/package.json +++ b/packages/attestation-service/package.json @@ -31,6 +31,8 @@ "@celo/utils": "^0.1.0", "bignumber.js": "^7.2.0", "body-parser": "1.19.0", + "bunyan": "1.8.12", + "bunyan-gke-stackdriver": "0.1.2", "debug": "^4.1.1", "dotenv": "8.0.0", "eth-lib": "^0.2.8", @@ -40,9 +42,11 @@ "web3": "1.0.0-beta.37", "express": "4.17.1", "express-rate-limit": "5.0.0", + "express-request-id": "1.4.1", "mysql2": "2.0.0-alpha1", "pg": "7.12.1", "pg-hstore": "2.3.3", + "prom-client": "11.2.0", "sequelize": "5.13.1", "sequelize-cli": "5.5.0", "sqlite3": "4.0.9", @@ -51,6 +55,7 @@ }, "devDependencies": { "@celo/protocol": "1.0.0", + "@types/bunyan": "1.8.4", "@types/dotenv": "4.0.3", "@types/debug": "^4.1.5", "@types/express-rate-limit": "2.9.3", diff --git a/packages/attestation-service/src/db.ts b/packages/attestation-service/src/db.ts index feec25c5bfd..06817c3275f 100644 --- a/packages/attestation-service/src/db.ts +++ b/packages/attestation-service/src/db.ts @@ -1,13 +1,17 @@ import { ContractKit, newKit } from '@celo/contractkit' import { FindOptions, Sequelize } from 'sequelize' import { fetchEnv } from './env' +import { rootLogger } from './logger' import Attestation, { AttestationModel, AttestationStatic } from './models/attestation' export let sequelize: Sequelize | undefined export function initializeDB() { if (sequelize === undefined) { - sequelize = new Sequelize(fetchEnv('DATABASE_URL')) + sequelize = new Sequelize(fetchEnv('DATABASE_URL'), { + logging: (msg: string, sequelizeLogArgs: any) => + rootLogger.debug({ sequelizeLogArgs, component: 'sequelize' }, msg), + }) return sequelize.authenticate() as Promise } return Promise.resolve() diff --git a/packages/attestation-service/src/env.ts b/packages/attestation-service/src/env.ts index adcc44f32b2..fc8e9306b08 100644 --- a/packages/attestation-service/src/env.ts +++ b/packages/attestation-service/src/env.ts @@ -1,5 +1,11 @@ +import * as dotenv from 'dotenv' + +if (process.env.CONFIG) { + dotenv.config({ path: process.env.CONFIG }) +} + export function fetchEnv(name: string): string { - if (process.env[name] === undefined) { + if (process.env[name] === undefined || process.env[name] === '') { console.error(`ENV var '${name}' was not defined`) throw new Error(`ENV var '${name}' was not defined`) } @@ -7,5 +13,7 @@ export function fetchEnv(name: string): string { } export function fetchEnvOrDefault(name: string, defaultValue: string): string { - return process.env[name] === undefined ? defaultValue : (process.env[name] as string) + return process.env[name] === undefined || process.env[name] === '' + ? defaultValue + : (process.env[name] as string) } diff --git a/packages/attestation-service/src/index.ts b/packages/attestation-service/src/index.ts index 8198c4192a0..f460647f9d6 100644 --- a/packages/attestation-service/src/index.ts +++ b/packages/attestation-service/src/index.ts @@ -1,8 +1,10 @@ -import * as dotenv from 'dotenv' import express from 'express' import RateLimiter from 'express-rate-limit' +import requestIdMiddleware from 'express-request-id' +import * as PromClient from 'prom-client' import { initializeDB, initializeKit } from './db' -import { createValidatedHandler } from './request' +import { rootLogger } from './logger' +import { createValidatedHandler, loggerMiddleware } from './request' import { AttestationRequestType, getAttestationKey, @@ -10,12 +12,8 @@ import { } from './requestHandlers/attestation' import { handleStatusRequest, StatusRequestType } from './requestHandlers/status' import { initializeSmsProviders } from './sms' -async function init() { - console.info(process.env.CONFIG) - if (process.env.CONFIG) { - dotenv.config({ path: process.env.CONFIG }) - } +async function init() { await initializeDB() await initializeKit() // TODO: Validate that the attestation key has been authorized by the account @@ -23,9 +21,9 @@ async function init() { await initializeSmsProviders() const app = express() - app.use(express.json()) + app.use([express.json(), requestIdMiddleware(), loggerMiddleware]) const port = process.env.PORT || 3000 - app.listen(port, () => console.log(`Server running on ${port}!`)) + app.listen(port, () => rootLogger.info({ port }, 'Attestation Service started')) const rateLimiter = new RateLimiter({ windowMs: 5 * 60 * 100, // 5 minutes @@ -33,6 +31,9 @@ async function init() { // @ts-ignore message: { status: false, error: 'Too many requests, please try again later' }, }) + app.get('/metrics', (_req, res) => { + res.send(PromClient.register.metrics()) + }) app.get('/status', rateLimiter, createValidatedHandler(StatusRequestType, handleStatusRequest)) app.post( '/attestations', @@ -41,7 +42,6 @@ async function init() { } init().catch((err) => { - console.error(`Error occurred while running server, exiting ....`) - console.error(err) + rootLogger.error({ err }) process.exit(1) }) diff --git a/packages/attestation-service/src/logger.ts b/packages/attestation-service/src/logger.ts new file mode 100644 index 00000000000..4badc84df01 --- /dev/null +++ b/packages/attestation-service/src/logger.ts @@ -0,0 +1,17 @@ +import Logger, { createLogger, levelFromName, LogLevelString, stdSerializers } from 'bunyan' +import { createStream } from 'bunyan-gke-stackdriver' +import { fetchEnvOrDefault } from './env' + +const logLevel = fetchEnvOrDefault('LOG_LEVEL', 'info') as LogLevelString +const logFormat = fetchEnvOrDefault('LOG_FORMAT', 'default') + +const stream = + logFormat === 'stackdriver' + ? createStream(levelFromName[logLevel]) + : { stream: process.stdout, level: logLevel } + +export const rootLogger: Logger = createLogger({ + name: 'attestation-service', + serializers: stdSerializers, + streams: [stream], +}) diff --git a/packages/attestation-service/src/metrics.ts b/packages/attestation-service/src/metrics.ts new file mode 100644 index 00000000000..cf9ed488b47 --- /dev/null +++ b/packages/attestation-service/src/metrics.ts @@ -0,0 +1,45 @@ +import { Counter } from 'prom-client' + +export const Counters = { + attestationRequestsTotal: new Counter({ + name: 'attestation_requests_total', + help: 'Counter for the number of attestation requests', + }), + attestationRequestsAlreadySent: new Counter({ + name: 'attestation_requests_already_sent', + help: 'Counter for the number of attestation requests that were already sent', + }), + attestationRequestsWrongIssuer: new Counter({ + name: 'attestation_requests_wrong_issuer', + help: 'Counter for the number of attestation requests that specified the wrong issuer', + }), + attestationRequestsWOIncompleteAttestation: new Counter({ + name: 'attestation_requests_without_incomplete_attestation', + help: + 'Counter for the number of attestation requests for which no incomplete attestations could be found', + }), + attestationRequestsValid: new Counter({ + name: 'attestation_requests_valid', + help: 'Counter for the number of requests involving valid attestation requests', + }), + attestationRequestsAttestationErrors: new Counter({ + name: 'attestation_requests_attestation_errors', + help: 'Counter for the number of requests for which producing the attestation failed', + }), + attestationRequestsUnableToServe: new Counter({ + name: 'attestation_requests_unable_to_serve', + help: 'Counter for the number of requests that could not be served', + }), + attestationRequestsSentSms: new Counter({ + name: 'attestation_requests_sent_sms', + help: 'Counter for the number of sms sent', + }), + attestationRequestsFailedToSendSms: new Counter({ + name: 'attestation_requests_failed_to_send_sms', + help: 'Counter for the number of sms that failed to send', + }), + attestationRequestUnexpectedErrors: new Counter({ + name: 'attestation_requests_unexpected_errors', + help: 'Counter for the number of unexpected errrors', + }), +} diff --git a/packages/attestation-service/src/request.ts b/packages/attestation-service/src/request.ts index 82a2f6b7825..d3dee4bf017 100644 --- a/packages/attestation-service/src/request.ts +++ b/packages/attestation-service/src/request.ts @@ -1,29 +1,36 @@ +import Logger from 'bunyan' import express from 'express' import { isLeft } from 'fp-ts/lib/Either' import * as t from 'io-ts' +import { rootLogger } from './logger' export enum ErrorMessages { UNKNOWN_ERROR = 'Something went wrong', } -export function catchAsyncErrorHandler( - handler: (req: express.Request, res: express.Response) => Promise -) { - return async (req: express.Request, res: express.Response) => { +export function asyncHandler(handler: (req: express.Request, res: Response) => Promise) { + return (req: express.Request, res: Response) => { + const handleUnknownError = (error: Error) => { + if (res.locals.logger) { + res.locals.logger.error(error) + } + respondWithError(res, 500, ErrorMessages.UNKNOWN_ERROR) + } try { - return handler(req, res) + handler(req, res) + .then(() => res.locals.logger.info({ res })) + .catch(handleUnknownError) } catch (error) { - console.error(error) - respondWithError(res, 500, ErrorMessages.UNKNOWN_ERROR) + handleUnknownError(error) } } } export function createValidatedHandler( requestType: t.Type, - handler: (req: express.Request, res: express.Response, parsedRequest: T) => Promise + handler: (req: express.Request, res: Response, parsedRequest: T) => Promise ) { - return catchAsyncErrorHandler(async (req: express.Request, res: express.Response) => { + return asyncHandler(async (req: express.Request, res: Response) => { const parsedRequest = requestType.decode({ ...req.query, ...req.body }) if (isLeft(parsedRequest)) { res.status(422).json({ @@ -64,3 +71,22 @@ function serializeErrors(errors: t.Errors) { export function respondWithError(res: express.Response, statusCode: number, error: string) { res.status(statusCode).json({ success: false, error }) } + +export type Response = Omit & { + locals: { logger: Logger } & Omit +} + +export function loggerMiddleware( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const requestLogger = rootLogger.child({ + // @ts-ignore express-request-id adds this + req_id: req.id, + }) + + res.locals.logger = requestLogger + requestLogger.info({ req }) + next() +} diff --git a/packages/attestation-service/src/requestHandlers/attestation.ts b/packages/attestation-service/src/requestHandlers/attestation.ts index b0f7bf54b37..dc1deb6c089 100644 --- a/packages/attestation-service/src/requestHandlers/attestation.ts +++ b/packages/attestation-service/src/requestHandlers/attestation.ts @@ -1,17 +1,18 @@ import { AttestationState } from '@celo/contractkit/lib/wrappers/Attestations' import { attestToIdentifier, SignatureUtils } from '@celo/utils' -import { Address, isValidPrivateKey, toChecksumAddress } from '@celo/utils/lib/address' -import { AddressType, E164Number, E164PhoneNumberType } from '@celo/utils/lib/io' +import { isValidPrivateKey, toChecksumAddress } from '@celo/utils/lib/address' +import { AddressType, E164PhoneNumberType } from '@celo/utils/lib/io' +import Logger from 'bunyan' import { isValidAddress } from 'ethereumjs-util' import express from 'express' import * as t from 'io-ts' import { Transaction } from 'sequelize' import { existingAttestationRequestRecord, getAttestationTable, kit, sequelize } from '../db' +import { Counters } from '../metrics' import { AttestationModel, AttestationStatus } from '../models/attestation' -import { respondWithError } from '../request' +import { respondWithError, Response } from '../request' import { smsProviderFor } from '../sms' import { SmsProviderType } from '../sms/base' - const SMS_SENDING_ERROR = 'Something went wrong while attempting to send SMS, try again later' const ATTESTATION_ERROR = 'Valid attestation could not be provided' const NO_INCOMPLETE_ATTESTATION_FOUND_ERROR = 'No incomplete attestation found' @@ -47,63 +48,6 @@ export function getAccountAddress() { return toChecksumAddress(process.env.ACCOUNT_ADDRESS) } -async function validateAttestationRequest(request: AttestationRequest) { - const attestationRecord = await existingAttestationRequestRecord( - request.phoneNumber, - request.account, - request.issuer - ) - // check if it exists in the database - if (attestationRecord && !attestationRecord.canSendSms()) { - throw new Error(ATTESTATION_ALREADY_SENT_ERROR) - } - const address = getAccountAddress() - - // TODO: Check with the new Accounts.sol - if (address.toLowerCase() !== request.issuer.toLowerCase()) { - throw new Error(`Mismatching issuer, I am ${address}`) - } - - const attestations = await kit.contracts.getAttestations() - const state = await attestations.getAttestationState( - request.phoneNumber, - request.account, - request.issuer - ) - - if (state.attestationState !== AttestationState.Incomplete) { - throw new Error(NO_INCOMPLETE_ATTESTATION_FOUND_ERROR) - } - - // TODO: Check expiration - return -} - -async function validateAttestation( - attestationRequest: AttestationRequest, - attestationCode: string -) { - const address = getAccountAddress() - const attestations = await kit.contracts.getAttestations() - const isValid = await attestations.validateAttestationCode( - attestationRequest.phoneNumber, - attestationRequest.account, - address, - attestationCode - ) - - if (!isValid) { - throw new Error(ATTESTATION_ERROR) - } - return -} - -function signAttestation(phoneNumber: E164Number, account: Address) { - const signature = attestToIdentifier(phoneNumber, account, getAttestationKey()) - - return SignatureUtils.serializeSignature(signature) -} - function toBase64(str: string) { return Buffer.from(str.slice(2), 'hex').toString('base64') } @@ -151,101 +95,176 @@ async function ensureLockedRecord( return attestationRecord } -async function sendSmsAndPersistAttestation( - attestationRequest: AttestationRequest, - attestationCode: string -) { - const textMessage = createAttestationTextMessage(attestationCode) - let attestationRecord: AttestationModel | null = null +class AttestationRequestHandler { + logger: Logger + sequelizeLogger: (_msg: string, sequelizeLog: any) => void + constructor(public readonly attestationRequest: AttestationRequest, logger: Logger) { + this.logger = logger.child({ attestationRequest }) + this.sequelizeLogger = (msg: string, sequelizeLogArgs: any) => + this.logger.debug({ sequelizeLogArgs, component: 'sequelize' }, msg) + } - const transaction = await sequelize!.transaction() + async validateAttestationRequest() { + const attestationRecord = await existingAttestationRequestRecord( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + this.attestationRequest.issuer, + { logging: this.sequelizeLogger } + ) + // check if it exists in the database + if (attestationRecord && !attestationRecord.canSendSms()) { + Counters.attestationRequestsAlreadySent.inc() + throw new Error(ATTESTATION_ALREADY_SENT_ERROR) + } + const address = getAccountAddress() - try { - attestationRecord = await ensureLockedRecord(attestationRequest, transaction) - const provider = smsProviderFor(attestationRequest.phoneNumber) - - if (!provider) { - await attestationRecord.update( - { status: AttestationStatus.UNABLE_TO_SERVE, smsProvider: SmsProviderType.UNKNOWN }, - { transaction } - ) - await transaction.commit() - return attestationRecord + // TODO: Check with the new Accounts.sol + if (address.toLowerCase() !== this.attestationRequest.issuer.toLowerCase()) { + Counters.attestationRequestsWrongIssuer.inc() + throw new Error(`Mismatching issuer, I am ${address}`) } - try { - await provider.sendSms(attestationRequest.phoneNumber, textMessage) - await attestationRecord.update( - { status: AttestationStatus.SENT, smsProvider: provider.type }, - { transaction } - ) - } catch (error) { - console.error(error) - await attestationRecord.update( - { status: AttestationStatus.FAILED, smsProvider: provider.type }, - { transaction } - ) + const attestations = await kit.contracts.getAttestations() + const state = await attestations.getAttestationState( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + this.attestationRequest.issuer + ) + + if (state.attestationState !== AttestationState.Incomplete) { + Counters.attestationRequestsWOIncompleteAttestation.inc() + throw new Error(NO_INCOMPLETE_ATTESTATION_FOUND_ERROR) } - await transaction.commit() - } catch (error) { - console.error(error) - await transaction.rollback() + // TODO: Check expiration + return } - return attestationRecord -} + signAttestation() { + const signature = attestToIdentifier( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + getAttestationKey() + ) -function respondAfterSendingSms(res: express.Response, attestationRecord: AttestationModel | null) { - if (!attestationRecord) { - console.error('Attestation Record was not created') - respondWithError(res, 500, SMS_SENDING_ERROR) + return SignatureUtils.serializeSignature(signature) + } + + async validateAttestation(attestationCode: string) { + const address = getAccountAddress() + const attestations = await kit.contracts.getAttestations() + const isValid = await attestations.validateAttestationCode( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + address, + attestationCode + ) + + if (!isValid) { + Counters.attestationRequestsAttestationErrors.inc() + throw new Error(ATTESTATION_ERROR) + } return } - switch (attestationRecord.status) { - case AttestationStatus.SENT: - res.status(201).json({ success: true }) - return - case AttestationStatus.FAILED: - respondWithError(res, 500, SMS_SENDING_ERROR) - return - case AttestationStatus.UNABLE_TO_SERVE: - respondWithError(res, 422, COUNTRY_CODE_NOT_SERVED_ERROR) - default: - console.error( - 'Attestation Record should either be failed or sent, but was ', - attestationRecord.status - ) + async sendSmsAndPersistAttestation(attestationCode: string) { + const textMessage = createAttestationTextMessage(attestationCode) + let attestationRecord: AttestationModel | null = null + + const transaction = await sequelize!.transaction({ logging: this.sequelizeLogger }) + + try { + attestationRecord = await ensureLockedRecord(this.attestationRequest, transaction) + const provider = smsProviderFor(this.attestationRequest.phoneNumber) + + if (!provider) { + await attestationRecord.update( + { status: AttestationStatus.UNABLE_TO_SERVE, smsProvider: SmsProviderType.UNKNOWN }, + { transaction, logging: this.sequelizeLogger } + ) + await transaction.commit() + Counters.attestationRequestsUnableToServe.inc() + return attestationRecord + } + + try { + await provider.sendSms(this.attestationRequest.phoneNumber, textMessage) + Counters.attestationRequestsSentSms.inc() + await attestationRecord.update( + { status: AttestationStatus.SENT, smsProvider: provider.type }, + { transaction, logging: this.sequelizeLogger } + ) + } catch (err) { + this.logger.error({ err }) + Counters.attestationRequestsFailedToSendSms.inc() + await attestationRecord.update( + { status: AttestationStatus.FAILED, smsProvider: provider.type }, + { transaction, logging: this.sequelizeLogger } + ) + } + + await transaction.commit() + } catch (err) { + this.logger.error({ err }) + await transaction.rollback() + } + + return attestationRecord + } + + respondAfterSendingSms(res: express.Response, attestationRecord: AttestationModel | null) { + if (!attestationRecord) { + this.logger.error({ err: 'Attestation Record was not created' }) respondWithError(res, 500, SMS_SENDING_ERROR) return + } + + switch (attestationRecord.status) { + case AttestationStatus.SENT: + res.status(201).json({ success: true }) + return + case AttestationStatus.FAILED: + respondWithError(res, 500, SMS_SENDING_ERROR) + return + case AttestationStatus.UNABLE_TO_SERVE: + respondWithError(res, 422, COUNTRY_CODE_NOT_SERVED_ERROR) + default: + this.logger.error({ + err: + 'Attestation Record should either be failed or sent, but was ' + + attestationRecord.status, + }) + respondWithError(res, 500, SMS_SENDING_ERROR) + return + } } } export async function handleAttestationRequest( _req: express.Request, - res: express.Response, + res: Response, attestationRequest: AttestationRequest ) { + const handler = new AttestationRequestHandler(attestationRequest, res.locals.logger) let attestationCode try { - await validateAttestationRequest(attestationRequest) - attestationCode = signAttestation(attestationRequest.phoneNumber, attestationRequest.account) - await validateAttestation(attestationRequest, attestationCode) - } catch (error) { - console.error(error) - respondWithError(res, 422, error.toString()) + Counters.attestationRequestsTotal.inc() + await handler.validateAttestationRequest() + Counters.attestationRequestsValid.inc() + attestationCode = handler.signAttestation() + await handler.validateAttestation(attestationCode) + } catch (err) { + handler.logger.info({ err }) + respondWithError(res, 422, err.toString()) return } try { - const attestationRecord = await sendSmsAndPersistAttestation( - attestationRequest, - attestationCode - ) - respondAfterSendingSms(res, attestationRecord) - } catch (error) { - console.error(error) + const attestationRecord = await handler.sendSmsAndPersistAttestation(attestationCode) + handler.respondAfterSendingSms(res, attestationRecord) + } catch (err) { + Counters.attestationRequestUnexpectedErrors.inc() + handler.logger.error({ err }) respondWithError(res, 500, SMS_SENDING_ERROR) return } diff --git a/yarn.lock b/yarn.lock index af920842271..4b7858f4117 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3711,6 +3711,14 @@ dependencies: "@types/node" "*" +"@types/bunyan@1.8.4": + version "1.8.4" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.4.tgz#69c11adc7b50538d45fb68d9ae39d062b9432f38" + integrity sha512-bxOF3fsm69ezKxdcJ7Oo/PsZMOJ+JIV/QJO2IADfScmR3sLulR88dpSnz6+q+9JJ1kD7dXFFgUrGRSKHLkOX7w== + dependencies: + "@types/events" "*" + "@types/node" "*" + "@types/bytebuffer@^5.0.40": version "5.0.40" resolved "https://registry.yarnpkg.com/@types/bytebuffer/-/bytebuffer-5.0.40.tgz#d6faac40dcfb09cd856cdc4c01d3690ba536d3ee" @@ -7864,7 +7872,12 @@ bunyan-debug-stream@^1.1.0: colors "^1.0.3" exception-formatter "^1.0.4" -bunyan@^1.8.12: +bunyan-gke-stackdriver@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/bunyan-gke-stackdriver/-/bunyan-gke-stackdriver-0.1.2.tgz#a47e3724bbb324b1ec0b7dc4350c4d7073aae66d" + integrity sha512-eY5OLgAXvOvOq2YpxI0HlV5HjAcLm36Ln3PxxsztO+2GrFSgU3oXoic2LCif/heBKoyOZdMyXKWF5dvswSOS6w== + +bunyan@1.8.12, bunyan@^1.8.12: version "1.8.12" resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.12.tgz#f150f0f6748abdd72aeae84f04403be2ef113797" integrity sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c= @@ -12267,6 +12280,13 @@ express-rate-limit@5.0.0: resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.0.0.tgz#9a6f4cacc388c1a1da7ba2f65db69f7395e9b04e" integrity sha512-dhT57wqxfqmkOi4HM7NuT4Gd7gbUgSK2ocG27Y6lwm8lbOAw9XQfeANawGq8wLDtlGPO1ZgDj0HmKsykTxfFAg== +express-request-id@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/express-request-id/-/express-request-id-1.4.1.tgz#66feaf1f0be3deca13f3abfe1c098e49b12f2c28" + integrity sha512-qpxK6XhDYtdx9FvxwCHkUeZVWtkGbWR87hBAzGECfwYF/QQCPXEwwB2/9NGkOR1tT7/aLs9mma3CT0vjSzuZVw== + dependencies: + uuid "^3.3.2" + express@4.16.4: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" @@ -30362,6 +30382,7 @@ websocket@1.0.29, websocket@^1.0.28: dependencies: debug "^2.2.0" es5-ext "^0.10.50" + gulp "^4.0.2" nan "^2.14.0" typedarray-to-buffer "^3.1.5" yaeti "^0.0.6"