typeorm-query-analyzer
Version:
A TypeORM interceptor that monitors query performance and sends webhook notifications when queries exceed defined thresholds
321 lines (258 loc) • 10.7 kB
Markdown
# TypeORM Query Analyzer
A TypeORM interceptor that monitors query performance and sends webhook notifications when queries exceed defined thresholds.
## Features
- 🚀 **Performance Monitoring**: Automatically tracks query execution times
- 📊 **Configurable Thresholds**: Set custom performance thresholds via environment variables
- 🔗 **Webhook Integration**: Send alerts to external monitoring systems
- 🔍 **Stack Trace Capture**: Optional stack trace collection for debugging
- 🌍 **Environment Aware**: Different settings for development and production
- 📝 **Comprehensive Logging**: Detailed query information in reports
- 📈 **Execution Plan Capture**: Automatic EXPLAIN query execution for SQL databases
## Installation
```bash
npm install typeorm-query-analyzer
```
## Quick Start
### 1. Environment Configuration
Create a `.env` file with the following variables:
```env
# Database
DB_CONNECTION=postgres://user:pass@localhost:5432/myapp
# Query Analyzer
QUERY_ANALYZER_THRESHOLD_MS=1000
QUERY_ANALYZER_API_ENDPOINT=https://monitoring.mycompany.com/api/slow-queries
QUERY_ANALYZER_API_KEY=sk-1234567890abcdef
QUERY_ANALYZER_CAPTURE_STACK=true
QUERY_ANALYZER_MAX_STACK=15
QUERY_ANALYZER_MAX_QUERY=5000
QUERY_ANALYZER_TIMEOUT_MS=10000
QUERY_ANALYZER_ENABLE_DEV=true
QUERY_ANALYZER_ENABLE_PROD=false
# Execution Plan Capture (Optional)
QUERY_ANALYZER_EXECUTION_PLAN_ENABLED=true
```
### 2. TypeORM Integration
#### Option A: Using the Helper Function (Recommended)
```typescript
import { DataSource } from "typeorm";
import { createDataSourceWithAnalyzer } from "typeorm-query-analyzer";
const dataSourceConfig = createDataSourceWithAnalyzer({
type: "postgres",
url: process.env.DB_CONNECTION,
entities: [
/* your entities */
],
synchronize: false,
});
const AppDataSource = new DataSource(dataSourceConfig);
// Automatically sets maxQueryExecutionTime and logger from QUERY_ANALYZER_THRESHOLD_MS
```
Or with extended types (e.g., with TypeORM Seeding):
```typescript
import { DataSource } from "typeorm";
import { SeederOptions } from "typeorm-extension";
import { createDataSourceWithAnalyzer } from "typeorm-query-analyzer";
const databaseConfig = createDataSourceWithAnalyzer<SeederOptions>({
type: "postgres",
url: process.env.DB_CONNECTION,
entities: [
/* your entities */
],
seeds: [
/* your seeds */
],
synchronize: false,
});
const AppDataSource = new DataSource(databaseConfig);
```
#### Option B: Manual Setup
```typescript
import { DataSource } from "typeorm";
import {
QueryAnalyzerLogger,
QueryAnalyzerConfig,
} from "typeorm-query-analyzer";
const config = new QueryAnalyzerConfig();
const AppDataSource = new DataSource({
type: "postgres",
url: process.env.DB_CONNECTION,
entities: [
/* your entities */
],
synchronize: false,
logging: true,
maxQueryExecutionTime: config.thresholdMs, // Uses QUERY_ANALYZER_THRESHOLD_MS
logger: new QueryAnalyzerLogger(config),
});
```
### 3. Custom Configuration
#### Using Partial Configuration Override
```typescript
import { DataSource } from "typeorm";
import { createDataSourceWithAnalyzer } from "typeorm-query-analyzer";
const databaseConfig = createDataSourceWithAnalyzer(
{
type: "postgres",
url: process.env.DB_CONNECTION,
entities: [
/* your entities */
],
synchronize: false,
},
{
// Override specific analyzer settings
thresholdMs: 500, // Custom threshold (overrides env var)
contextType: "my-custom-context",
enableDev: true,
captureStack: false,
}
);
const AppDataSource = new DataSource(databaseConfig);
```
#### Manual Configuration
```typescript
import {
DataSource,
QueryAnalyzerLogger,
QueryAnalyzerConfig,
} from "typeorm-query-analyzer";
const customConfig = new QueryAnalyzerConfig({
thresholdMs: 2000,
apiEndpoint: "https://custom-endpoint.com/webhooks",
apiKey: "custom-key",
enableDev: true,
captureStack: true,
contextType: "my-app-production",
});
const AppDataSource = new DataSource({
type: "postgres",
url: process.env.DB_CONNECTION,
entities: [
/* your entities */
],
maxQueryExecutionTime: customConfig.thresholdMs,
logger: new QueryAnalyzerLogger(customConfig),
});
```
## Configuration Options
### Environment Variables
| Variable | Default | Description |
| ------------------------------ | ------- | ----------------------------------------------- |
| `QUERY_ANALYZER_THRESHOLD_MS` | `1000` | Query execution threshold in milliseconds |
| `QUERY_ANALYZER_API_ENDPOINT` | - | **Required** Webhook endpoint URL |
| `QUERY_ANALYZER_API_KEY` | - | **Required** API key for webhook authentication |
| `QUERY_ANALYZER_CAPTURE_STACK` | `false` | Enable stack trace capture |
| `QUERY_ANALYZER_MAX_STACK` | `15` | Maximum stack trace depth |
| `QUERY_ANALYZER_MAX_QUERY` | `5000` | Maximum query length before truncation |
| `QUERY_ANALYZER_TIMEOUT_MS` | `10000` | Webhook request timeout |
| `QUERY_ANALYZER_ENABLE_DEV` | `false` | Enable in development environment |
| `QUERY_ANALYZER_ENABLE_PROD` | `false` | Enable in production environment |
| `QUERY_ANALYZER_EXECUTION_PLAN_ENABLED` | `false` | Enable execution plan capture for SQL databases |
| `NODE_ENV` or `APP_ENV` | - | Environment detection (development/production) |
### Automatic Configuration
| Property | Source | Description |
| ----------------- | ----------------------------------------------- | ------------------------------------ |
| `applicationName` | `package.json` name field | Auto-detected from consuming project |
| `version` | `package.json` version field | Auto-detected from consuming project |
| `contextType` | `${applicationName}-${databaseType}` | Generated context identifier |
| `logging` | `false` (analyzer config) / `true` (DataSource) | Separate logging controls |
### Configuration Priority
1. **Partial config parameter** (highest priority)
2. **Environment variables**
3. **Auto-detected values** (package.json)
4. **Default fallbacks** (lowest priority)
## Webhook Payload
When a slow query is detected, the following payload is sent to your webhook endpoint:
```typescript
{
queryId: string; // Unique query identifier (UUID)
rawQuery: string; // SQL query (truncated if too long)
parameters: Record<string, unknown>; // Sanitized query parameters
executionTimeMs: number; // Execution time in milliseconds
stackTrace: string[]; // Stack trace (if captureStack enabled)
timestamp: string; // ISO timestamp
contextType: string; // Format: "${applicationName}-${databaseType}"
environment: string; // NODE_ENV or APP_ENV value
applicationName?: string; // Auto-detected from package.json
version?: string; // Version from config or package.json
executionPlan: { // Query execution plan (if enabled)
databaseProvider: string; // Database type (mysql, postgres, etc.)
planFormat: {
contentType: string; // MIME type (application/json, application/xml, text/plain)
fileExtension: string; // File extension (.json, .xml, .txt)
description: string; // Format description (JSON, XML, TEXT)
};
content: string; // Raw execution plan data as string
};
}
```
### Example Payload
```json
{
"queryId": "123e4567-e89b-12d3-a456-426614174000",
"rawQuery": "SELECT * FROM users WHERE email = $1 AND status = $2",
"parameters": {
"param_0": "user@example.com",
"param_1": "active"
},
"executionTimeMs": 1500,
"stackTrace": [
"at UserRepository.findByEmail (/app/src/repositories/UserRepository.ts:45:12)",
"at UserService.authenticate (/app/src/services/UserService.ts:23:8)"
],
"timestamp": "2024-01-15T14:30:45.123Z",
"contextType": "my-api-postgres",
"environment": "production",
"applicationName": "my-api",
"version": "1.2.3",
"executionPlan": {
"databaseProvider": "postgres",
"planFormat": {
"contentType": "application/json",
"fileExtension": ".json",
"description": "JSON"
},
"content": "[\n {\n \"Plan\": {\n \"Node Type\": \"Seq Scan\",\n \"Relation Name\": \"users\",\n \"Startup Cost\": 0.00,\n \"Total Cost\": 25.50,\n \"Plan Rows\": 1,\n \"Plan Width\": 244\n }\n }\n]"
}
}
```
## Execution Plan Capture
### Supported Databases
The execution plan capture feature works with SQL databases and automatically adapts the format based on the database type:
| Database | EXPLAIN Command | Output Format | Content Type |
|----------|----------------|---------------|--------------|
| **MySQL** | `EXPLAIN FORMAT=JSON` | JSON | `application/json` |
| **MariaDB** | `EXPLAIN FORMAT=JSON` | JSON | `application/json` |
| **PostgreSQL** | `EXPLAIN (FORMAT JSON)` | JSON | `application/json` |
| **SQLite** | `EXPLAIN QUERY PLAN` | Text | `text/plain` |
| **SQL Server** | `SET SHOWPLAN_XML ON` | XML | `application/xml` |
| **Oracle** | `EXPLAIN PLAN FOR` | Text | `text/plain` |
### NoSQL Databases
Execution plan capture is automatically **disabled** for NoSQL databases (MongoDB, etc.) as they don't support SQL EXPLAIN queries.
### Configuration
Enable execution plan capture by setting the environment variable:
```env
QUERY_ANALYZER_EXECUTION_PLAN_ENABLED=true
```
When enabled:
- A separate database connection is created for EXPLAIN queries
- EXPLAIN queries are executed using the same parameters as the original query
- Execution plans are captured **before** sending webhook notifications
- If EXPLAIN fails, the webhook is still sent without the execution plan
- The feature gracefully falls back for unsupported database types
### Performance Considerations
- Execution plan capture uses a dedicated connection pool (minimal overhead)
- EXPLAIN queries typically execute quickly but add slight latency
- The feature only activates for slow queries that exceed the threshold
- Failed EXPLAIN queries don't prevent webhook delivery
## Development
```bash
# Install dependencies
npm install
# Build the project
npm run build
# Watch mode for development
npm run dev
```
## License
MIT - see [LICENSE](LICENSE) file for details.