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

fix(qe): fix error logging #16287

Merged
merged 18 commits into from
Nov 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/client/src/runtime/RequestHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Context } from '@opentelemetry/api'
import Debug from '@prisma/debug'
import { getTraceParent, hasBatchIndex, TracingConfig } from '@prisma/engine-core'
import { EventEmitter, getTraceParent, hasBatchIndex, TracingConfig } from '@prisma/engine-core'
import stripAnsi from 'strip-ansi'

import {
Expand All @@ -11,7 +11,7 @@ import {
} from '.'
import { PrismaPromiseTransaction } from './core/request/PrismaPromise'
import { DataLoader } from './DataLoader'
import type { Client, Unpacker } from './getPrismaClient'
import type { Client, LogEvent, Unpacker } from './getPrismaClient'
import type { EngineMiddleware } from './MiddlewareHandler'
import type { Document } from './query'
import { Args, unpack } from './query'
Expand Down Expand Up @@ -78,8 +78,10 @@ export class RequestHandler {
client: Client
hooks: any
dataloader: DataLoader<Request>
private logEmmitter?: EventEmitter

constructor(client: Client, hooks?: any) {
constructor(client: Client, hooks?: any, logEmitter?: EventEmitter) {
this.logEmmitter = logEmitter
this.client = client
this.hooks = hooks
this.dataloader = new DataLoader({
Expand Down Expand Up @@ -195,7 +197,22 @@ export class RequestHandler {
}
return unpackResult
} catch (error) {
this.handleAndLogRequestError({ error, clientMethod, callsite, transaction })
}
}

/**
* Handles the error and logs it, logging the error is done synchronously waiting for the event
* handlers to finish.
*/
handleAndLogRequestError({ error, clientMethod, callsite, transaction }: HandleErrorParams): never {
try {
this.handleRequestError({ error, clientMethod, callsite, transaction })
} catch (err) {
if (this.logEmmitter) {
this.logEmmitter.emit('error', { message: err.message, target: clientMethod, timestamp: new Date() })
}
throw err
}
}

Expand Down
15 changes: 13 additions & 2 deletions packages/client/src/runtime/getPrismaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { DataSource, GeneratorConfig } from '@prisma/generator-helper'
import { callOnce, ClientEngineType, getClientEngineType, logger, tryLoadEnvs, warnOnce } from '@prisma/internals'
import type { LoadedEnv } from '@prisma/internals/dist/utils/tryLoadEnvs'
import { AsyncResource } from 'async_hooks'
import { EventEmitter } from 'events'
import fs from 'fs'
import path from 'path'
import { RawValue, Sql } from 'sql-template-tag'
Expand Down Expand Up @@ -342,6 +343,15 @@ export function getPrismaClient(config: GetPrismaClientConfig) {
validatePrismaClientOptions(optionsArg, config.datasourceNames)
}

const logEmitter = new EventEmitter().on('error', (e) => {
miguelff marked this conversation as resolved.
Show resolved Hide resolved
// this is a no-op to prevent unhandled error events
//
// If the user enabled error logging this would never be executed. If the user did not
// enabled error logging, this would be executed, and a trace for the error would be logged
// in debug mode, which is like going in the opposite direction than what the user wanted by
// not enabling error logging in the first place.
})

this._extensions = []
this._previewFeatures = config.generator?.previewFeatures ?? []
this._rejectOnNotFound = optionsArg?.rejectOnNotFound
Expand Down Expand Up @@ -447,6 +457,7 @@ export function getPrismaClient(config: GetPrismaClientConfig) {
inlineDatasources: config.inlineDatasources,
inlineSchemaHash: config.inlineSchemaHash,
tracingConfig: this._tracingConfig,
logEmitter: logEmitter,
}

debug('clientVersion', config.clientVersion)
Expand All @@ -460,7 +471,7 @@ export function getPrismaClient(config: GetPrismaClientConfig) {
this._engine = this.getEngine()
void this._getActiveProvider()

this._fetcher = new RequestHandler(this, this._hooks) as any
this._fetcher = new RequestHandler(this, this._hooks, logEmitter) as any

if (options.log) {
for (const log of options.log) {
Expand Down Expand Up @@ -1221,7 +1232,7 @@ new PrismaClient({
const dmmf = await this._engine.getDmmf()
return new DMMFHelper(getPrismaClientDMMF(dmmf))
} catch (error) {
this._fetcher.handleRequestError({ ...params, error })
this._fetcher.handleAndLogRequestError({ ...params, error })
}
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineMatrix } from '../_utils/defineMatrix'
import { allProviders } from '../_utils/providers'

export default defineMatrix(() => [allProviders])
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { idForProvider } from '../../_utils/idForProvider'
import testMatrix from '../_matrix'

export default testMatrix.setupSchema(({ provider }) => {
return /* Prisma */ `
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "${provider}"
url = env("DATABASE_URI_${provider}")
}

model User {
id ${idForProvider(provider)}
email String @unique
}
`
})
66 changes: 66 additions & 0 deletions packages/client/tests/functional/query-error-logging/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { faker } from '@faker-js/faker'
// @ts-ignore
import type { PrismaClient } from '@prisma/client'

import { LogEvent } from '../../../src/runtime/getPrismaClient'
import { NewPrismaClient } from '../_utils/types'
import testMatrix from './_matrix'

let prisma: PrismaClient<{ log: [{ emit: 'event'; level: 'error' }] }>
declare let newPrismaClient: NewPrismaClient<typeof PrismaClient>

const email = faker.internet.email()

testMatrix.setupTestSuite(
() => {
const errors: LogEvent[] = []

beforeAll(() => {
prisma = newPrismaClient({ log: [{ emit: 'event', level: 'error' }] })
prisma.$on('error', (e) => errors.push(e))
})

afterEach(() => {
errors.length = 0
})

test('findUniqueOrThrown when error thrown', async () => {
await expect(() =>
prisma.user.findUniqueOrThrow({
where: {
email,
},
}),
).rejects.toThrowError('No User found')

expect(errors).toHaveLength(1)

const errorEvent = errors[0]
expect(errorEvent.message).toContain(
'An operation failed because it depends on one or more records that were required but not found. Expected a record, found none.',
)
expect(errorEvent.target).toContain('user.findUniqueOrThrow')
})

test('findFirstOrThrow when error thrown', async () => {
await expect(() =>
prisma.user.findFirstOrThrow({
where: {
email,
},
}),
).rejects.toThrowError('No User found')

expect(errors).toHaveLength(1)

const errorEvent = errors[0]
expect(errorEvent.message).toContain(
'An operation failed because it depends on one or more records that were required but not found. Expected a record, found none.',
)
expect(errorEvent.target).toContain('user.findFirstOrThrow')
})
},
{
skipDefaultClientInstance: true,
},
)