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

@@ -11,6 +11,9 @@
import 'reflect-metadata' import 'reflect-metadata'
import { DataSource, DataSourceOptions } from 'typeorm' import { DataSource, DataSourceOptions } from 'typeorm'
import { ConfigManager } from '../config/config-manager' import { ConfigManager } from '../config/config-manager'
import { createLogger } from '../logger'
const log = createLogger('DataSource')
/** /**
* Get database type from config manager * Get database type from config manager
@@ -26,6 +29,7 @@ function getDatabaseType(): 'mysql' | 'mssql' {
*/ */
function buildDataSourceOptions(): DataSourceOptions { function buildDataSourceOptions(): DataSourceOptions {
const type = getDatabaseType() const type = getDatabaseType()
log.debug('Building DataSource options', { type })
const configManager = ConfigManager.getInstance() const configManager = ConfigManager.getInstance()
const config = configManager.getConfig() const config = configManager.getConfig()
@@ -74,7 +78,11 @@ let dataSource: DataSource | null = null
*/ */
export function getDataSource(): DataSource { export function getDataSource(): DataSource {
if (!dataSource) { if (!dataSource) {
const type = getDatabaseType()
log.info('Creating new TypeORM DataSource', { type })
dataSource = new DataSource(buildDataSourceOptions()) dataSource = new DataSource(buildDataSourceOptions())
} else {
log.debug('Reusing existing DataSource')
} }
return dataSource return dataSource
} }
@@ -85,7 +93,14 @@ export function getDataSource(): DataSource {
export async function initializeDataSource(): Promise<DataSource> { export async function initializeDataSource(): Promise<DataSource> {
const ds = getDataSource() const ds = getDataSource()
if (!ds.isInitialized) { if (!ds.isInitialized) {
try {
await ds.initialize() await ds.initialize()
const type = getDatabaseType()
log.info('TypeORM DataSource initialized', { type })
} catch (error) {
log.error('Failed to initialize DataSource', { error })
throw error
}
} }
return ds return ds
} }
@@ -95,8 +110,13 @@ export async function initializeDataSource(): Promise<DataSource> {
*/ */
export async function destroyDataSource(): Promise<void> { export async function destroyDataSource(): Promise<void> {
if (dataSource && dataSource.isInitialized) { if (dataSource && dataSource.isInitialized) {
try {
await dataSource.destroy() await dataSource.destroy()
dataSource = null dataSource = null
log.info('TypeORM DataSource destroyed')
} catch (error) {
log.error('Failed to destroy DataSource', { error })
}
} }
} }

View File

@@ -5,6 +5,9 @@ import type {
QueryResult, QueryResult,
MySqlConfig MySqlConfig
} from '../../types/database.types' } from '../../types/database.types'
import { createLogger, trackDuration } from '../logger'
const log = createLogger('MySqlService')
export type { MySqlConfig } from '../../types/database.types' export type { MySqlConfig } from '../../types/database.types'
@@ -24,6 +27,7 @@ export class MySqlService implements IDatabaseService {
*/ */
async connect(): Promise<void> { async connect(): Promise<void> {
if (this.connection) { if (this.connection) {
log.warn('Already connected to MySQL')
throw new Error('Already connected to MySQL') throw new Error('Already connected to MySQL')
} }
@@ -38,7 +42,18 @@ export class MySqlService implements IDatabaseService {
// Test connection // Test connection
await this.connection.ping() await this.connection.ping()
log.info('Connected to MySQL', {
host: this.config.host,
port: this.config.port,
database: this.config.database
})
} catch (error) { } 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}`) throw new Error(`Failed to connect to MySQL: ${(error as Error).message}`)
} }
} }
@@ -54,7 +69,9 @@ export class MySqlService implements IDatabaseService {
try { try {
await this.connection.end() await this.connection.end()
this.connection = null this.connection = null
log.info('Disconnected from MySQL')
} catch (error) { } catch (error) {
log.error('Failed to disconnect from MySQL', { error })
throw new Error(`Failed to disconnect from MySQL: ${(error as Error).message}`) throw new Error(`Failed to disconnect from MySQL: ${(error as Error).message}`)
} }
} }
@@ -74,8 +91,13 @@ export class MySqlService implements IDatabaseService {
throw new Error('Not connected to MySQL. Call connect() first.') throw new Error('Not connected to MySQL. Call connect() first.')
} }
const sqlPreview = sql.substring(0, 100)
const paramCount = params?.length ?? 0
try { 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 // Convert to plain objects and extract column names
const columns = Array.isArray(fields) ? fields.map((field) => field.name) : [] const columns = Array.isArray(fields) ? fields.map((field) => field.name) : []
@@ -94,12 +116,15 @@ export class MySqlService implements IDatabaseService {
rowCount = okPacket.affectedRows || okPacket.changedRows || 0 rowCount = okPacket.affectedRows || okPacket.changedRows || 0
} }
return { return { rows, columns, rowCount }
rows, },
columns, { operationName: 'MySqlService.query' }
rowCount )
}
log.debug('Query executed', { sqlPreview, rowCount: queryResult.rowCount, paramCount })
return queryResult
} catch (error) { } catch (error) {
log.error('MySQL query failed', { sqlPreview, paramCount, error })
throw new Error(`MySQL query failed: ${(error as Error).message}`) 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.') throw new Error('Not connected to MySQL. Call connect() first.')
} }
const queryCount = queries.length
log.info('Transaction started', { queryCount })
try { try {
await this.connection.beginTransaction() 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) await this.connection.execute(sql, params)
log.debug('Transaction query executed', { index: i, sqlPreview: sql.substring(0, 100) })
} }
await this.connection.commit() await this.connection.commit()
log.info('Transaction committed', { queryCount })
} catch (error) { } catch (error) {
if (this.connection) { if (this.connection) {
await this.connection.rollback() await this.connection.rollback()
log.warn('Transaction rolled back', { queryCount, error })
} }
throw new Error(`MySQL transaction failed: ${(error as Error).message}`) throw new Error(`MySQL transaction failed: ${(error as Error).message}`)
} }

View File

@@ -5,6 +5,9 @@ import type {
QueryResult, QueryResult,
SqlServerConfig SqlServerConfig
} from '../../types/database.types' } from '../../types/database.types'
import { createLogger, trackDuration } from '../logger'
const log = createLogger('SqlServerService')
export type { SqlServerConfig } from '../../types/database.types' export type { SqlServerConfig } from '../../types/database.types'
@@ -24,6 +27,7 @@ export class SqlServerService implements IDatabaseService {
*/ */
async connect(): Promise<void> { async connect(): Promise<void> {
if (this.pool) { if (this.pool) {
log.warn('Already connected to SQL Server')
throw new Error('Already connected to SQL Server') throw new Error('Already connected to SQL Server')
} }
@@ -42,7 +46,18 @@ export class SqlServerService implements IDatabaseService {
this.pool = new sql.ConnectionPool(poolConfig) this.pool = new sql.ConnectionPool(poolConfig)
await this.pool.connect() await this.pool.connect()
log.info('Connected to SQL Server', {
server: this.config.server,
port: this.config.port,
database: this.config.database
})
} catch (error) { } catch (error) {
log.error('Failed to connect to SQL Server', {
server: this.config.server,
port: this.config.port,
database: this.config.database,
error
})
throw new Error(`Failed to connect to SQL Server: ${(error as Error).message}`) throw new Error(`Failed to connect to SQL Server: ${(error as Error).message}`)
} }
} }
@@ -58,7 +73,9 @@ export class SqlServerService implements IDatabaseService {
try { try {
await this.pool.close() await this.pool.close()
this.pool = null this.pool = null
log.info('Disconnected from SQL Server')
} catch (error) { } catch (error) {
log.error('Failed to disconnect from SQL Server', { error })
throw new Error(`Failed to disconnect from SQL Server: ${(error as Error).message}`) throw new Error(`Failed to disconnect from SQL Server: ${(error as Error).message}`)
} }
} }
@@ -80,8 +97,13 @@ export class SqlServerService implements IDatabaseService {
throw new Error('Not connected to SQL Server. Call connect() first.') throw new Error('Not connected to SQL Server. Call connect() first.')
} }
const sqlPreview = sqlString.substring(0, 100)
const paramCount = params?.length ?? 0
try { try {
const request = this.pool.request() const { result: queryResult } = await trackDuration(
async () => {
const request = this.pool!.request()
// Add parameters if provided - convert array to @p0, @p1, ... format // Add parameters if provided - convert array to @p0, @p1, ... format
if (params && params.length > 0) { if (params && params.length > 0) {
@@ -102,7 +124,14 @@ export class SqlServerService implements IDatabaseService {
columns, columns,
rowCount: result.rowsAffected?.[0] || rows.length rowCount: result.rowsAffected?.[0] || rows.length
} }
},
{ operationName: 'SqlServerService.query' }
)
log.debug('Query executed', { sqlPreview, rowCount: queryResult.rowCount, paramCount })
return queryResult
} catch (error) { } catch (error) {
log.error('SQL Server query failed', { sqlPreview, paramCount, error })
throw new Error(`SQL Server query failed: ${(error as Error).message}`) throw new Error(`SQL Server query failed: ${(error as Error).message}`)
} }
} }
@@ -126,8 +155,13 @@ export class SqlServerService implements IDatabaseService {
throw new Error('Not connected to SQL Server. Call connect() first.') throw new Error('Not connected to SQL Server. Call connect() first.')
} }
const sqlPreview = sqlString.substring(0, 100)
const paramNames = Object.keys(params)
try { try {
const request = this.pool.request() const { result: queryResult } = await trackDuration(
async () => {
const request = this.pool!.request()
// Add parameters with explicit types // Add parameters with explicit types
for (const [key, { value, type }] of Object.entries(params)) { for (const [key, { value, type }] of Object.entries(params)) {
@@ -150,7 +184,18 @@ export class SqlServerService implements IDatabaseService {
columns, columns,
rowCount: result.rowsAffected?.[0] || rows.length rowCount: result.rowsAffected?.[0] || rows.length
} }
},
{ operationName: 'SqlServerService.queryWithParams' }
)
log.debug('Query with params executed', {
sqlPreview,
rowCount: queryResult.rowCount,
paramNames
})
return queryResult
} catch (error) { } catch (error) {
log.error('SQL Server query with params failed', { sqlPreview, paramNames, error })
throw new Error(`SQL Server query failed: ${(error as Error).message}`) throw new Error(`SQL Server query failed: ${(error as Error).message}`)
} }
} }
@@ -164,12 +209,15 @@ export class SqlServerService implements IDatabaseService {
throw new Error('Not connected to SQL Server. Call connect() first.') throw new Error('Not connected to SQL Server. Call connect() first.')
} }
const queryCount = queries.length
const transaction = new sql.Transaction(this.pool) const transaction = new sql.Transaction(this.pool)
log.info('Transaction started', { queryCount })
try { try {
await transaction.begin() await transaction.begin()
for (const { sql: sqlString, params } of queries) { for (let i = 0; i < queries.length; i++) {
const { sql: sqlString, params } = queries[i]
const request = new sql.Request(transaction) const request = new sql.Request(transaction)
// Add parameters if provided - convert array to @p0, @p1, ... format // Add parameters if provided - convert array to @p0, @p1, ... format
@@ -180,11 +228,17 @@ export class SqlServerService implements IDatabaseService {
} }
await request.query(sqlString) await request.query(sqlString)
log.debug('Transaction query executed', {
index: i,
sqlPreview: sqlString.substring(0, 100)
})
} }
await transaction.commit() await transaction.commit()
log.info('Transaction committed', { queryCount })
} catch (error) { } catch (error) {
await transaction.rollback() await transaction.rollback()
log.warn('Transaction rolled back', { queryCount, error })
throw new Error(`SQL Server transaction failed: ${(error as Error).message}`) throw new Error(`SQL Server transaction failed: ${(error as Error).message}`)
} }
} }