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:
@@ -11,6 +11,9 @@
|
||||
import 'reflect-metadata'
|
||||
import { DataSource, DataSourceOptions } from 'typeorm'
|
||||
import { ConfigManager } from '../config/config-manager'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('DataSource')
|
||||
|
||||
/**
|
||||
* Get database type from config manager
|
||||
@@ -26,6 +29,7 @@ function getDatabaseType(): 'mysql' | 'mssql' {
|
||||
*/
|
||||
function buildDataSourceOptions(): DataSourceOptions {
|
||||
const type = getDatabaseType()
|
||||
log.debug('Building DataSource options', { type })
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const config = configManager.getConfig()
|
||||
|
||||
@@ -74,7 +78,11 @@ let dataSource: DataSource | null = null
|
||||
*/
|
||||
export function getDataSource(): DataSource {
|
||||
if (!dataSource) {
|
||||
const type = getDatabaseType()
|
||||
log.info('Creating new TypeORM DataSource', { type })
|
||||
dataSource = new DataSource(buildDataSourceOptions())
|
||||
} else {
|
||||
log.debug('Reusing existing DataSource')
|
||||
}
|
||||
return dataSource
|
||||
}
|
||||
@@ -85,7 +93,14 @@ export function getDataSource(): DataSource {
|
||||
export async function initializeDataSource(): Promise<DataSource> {
|
||||
const ds = getDataSource()
|
||||
if (!ds.isInitialized) {
|
||||
await ds.initialize()
|
||||
try {
|
||||
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
|
||||
}
|
||||
@@ -95,8 +110,13 @@ export async function initializeDataSource(): Promise<DataSource> {
|
||||
*/
|
||||
export async function destroyDataSource(): Promise<void> {
|
||||
if (dataSource && dataSource.isInitialized) {
|
||||
await dataSource.destroy()
|
||||
dataSource = null
|
||||
try {
|
||||
await dataSource.destroy()
|
||||
dataSource = null
|
||||
log.info('TypeORM DataSource destroyed')
|
||||
} catch (error) {
|
||||
log.error('Failed to destroy DataSource', { error })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import type {
|
||||
QueryResult,
|
||||
SqlServerConfig
|
||||
} from '../../types/database.types'
|
||||
import { createLogger, trackDuration } from '../logger'
|
||||
|
||||
const log = createLogger('SqlServerService')
|
||||
|
||||
export type { SqlServerConfig } from '../../types/database.types'
|
||||
|
||||
@@ -24,6 +27,7 @@ export class SqlServerService implements IDatabaseService {
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.pool) {
|
||||
log.warn('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)
|
||||
await this.pool.connect()
|
||||
log.info('Connected to SQL Server', {
|
||||
server: this.config.server,
|
||||
port: this.config.port,
|
||||
database: this.config.database
|
||||
})
|
||||
} 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}`)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +73,9 @@ export class SqlServerService implements IDatabaseService {
|
||||
try {
|
||||
await this.pool.close()
|
||||
this.pool = null
|
||||
log.info('Disconnected from SQL Server')
|
||||
} catch (error) {
|
||||
log.error('Failed to disconnect from SQL Server', { error })
|
||||
throw new Error(`Failed to disconnect from SQL Server: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
@@ -80,29 +97,41 @@ export class SqlServerService implements IDatabaseService {
|
||||
throw new Error('Not connected to SQL Server. Call connect() first.')
|
||||
}
|
||||
|
||||
const sqlPreview = sqlString.substring(0, 100)
|
||||
const paramCount = params?.length ?? 0
|
||||
|
||||
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
|
||||
if (params && params.length > 0) {
|
||||
params.forEach((value, index) => {
|
||||
request.input(`p${index}`, value)
|
||||
})
|
||||
}
|
||||
// Add parameters if provided - convert array to @p0, @p1, ... format
|
||||
if (params && params.length > 0) {
|
||||
params.forEach((value, index) => {
|
||||
request.input(`p${index}`, value)
|
||||
})
|
||||
}
|
||||
|
||||
const result = await request.query(sqlString)
|
||||
const result = await request.query(sqlString)
|
||||
|
||||
// Convert recordset to array of objects (may be undefined for DELETE/INSERT/UPDATE)
|
||||
const rows = (result.recordset as Record<string, unknown>[]) || []
|
||||
// Extract column names from the first row if available
|
||||
const columns = rows.length > 0 ? Object.keys(rows[0]) : []
|
||||
// Convert recordset to array of objects (may be undefined for DELETE/INSERT/UPDATE)
|
||||
const rows = (result.recordset as Record<string, unknown>[]) || []
|
||||
// Extract column names from the first row if available
|
||||
const columns = rows.length > 0 ? Object.keys(rows[0]) : []
|
||||
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
rowCount: result.rowsAffected?.[0] || rows.length
|
||||
}
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
rowCount: result.rowsAffected?.[0] || rows.length
|
||||
}
|
||||
},
|
||||
{ operationName: 'SqlServerService.query' }
|
||||
)
|
||||
|
||||
log.debug('Query executed', { sqlPreview, rowCount: queryResult.rowCount, paramCount })
|
||||
return queryResult
|
||||
} catch (error) {
|
||||
log.error('SQL Server query failed', { sqlPreview, paramCount, error })
|
||||
throw new Error(`SQL Server query failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
@@ -126,31 +155,47 @@ export class SqlServerService implements IDatabaseService {
|
||||
throw new Error('Not connected to SQL Server. Call connect() first.')
|
||||
}
|
||||
|
||||
const sqlPreview = sqlString.substring(0, 100)
|
||||
const paramNames = Object.keys(params)
|
||||
|
||||
try {
|
||||
const request = this.pool.request()
|
||||
const { result: queryResult } = await trackDuration(
|
||||
async () => {
|
||||
const request = this.pool!.request()
|
||||
|
||||
// Add parameters with explicit types
|
||||
for (const [key, { value, type }] of Object.entries(params)) {
|
||||
if (type) {
|
||||
request.input(key, type, value)
|
||||
} else {
|
||||
request.input(key, value)
|
||||
}
|
||||
}
|
||||
// Add parameters with explicit types
|
||||
for (const [key, { value, type }] of Object.entries(params)) {
|
||||
if (type) {
|
||||
request.input(key, type, value)
|
||||
} else {
|
||||
request.input(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await request.query(sqlString)
|
||||
const result = await request.query(sqlString)
|
||||
|
||||
// Convert recordset to array of objects
|
||||
const rows = result.recordset as Record<string, unknown>[]
|
||||
// Extract column names from the first row if available
|
||||
const columns = rows.length > 0 ? Object.keys(rows[0]) : []
|
||||
// Convert recordset to array of objects
|
||||
const rows = result.recordset as Record<string, unknown>[]
|
||||
// Extract column names from the first row if available
|
||||
const columns = rows.length > 0 ? Object.keys(rows[0]) : []
|
||||
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
rowCount: result.rowsAffected?.[0] || rows.length
|
||||
}
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
rowCount: result.rowsAffected?.[0] || rows.length
|
||||
}
|
||||
},
|
||||
{ operationName: 'SqlServerService.queryWithParams' }
|
||||
)
|
||||
|
||||
log.debug('Query with params executed', {
|
||||
sqlPreview,
|
||||
rowCount: queryResult.rowCount,
|
||||
paramNames
|
||||
})
|
||||
return queryResult
|
||||
} catch (error) {
|
||||
log.error('SQL Server query with params failed', { sqlPreview, paramNames, error })
|
||||
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.')
|
||||
}
|
||||
|
||||
const queryCount = queries.length
|
||||
const transaction = new sql.Transaction(this.pool)
|
||||
log.info('Transaction started', { queryCount })
|
||||
|
||||
try {
|
||||
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)
|
||||
|
||||
// Add parameters if provided - convert array to @p0, @p1, ... format
|
||||
@@ -180,11 +228,17 @@ export class SqlServerService implements IDatabaseService {
|
||||
}
|
||||
|
||||
await request.query(sqlString)
|
||||
log.debug('Transaction query executed', {
|
||||
index: i,
|
||||
sqlPreview: sqlString.substring(0, 100)
|
||||
})
|
||||
}
|
||||
|
||||
await transaction.commit()
|
||||
log.info('Transaction committed', { queryCount })
|
||||
} catch (error) {
|
||||
await transaction.rollback()
|
||||
log.warn('Transaction rolled back', { queryCount, error })
|
||||
throw new Error(`SQL Server transaction failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user