Skip to content

Commit 8001ea3

Browse files
committed
feat: Add structured logging with integration into database and storage modules, enhance database connection configuration, and create API utilities.
1 parent f4d6115 commit 8001ea3

7 files changed

Lines changed: 248 additions & 106 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Database
22
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
33

4+
# Database SSL Configuration
5+
# Set to 'true' to enable SSL for database connections (recommended for production)
6+
# Set to 'false' for local development
7+
DATABASE_SSL=false
8+
49
# Authentication
510
JWT_SECRET=your-secret-key-here
611

src/app/api/cloudlog/bands/route.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { NextRequest, NextResponse } from 'next/server';
55
import { verifyApiKey } from '@/lib/api-auth';
66
import { addCorsHeaders, createCorsPreflightResponse } from '@/lib/cors';
7+
import { addRateLimitHeaders } from '@/lib/api-utils';
78

89
// Standard amateur radio bands with frequencies
910
const AMATEUR_BANDS = [
@@ -46,7 +47,7 @@ export async function OPTIONS() {
4647

4748
export async function GET(request: NextRequest) {
4849
const authResult = await verifyApiKey(request);
49-
50+
5051
if (!authResult.success) {
5152
const response = NextResponse.json({
5253
success: false,
@@ -57,7 +58,7 @@ export async function GET(request: NextRequest) {
5758

5859
const auth = authResult.auth!;
5960
const url = new URL(request.url);
60-
61+
6162
try {
6263
const bandFilter = url.searchParams.get('band');
6364
const format = url.searchParams.get('format') || 'detailed';
@@ -66,13 +67,13 @@ export async function GET(request: NextRequest) {
6667

6768
// Filter by specific band if requested
6869
if (bandFilter) {
69-
bands = bands.filter(b =>
70+
bands = bands.filter(b =>
7071
b.band.toUpperCase() === bandFilter.toUpperCase()
7172
);
7273
}
7374

7475
let responseData;
75-
76+
7677
if (format === 'simple') {
7778
// Simple format - just band names
7879
responseData = bands.map(b => b.band);
@@ -83,8 +84,8 @@ export async function GET(request: NextRequest) {
8384
frequency_start_mhz: band.freq_start,
8485
frequency_end_mhz: band.freq_end,
8586
wavelength: band.wavelength,
86-
type: band.band.includes('M') ? 'HF/VHF/UHF' :
87-
band.band.includes('CM') ? 'Microwave' : 'Millimeter'
87+
type: band.band.includes('M') ? 'HF/VHF/UHF' :
88+
band.band.includes('CM') ? 'Microwave' : 'Millimeter'
8889
}));
8990
}
9091

@@ -96,9 +97,8 @@ export async function GET(request: NextRequest) {
9697
});
9798

9899
// Add rate limit headers
99-
response.headers.set('X-RateLimit-Limit', auth.rateLimitPerHour.toString());
100-
response.headers.set('X-RateLimit-Remaining', '999'); // TODO: Get actual remaining
101-
100+
addRateLimitHeaders(response, 999, auth.rateLimitPerHour);
101+
102102
return addCorsHeaders(response);
103103

104104
} catch (error) {

src/app/api/cloudlog/station/route.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import { NextRequest, NextResponse } from 'next/server';
55
import { verifyApiKey, canAccessStation } from '@/lib/api-auth';
66
import { query } from '@/lib/db';
7+
import { addRateLimitHeaders } from '@/lib/api-utils';
8+
import { logger } from '@/lib/logger';
79

810
export async function GET(request: NextRequest) {
911
const authResult = await verifyApiKey(request);
10-
12+
1113
if (!authResult.success) {
1214
return NextResponse.json({
1315
success: false,
@@ -17,7 +19,7 @@ export async function GET(request: NextRequest) {
1719

1820
const auth = authResult.auth!;
1921
const url = new URL(request.url);
20-
22+
2123
try {
2224
const stationIdParam = url.searchParams.get('station_id');
2325
let stationId = auth.stationId;
@@ -111,14 +113,13 @@ export async function GET(request: NextRequest) {
111113
count: stations.length
112114
});
113115

114-
// Add rate limit headers
115-
response.headers.set('X-RateLimit-Limit', auth.rateLimitPerHour.toString());
116-
response.headers.set('X-RateLimit-Remaining', '999'); // TODO: Get actual remaining
117-
116+
// Add rate limit headers
117+
addRateLimitHeaders(response, 999, auth.rateLimitPerHour);
118+
118119
return response;
119120

120121
} catch (error) {
121-
console.error('Station retrieval error:', error);
122+
logger.error('Station retrieval error', error);
122123
return NextResponse.json({
123124
success: false,
124125
error: 'Internal server error'

src/lib/api-utils.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// API utility functions for Nextlog
2+
// Shared utilities for API routes
3+
4+
import { NextResponse } from 'next/server';
5+
6+
/**
7+
* Add rate limiting headers to API responses
8+
* @param response - NextResponse to add headers to
9+
* @param remaining - Remaining requests in current window
10+
* @param limit - Total rate limit per hour
11+
* @param resetTime - Optional reset timestamp (defaults to 1 hour from now)
12+
* @returns Response with rate limit headers added
13+
*/
14+
export function addRateLimitHeaders(
15+
response: NextResponse,
16+
remaining: number,
17+
limit: number,
18+
resetTime?: number
19+
): NextResponse {
20+
const reset = resetTime || Math.floor(Date.now() / 1000 + 3600);
21+
22+
response.headers.set('X-RateLimit-Limit', limit.toString());
23+
response.headers.set('X-RateLimit-Remaining', remaining.toString());
24+
response.headers.set('X-RateLimit-Reset', reset.toString());
25+
26+
return response;
27+
}
28+
29+
/**
30+
* Create a JSON response with rate limit headers
31+
* @param data - Response data
32+
* @param status - HTTP status code
33+
* @param remaining - Remaining requests
34+
* @param limit - Rate limit per hour
35+
* @returns NextResponse with JSON data and rate limit headers
36+
*/
37+
export function createRateLimitedResponse(
38+
data: unknown,
39+
status: number,
40+
remaining: number,
41+
limit: number
42+
): NextResponse {
43+
const response = NextResponse.json(data, { status });
44+
return addRateLimitHeaders(response, remaining, limit);
45+
}
46+
47+
/**
48+
* Create a successful JSON response with rate limit headers
49+
* @param data - Response data
50+
* @param remaining - Remaining requests
51+
* @param limit - Rate limit per hour
52+
* @returns NextResponse with JSON data and rate limit headers
53+
*/
54+
export function successWithRateLimit(
55+
data: unknown,
56+
remaining: number,
57+
limit: number
58+
): NextResponse {
59+
return createRateLimitedResponse(data, 200, remaining, limit);
60+
}
61+
62+
/**
63+
* Create an error response
64+
* @param message - Error message
65+
* @param status - HTTP status code
66+
* @returns NextResponse with error
67+
*/
68+
export function errorResponse(
69+
message: string,
70+
status: number = 500
71+
): NextResponse {
72+
return NextResponse.json({ error: message }, { status });
73+
}

src/lib/db.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// Database connection utility for Nextlog PostgreSQL
2-
31
import { Pool, PoolClient, QueryResult } from 'pg';
2+
import { logger } from './logger';
43

54
// Global connection pool
65
let pool: Pool;
@@ -10,47 +9,51 @@ let pool: Pool;
109
*/
1110
function getPool(): Pool {
1211
if (!pool) {
12+
const sslConfig = process.env.DATABASE_SSL === 'true'
13+
? { rejectUnauthorized: false }
14+
: false;
15+
1316
pool = new Pool({
1417
connectionString: process.env.DATABASE_URL,
15-
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
18+
ssl: sslConfig,
1619
max: 20, // Maximum number of clients in the pool
17-
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
18-
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
20+
idleTimeoutMillis: 60000, // Close idle clients after 60 seconds
21+
connectionTimeoutMillis: 10000, // Return an error after 10 seconds if connection could not be established
22+
query_timeout: 30000, // Query timeout: 30 seconds
23+
statement_timeout: 30000, // Statement timeout: 30 seconds
1924
});
2025

2126
// Handle pool errors
2227
pool.on('error', (err) => {
23-
console.error('Unexpected error on idle client', err);
28+
logger.error('Unexpected error on idle client', err);
2429
});
30+
31+
logger.info('Database connection pool initialized');
2532
}
2633

2734
return pool;
2835
}
2936

37+
3038
/**
3139
* Execute a query using the connection pool
3240
*/
3341
export async function query(text: string, params?: unknown[]): Promise<QueryResult> {
3442
const pool = getPool();
3543
const start = Date.now();
36-
44+
3745
try {
3846
const result = await pool.query(text, params);
3947
const duration = Date.now() - start;
40-
48+
4149
// Log slow queries (> 100ms)
4250
if (duration > 100) {
43-
console.warn('Slow query detected:', {
44-
duration: `${duration}ms`,
45-
text: text.substring(0, 100) + (text.length > 100 ? '...' : ''),
46-
params: params?.length ? `${params.length} params` : 'no params'
47-
});
51+
logger.slowQuery(duration, text, params);
4852
}
49-
53+
5054
return result;
5155
} catch (error) {
52-
console.error('Database query error:', {
53-
error: error instanceof Error ? error.message : 'Unknown error',
56+
logger.error('Database query error', error, {
5457
query: text.substring(0, 100) + (text.length > 100 ? '...' : ''),
5558
params: params?.length ? `${params.length} params` : 'no params'
5659
});
@@ -73,7 +76,7 @@ export async function transaction<T>(
7376
callback: (client: PoolClient) => Promise<T>
7477
): Promise<T> {
7578
const client = await getClient();
76-
79+
7780
try {
7881
await client.query('BEGIN');
7982
const result = await callback(client);
@@ -98,13 +101,13 @@ export async function closePool(): Promise<void> {
98101

99102
// Handle process termination
100103
process.on('SIGINT', async () => {
101-
console.log('Closing database pool...');
104+
logger.info('Shutting down: Closing database pool');
102105
await closePool();
103106
process.exit(0);
104107
});
105108

106109
process.on('SIGTERM', async () => {
107-
console.log('Closing database pool...');
110+
logger.info('Shutting down: Closing database pool');
108111
await closePool();
109112
process.exit(0);
110113
});

src/lib/logger.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Logging utility for Nextlog
2+
// Provides environment-aware, structured logging with different log levels
3+
4+
interface LogContext {
5+
[key: string]: unknown;
6+
}
7+
8+
/**
9+
* Structured logger with environment-aware log levels
10+
*/
11+
export const logger = {
12+
/**
13+
* Debug logging - only shown in development environment
14+
* Use for detailed debugging information
15+
*/
16+
debug: (message: string, context?: LogContext) => {
17+
if (process.env.NODE_ENV === 'development') {
18+
console.log(`[DEBUG] ${message}`, context || '');
19+
}
20+
},
21+
22+
/**
23+
* Info logging - shown in all environments
24+
* Use for general informational messages
25+
*/
26+
info: (message: string, context?: LogContext) => {
27+
console.log(`[INFO] ${message}`, context || '');
28+
},
29+
30+
/**
31+
* Warning logging - shown in all environments
32+
* Use for Warning conditions that don't prevent operation
33+
*/
34+
warn: (message: string, context?: LogContext) => {
35+
console.warn(`[WARN] ${message}`, context || '');
36+
},
37+
38+
/**
39+
* Error logging - shown in all environments
40+
* Use for error conditions that need attention
41+
*/
42+
error: (message: string, error?: unknown, context?: LogContext) => {
43+
const errorDetails = error instanceof Error ? {
44+
message: error.message,
45+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
46+
...context
47+
} : { error, ...context };
48+
49+
console.error(`[ERROR] ${message}`, errorDetails);
50+
},
51+
52+
/**
53+
* Log slow database queries (development only)
54+
*/
55+
slowQuery: (duration: number, query: string, params?: unknown) => {
56+
if (process.env.NODE_ENV === 'development') {
57+
console.warn(`[SLOW QUERY] ${duration}ms`, {
58+
query: query.substring(0, 100) + (query.length > 100 ? '...' : ''),
59+
params: params ? `${Array.isArray(params) ? params.length : 'object'} params` : 'no params'
60+
});
61+
}
62+
}
63+
};
64+
65+
/**
66+
* Legacy console.log replacement - use logger.debug instead
67+
* @deprecated Use logger.debug() instead
68+
*/
69+
export const log = logger.debug;

0 commit comments

Comments
 (0)