diff --git a/.env.example b/.env.example index 52264ab1..c70c1eb9 100644 --- a/.env.example +++ b/.env.example @@ -38,4 +38,7 @@ NEXTAUTH_SECRET="your-super-secret-key-change-in-production" # OPENAI_EMBEDDING_MODEL=text-embedding-3-small # Optional: embedding model for AI search # OPENAI_GENERATIVE_MODEL=gpt-4o-mini # Optional: generative model for AI generation -# GOOGLE_ANALYTICS_ID="G-XXXXXXXXX" \ No newline at end of file +# GOOGLE_ANALYTICS_ID="G-XXXXXXXXX" + +# Logging (optional) +# LOG_LEVEL="info" # Options: trace, debug, info, warn, error, fatal \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 98563d51..9ad63776 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "next-intl": "^4.5.8", "next-themes": "^0.4.6", "openai": "^6.10.0", + "pino": "^10.1.0", "react": "19.2.0", "react-dom": "19.2.0", "react-hook-form": "^7.68.0", @@ -3162,6 +3163,12 @@ "node": ">=0.10" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@prisma/client": { "version": "6.19.1", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.1.tgz", @@ -6702,6 +6709,15 @@ "node": ">= 0.4" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -11775,6 +11791,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -12000,6 +12025,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -12121,6 +12183,22 @@ } } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -12219,6 +12297,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12408,6 +12492,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12641,6 +12734,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12916,6 +13018,15 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -12945,6 +13056,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -13241,6 +13361,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", diff --git a/package.json b/package.json index e8244ff3..38a5a44b 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "next-intl": "^4.5.8", "next-themes": "^0.4.6", "openai": "^6.10.0", + "pino": "^10.1.0", "react": "19.2.0", "react-dom": "19.2.0", "react-hook-form": "^7.68.0", diff --git a/prompts.config.ts b/prompts.config.ts index ae1f6e34..5a6005ab 100644 --- a/prompts.config.ts +++ b/prompts.config.ts @@ -65,7 +65,7 @@ export default defineConfig({ // Enable MCP (Model Context Protocol) features including API key generation mcp: true, // Enable comments on prompts - comments: true, + comments: false, }, // Homepage customization diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 00000000..bf2a49d8 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,100 @@ +import pino from "pino"; + +const isDevelopment = process.env.NODE_ENV === "development"; + +export const logger = pino({ + level: process.env.LOG_LEVEL || (isDevelopment ? "debug" : "info"), + + // Structured JSON format for DigitalOcean App Platform log forwarding + formatters: { + level: (label) => ({ level: label }), + bindings: (bindings) => ({ + pid: bindings.pid, + host: bindings.hostname, + app: "prompts-chat", + }), + }, + + // Add timestamp in ISO format for better log aggregation + timestamp: pino.stdTimeFunctions.isoTime, + + // Base context added to all logs + base: { + env: process.env.NODE_ENV, + version: process.env.npm_package_version || "unknown", + }, + + // Redact sensitive fields + redact: { + paths: [ + "password", + "token", + "apiKey", + "api_key", + "authorization", + "cookie", + "req.headers.authorization", + "req.headers.cookie", + ], + censor: "[REDACTED]", + }, +}); + +// Create child loggers for specific contexts +export const createLogger = (context: string) => { + return logger.child({ context }); +}; + +// Pre-configured loggers for common use cases +export const dbLogger = createLogger("database"); +export const authLogger = createLogger("auth"); +export const apiLogger = createLogger("api"); + +// Helper for request logging +export const logRequest = ( + method: string, + path: string, + statusCode: number, + duration: number, + userId?: string +) => { + const log = apiLogger.child({ + method, + path, + statusCode, + duration: `${duration}ms`, + ...(userId && { userId }), + }); + + if (statusCode >= 500) { + log.error("Request failed"); + } else if (statusCode >= 400) { + log.warn("Request error"); + } else { + log.info("Request completed"); + } +}; + +// Helper for error logging with stack traces +export const logError = ( + error: Error | unknown, + context?: string, + meta?: Record +) => { + const errorObj = error instanceof Error ? error : new Error(String(error)); + + logger.error( + { + err: { + message: errorObj.message, + name: errorObj.name, + stack: errorObj.stack, + }, + context, + ...meta, + }, + errorObj.message + ); +}; + +export default logger;