Log slow Blink database operations (#11505)

This commit is contained in:
Austin McKinley
2026-08-19 12:38:48 -07:00
committed by GitHub
parent a329899552
commit cc26e5a090
5 changed files with 98 additions and 16 deletions
+4 -2
View File
@@ -98,13 +98,15 @@ export class SafelinkClient {
url: string, url: string,
pattern: ToolsOzoneSafelinkDefs.PatternType, pattern: ToolsOzoneSafelinkDefs.PatternType,
): Promise<SafelinkRule> { ): Promise<SafelinkRule> {
return db.db return db.observeQuery(`resolve_safelink_${pattern}_rule`, () =>
db.db
.selectFrom('safelink_rule') .selectFrom('safelink_rule')
.selectAll() .selectAll()
.where('url', '=', url) .where('url', '=', url)
.where('pattern', '=', pattern) .where('pattern', '=', pattern)
.orderBy('createdAt', 'desc') .orderBy('createdAt', 'desc')
.executeTakeFirstOrThrow() .executeTakeFirstOrThrow(),
)
} }
private async addRule(db: Database, rule: SafelinkRule) { private async addRule(db: Database, rule: SafelinkRule) {
+48
View File
@@ -1,4 +1,5 @@
import assert from 'assert' import assert from 'assert'
import {performance} from 'node:perf_hooks'
import { import {
Kysely, Kysely,
type KyselyPlugin, type KyselyPlugin,
@@ -17,6 +18,8 @@ import {default as migrations} from './migrations/index.js'
import {DbMigrationProvider} from './migrations/provider.js' import {DbMigrationProvider} from './migrations/provider.js'
import {type DbSchema} from './schema.js' import {type DbSchema} from './schema.js'
const SLOW_QUERY_THRESHOLD_MS = 1000
export class Database { export class Database {
migrator: Migrator migrator: Migrator
destroyed = false destroyed = false
@@ -101,6 +104,51 @@ export class Database {
return this.db.isTransaction return this.db.isTransaction
} }
async observeQuery<T>(
operation: string,
query: () => Promise<T>,
): Promise<T> {
const poolIdleConnectionsAtStart = this.cfg.pool.idleCount
const poolTotalConnectionsAtStart = this.cfg.pool.totalCount
const poolWaitingRequestsAtStart = this.cfg.pool.waitingCount
const startedAt = performance.now()
let poolIdleConnectionsAtThreshold: number | undefined
let poolTotalConnectionsAtThreshold: number | undefined
let poolWaitingRequestsAtThreshold: number | undefined
const slowQueryTimer = setTimeout(() => {
poolIdleConnectionsAtThreshold = this.cfg.pool.idleCount
poolTotalConnectionsAtThreshold = this.cfg.pool.totalCount
poolWaitingRequestsAtThreshold = this.cfg.pool.waitingCount
}, SLOW_QUERY_THRESHOLD_MS)
slowQueryTimer.unref()
try {
return await query()
} finally {
clearTimeout(slowQueryTimer)
const durationMs = Math.round(performance.now() - startedAt)
if (durationMs >= SLOW_QUERY_THRESHOLD_MS) {
log.warn(
{
durationMs,
operation,
poolIdleConnectionsAtEnd: this.cfg.pool.idleCount,
poolIdleConnectionsAtStart,
poolIdleConnectionsAtThreshold,
poolStateAtThresholdCaptured:
poolWaitingRequestsAtThreshold !== undefined,
poolTotalConnectionsAtEnd: this.cfg.pool.totalCount,
poolTotalConnectionsAtStart,
poolTotalConnectionsAtThreshold,
poolWaitingRequestsAtEnd: this.cfg.pool.waitingCount,
poolWaitingRequestsAtStart,
poolWaitingRequestsAtThreshold,
},
'slow database query',
)
}
}
}
assertTransaction() { assertTransaction() {
assert(this.isTransaction, 'Transaction required') assert(this.isTransaction, 'Transaction required')
} }
+2 -2
View File
@@ -8,7 +8,7 @@ import {linkRedirectContents} from '../html/linkRedirectContents.js'
import {linkWarningContents} from '../html/linkWarningContents.js' import {linkWarningContents} from '../html/linkWarningContents.js'
import {linkWarningLayout} from '../html/linkWarningLayout.js' import {linkWarningLayout} from '../html/linkWarningLayout.js'
import {redirectLogger} from '../logger.js' import {redirectLogger} from '../logger.js'
import {handler} from './util.js' import {observedHandler} from './util.js'
const INTERNAL_IP_REGEX = new RegExp( const INTERNAL_IP_REGEX = new RegExp(
'(^127.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^10.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.1[6-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.2[0-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.3[0-1]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)|^localhost', '(^127.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^10.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.1[6-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.2[0-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.3[0-1]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)|^localhost',
@@ -18,7 +18,7 @@ const INTERNAL_IP_REGEX = new RegExp(
export default function (ctx: AppContext, app: Express) { export default function (ctx: AppContext, app: Express) {
return app.get( return app.get(
'/redirect', '/redirect',
handler(async (req, res) => { observedHandler('redirect', async (req, res) => {
let link = req.query.u let link = req.query.u
assert( assert(
typeof link === 'string', typeof link === 'string',
+6 -4
View File
@@ -4,23 +4,25 @@ import {DAY, SECOND} from '@atproto/common'
import {Express} from 'express' import {Express} from 'express'
import {AppContext} from '../context.js' import {AppContext} from '../context.js'
import {handler} from './util.js' import {observedHandler} from './util.js'
export default function (ctx: AppContext, app: Express) { export default function (ctx: AppContext, app: Express) {
return app.get( return app.get(
'/:linkId', '/:linkId',
handler(async (req, res) => { observedHandler('short_link', async (req, res) => {
const linkId = req.params.linkId const linkId = req.params.linkId
const contentType = req.accepts(['html', 'json']) const contentType = req.accepts(['html', 'json'])
assert( assert(
typeof linkId === 'string', typeof linkId === 'string',
'express guarantees id parameter is a string', 'express guarantees id parameter is a string',
) )
const found = await ctx.db.db const found = await ctx.db.observeQuery('resolve_short_link', () =>
ctx.db.db
.selectFrom('link') .selectFrom('link')
.selectAll() .selectAll()
.where('id', '=', linkId) .where('id', '=', linkId)
.executeTakeFirst() .executeTakeFirst(),
)
if (!found) { if (!found) {
// potentially broken or mistyped link // potentially broken or mistyped link
res.setHeader('Cache-Control', 'no-store') res.setHeader('Cache-Control', 'no-store')
+30
View File
@@ -1,7 +1,11 @@
import {performance} from 'node:perf_hooks'
import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express' import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express'
import {httpLogger} from '../logger.js' import {httpLogger} from '../logger.js'
const SLOW_REQUEST_THRESHOLD_MS = 1000
export type Handler = (req: Request, res: Response) => Awaited<void> export type Handler = (req: Request, res: Response) => Awaited<void>
export const handler = (runHandler: Handler): RequestHandler => { export const handler = (runHandler: Handler): RequestHandler => {
@@ -14,6 +18,32 @@ export const handler = (runHandler: Handler): RequestHandler => {
} }
} }
export const observedHandler = (
operation: string,
runHandler: Handler,
): RequestHandler => {
return handler(async (req, res) => {
const startedAt = performance.now()
try {
await runHandler(req, res)
} finally {
const durationMs = Math.round(performance.now() - startedAt)
if (durationMs >= SLOW_REQUEST_THRESHOLD_MS) {
httpLogger.warn(
{
durationMs,
method: req.method,
operation,
requestTraceId: req.get('x-amzn-trace-id'),
statusCode: res.statusCode,
},
'slow request',
)
}
}
})
}
export const errorHandler: ErrorRequestHandler = (err, _req, res, next) => { export const errorHandler: ErrorRequestHandler = (err, _req, res, next) => {
httpLogger.error({err}, 'request error') httpLogger.error({err}, 'request error')
if (res.headersSent) { if (res.headersSent) {