feat(logging-p0): add structured logging to database driver services

Add createLogger/trackDuration logging to mysql.ts, sql-server.ts, and
data-source.ts — the only database layer files without observability.
Connect/disconnect, query execution (with duration tracking), and
transaction lifecycle events are now logged. Passwords and parameter
values are excluded; SQL statements are capped at 100 chars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-04-04 11:43:02 +08:00
parent 42b76c4de5
commit 018d524fe8
3 changed files with 167 additions and 61 deletions

View File

@@ -5,6 +5,9 @@ import type {
QueryResult,
MySqlConfig
} from '../../types/database.types'
import { createLogger, trackDuration } from '../logger'
const log = createLogger('MySqlService')
export type { MySqlConfig } from '../../types/database.types'
@@ -24,6 +27,7 @@ export class MySqlService implements IDatabaseService {
*/
async connect(): Promise<void> {
if (this.connection) {
log.warn('Already connected to MySQL')
throw new Error('Already connected to MySQL')
}
@@ -38,7 +42,18 @@ export class MySqlService implements IDatabaseService {
// Test connection
await this.connection.ping()
log.info('Connected to MySQL', {
host: this.config.host,
port: this.config.port,
database: this.config.database
})
} catch (error) {
log.error('Failed to connect to MySQL', {
host: this.config.host,
port: this.config.port,
database: this.config.database,
error
})
throw new Error(`Failed to connect to MySQL: ${(error as Error).message}`)
}
}
@@ -54,7 +69,9 @@ export class MySqlService implements IDatabaseService {
try {
await this.connection.end()
this.connection = null
log.info('Disconnected from MySQL')
} catch (error) {
log.error('Failed to disconnect from MySQL', { error })
throw new Error(`Failed to disconnect from MySQL: ${(error as Error).message}`)
}
}
@@ -74,32 +91,40 @@ export class MySqlService implements IDatabaseService {
throw new Error('Not connected to MySQL. Call connect() first.')
}
const sqlPreview = sql.substring(0, 100)
const paramCount = params?.length ?? 0
try {
const [result, fields] = await this.connection.execute(sql, params)
const { result: queryResult } = await trackDuration(
async () => {
const [result, fields] = await this.connection!.execute(sql, params)
// Convert to plain objects and extract column names
const columns = Array.isArray(fields) ? fields.map((field) => field.name) : []
// Convert to plain objects and extract column names
const columns = Array.isArray(fields) ? fields.map((field) => field.name) : []
// Handle different result types
let rows: Record<string, unknown>[] = []
let rowCount = 0
// Handle different result types
let rows: Record<string, unknown>[] = []
let rowCount = 0
if (Array.isArray(result)) {
// SELECT query - result is an array of rows
rows = result as Record<string, unknown>[]
rowCount = rows.length
} else if (typeof result === 'object' && result !== null) {
// INSERT/UPDATE/DELETE query - result is OkPacket
const okPacket = result as any
rowCount = okPacket.affectedRows || okPacket.changedRows || 0
}
if (Array.isArray(result)) {
// SELECT query - result is an array of rows
rows = result as Record<string, unknown>[]
rowCount = rows.length
} else if (typeof result === 'object' && result !== null) {
// INSERT/UPDATE/DELETE query - result is OkPacket
const okPacket = result as any
rowCount = okPacket.affectedRows || okPacket.changedRows || 0
}
return {
rows,
columns,
rowCount
}
return { rows, columns, rowCount }
},
{ operationName: 'MySqlService.query' }
)
log.debug('Query executed', { sqlPreview, rowCount: queryResult.rowCount, paramCount })
return queryResult
} catch (error) {
log.error('MySQL query failed', { sqlPreview, paramCount, error })
throw new Error(`MySQL query failed: ${(error as Error).message}`)
}
}
@@ -112,17 +137,24 @@ export class MySqlService implements IDatabaseService {
throw new Error('Not connected to MySQL. Call connect() first.')
}
const queryCount = queries.length
log.info('Transaction started', { queryCount })
try {
await this.connection.beginTransaction()
for (const { sql, params } of queries) {
for (let i = 0; i < queries.length; i++) {
const { sql, params } = queries[i]
await this.connection.execute(sql, params)
log.debug('Transaction query executed', { index: i, sqlPreview: sql.substring(0, 100) })
}
await this.connection.commit()
log.info('Transaction committed', { queryCount })
} catch (error) {
if (this.connection) {
await this.connection.rollback()
log.warn('Transaction rolled back', { queryCount, error })
}
throw new Error(`MySQL transaction failed: ${(error as Error).message}`)
}