The logger is automatically configured based on NODE_ENV:
# In .env file
LOG_LEVEL=info # fatal, error, warn, info, debug, trace
NODE_ENV=development # development (pretty-print) or production (JSON)const logger = require('./utils/logger');
// Simple message
logger.info('Server started');
// With context object
logger.warn({ userId: 'abc123', action: 'delete' }, 'User action attempted');
// Different log levels
logger.fatal({ error: err }, 'Critical error');
logger.error({ err }, 'Request failed');
logger.warn({ statusCode }, 'Rate limit approaching');
logger.info({ endpoint: '/api/users' }, 'Endpoint called');
logger.debug({ data: obj }, 'Debug information');Development (pretty-printed):
[11:45:23.123] INFO (pid=1234): User action attempted
userId: "abc123"
action: "delete"
Production (JSON):
{"level":30,"time":1719313523123,"pid":1234,"hostname":"server1","msg":"User action attempted","userId":"abc123","action":"delete"}fatal(60) - Server won't continueerror(50) - Error state, needs attentionwarn(40) - Warning, potential issueinfo(30) - General informationdebug(20) - Debug-level informationtrace(10) - Very verbose debug
The ETag middleware automatically generates cache identifiers for GET responses:
- First Request: Server calculates SHA256 hash of response body → sets
ETagheader - Subsequent Request: Client sends
If-None-Matchheader with cached ETag - Match: Server returns
304 Not Modified(no body, saves bandwidth) - No Match: Server returns
200with full response and new ETag
JavaScript (Fetch API):
// First request
const res1 = await fetch('/network-status');
const etag = res1.headers.get('etag');
const data = await res1.json();
// Subsequent request - conditional
const res2 = await fetch('/network-status', {
headers: { 'If-None-Match': etag }
});
if (res2.status === 304) {
// Use cached data, save bandwidth
console.log('Using cached data');
} else if (res2.status === 200) {
// Data changed, update cache
const newData = await res2.json();
const newETag = res2.headers.get('etag');
}cURL:
# First request
curl -i https://api.stellar.example.com/network-status
# Response includes: ETag: "a1b2c3d4e5f6..."
# Subsequent request with ETag
curl -i -H 'If-None-Match: "a1b2c3d4e5f6..."' \
https://api.stellar.example.com/network-status
# Response: 304 Not Modified (no body)The following endpoints support ETag caching:
/network-status/fee-estimate/account/*/asset/*/dex/*/liquidity-pools/*/claimable-balances/*/network/*
When rate limited (429):
{
"success": false,
"error": {
"type": "RateLimitExceeded",
"message": "Too many requests, please try again later.",
"retryAfter": 900,
"resetAt": "2024-06-29T12:30:00Z"
}
}All rate-limited responses include:
Retry-After: 900 # Seconds to retry
X-RateLimit-Limit: 100 # Max requests in window
X-RateLimit-Remaining: 0 # Requests remaining (0 when limited)
X-RateLimit-Reset: 2024-06-29T12:30:00Z # ISO timestamp when limit resets
# In .env file
RATE_LIMIT_MAX=100 # Global requests per 15 minutes per IPasync function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const res = await fetch(url, options);
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') || '60');
console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
if (res.status >= 200 && res.status < 300) {
return res;
}
throw new Error(`HTTP ${res.status}`);
}
throw new Error('Max retries exceeded');
}All timestamps in API responses are ISO 8601 strings:
"2024-06-29T12:30:45.123Z"
createdAt- When the resource was createdclosedAt- When a ledger was closedupdatedAt- When the resource was last updatedfirstSeenAt- When we first observed the resource
{
"data": {
"latestLedger": {
"sequence": 12345,
"closedAt": "2024-06-29T12:30:00Z"
},
"transactions": [
{
"hash": "abc123...",
"createdAt": "2024-06-29T12:15:30Z"
}
],
"lastSeenAt": "2024-06-29T12:25:00Z"
}
}- Implement ETag-based caching with
If-None-Matchheaders to reduce bandwidth - Parse
Retry-Afterheader in rate limit errors for proper backoff - Update parsing to handle ISO 8601 timestamp strings
- Log API responses using JSON parsing for better debugging
- Set
LOG_LEVELtodebugin development environments - Configure centralized log aggregation to parse Pino JSON output
- Monitor rate limiter response counts via
X-RateLimit-*headers - Set up alerts for high rate limiting (429 responses)
- Install dependencies:
npm install - Run tests:
npm test - Verify no console.* calls remain:
grep -r 'console\.' src/ - Update log parsing in monitoring systems
// OLD
console.log('User logged in:', userId);
console.error('Failed to load:', err.message);
// NEW
logger.info({ userId }, 'User logged in');
logger.error({ err }, 'Failed to load');- Clients should check
ETagheader on responses - Send
If-None-Matchon subsequent requests - Handle
304responses (no parsing needed, use cached data) - Implement retry logic using
Retry-Afterheader value
// Check LOG_LEVEL is not set too high
process.env.LOG_LEVEL = 'debug';
// Verify logger is imported correctly
const logger = require('./utils/logger');
logger.info('Test message'); // Should appearETags are generated from exact response body. If cache busting is needed:
// Force cache bypass
fetch('/network-status?bust=' + Date.now());
// Or use Cache-Control headers
// Server already sets: Cache-Control: public, max-age=3600# Increase limit (default: 100 per 15 minutes)
RATE_LIMIT_MAX=200# In centralized logging system
level: "error" AND type: "RateLimitExceeded"
# Via JSON logs
level: "warn" AND message: "slow endpoint"
# Count 304 responses
status: 304
For more information, see REFACTORING_SUMMARY.md for complete implementation details.