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 '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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user