From 58ae897e9050d2e1dee1d43ae27571fe1b0fb069 Mon Sep 17 00:00:00 2001 From: Bojan Kucera Date: Tue, 3 Jun 2025 18:31:02 -0400 Subject: [PATCH] added logger --- .../data-services/data-processor/package.json | 9 +- .../data-services/data-processor/src/index.ts | 14 +- .../data-processor/tsconfig.json | 4 +- bun.lock | 140 ++++++- libs/logger/README.md | 342 +++++++++++++++++ libs/logger/package.json | 26 ++ libs/logger/src/index.ts | 43 +++ libs/logger/src/logger.ts | 358 ++++++++++++++++++ libs/logger/src/middleware.ts | 200 ++++++++++ libs/logger/src/types.ts | 119 ++++++ libs/logger/src/utils.ts | 230 +++++++++++ libs/logger/tsconfig.json | 16 + tsconfig.json | 4 +- 13 files changed, 1493 insertions(+), 12 deletions(-) create mode 100644 libs/logger/README.md create mode 100644 libs/logger/package.json create mode 100644 libs/logger/src/index.ts create mode 100644 libs/logger/src/logger.ts create mode 100644 libs/logger/src/middleware.ts create mode 100644 libs/logger/src/types.ts create mode 100644 libs/logger/src/utils.ts create mode 100644 libs/logger/tsconfig.json diff --git a/apps/data-services/data-processor/package.json b/apps/data-services/data-processor/package.json index e1751a6..412d408 100644 --- a/apps/data-services/data-processor/package.json +++ b/apps/data-services/data-processor/package.json @@ -2,19 +2,18 @@ "name": "data-processor", "version": "1.0.0", "description": "Data processing and pipeline orchestration service", - "main": "src/index.ts", - "scripts": { + "main": "src/index.ts", "scripts": { "dev": "bun run --watch src/index.ts", "start": "bun run src/index.ts", - "build": "bun build src/index.ts --outdir=dist", + "build": "bun build src/index.ts --outdir=dist --target=bun", "test": "bun test", "lint": "eslint src/**/*.ts", "type-check": "tsc --noEmit" - }, - "dependencies": { + },"dependencies": { "@stock-bot/types": "*", "@stock-bot/event-bus": "*", "@stock-bot/utils": "*", + "@stock-bot/logger": "*", "@stock-bot/api-client": "*", "hono": "^4.6.3", "ioredis": "^5.4.1", diff --git a/apps/data-services/data-processor/src/index.ts b/apps/data-services/data-processor/src/index.ts index b9ffcd0..a05ed8c 100644 --- a/apps/data-services/data-processor/src/index.ts +++ b/apps/data-services/data-processor/src/index.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import { serve } from 'bun'; -import { logger } from '@stock-bot/utils'; +import { getLogger, loggingMiddleware, errorLoggingMiddleware } from '@stock-bot/logger'; import { DataPipelineOrchestrator } from './core/DataPipelineOrchestrator'; import { DataQualityService } from './services/DataQualityService'; import { DataIngestionService } from './services/DataIngestionService'; @@ -12,6 +12,18 @@ import { JobController } from './controllers/JobController'; const app = new Hono(); +// Initialize logger +const logger = getLogger('data-processor'); + +// Add logging middleware +app.use('*', loggingMiddleware({ + serviceName: 'data-processor', + skipPaths: ['/health'] +})); + +// Add error logging middleware +app.use('*', errorLoggingMiddleware(logger)); + // Services const dataQualityService = new DataQualityService(); const dataIngestionService = new DataIngestionService(); diff --git a/apps/data-services/data-processor/tsconfig.json b/apps/data-services/data-processor/tsconfig.json index 0271e03..db42c0d 100644 --- a/apps/data-services/data-processor/tsconfig.json +++ b/apps/data-services/data-processor/tsconfig.json @@ -11,12 +11,12 @@ "types": ["bun-types"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"], - "references": [ + "exclude": ["node_modules", "dist"], "references": [ { "path": "../../../libs/api-client" }, { "path": "../../../libs/config" }, { "path": "../../../libs/event-bus" }, { "path": "../../../libs/http-client" }, + { "path": "../../../libs/logger" }, { "path": "../../../libs/types" }, { "path": "../../../libs/utils" }, ] diff --git a/bun.lock b/bun.lock index a9654b8..9538290 100644 --- a/bun.lock +++ b/bun.lock @@ -93,6 +93,7 @@ "dependencies": { "@stock-bot/api-client": "*", "@stock-bot/event-bus": "*", + "@stock-bot/logger": "*", "@stock-bot/types": "*", "@stock-bot/utils": "*", "axios": "^1.6.2", @@ -286,6 +287,23 @@ "typescript": "^5.3.0", }, }, + "libs/logger": { + "name": "@stock-bot/logger", + "version": "1.0.0", + "dependencies": { + "@stock-bot/config": "workspace:*", + "@stock-bot/types": "workspace:*", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^4.7.1", + "winston-loki": "^6.0.8", + }, + "devDependencies": { + "@types/jest": "^29.5.2", + "@types/node": "^20.5.0", + "jest": "^29.5.0", + "typescript": "^5.4.5", + }, + }, "libs/types": { "name": "@stock-bot/types", "version": "1.0.0", @@ -417,7 +435,9 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], - "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], @@ -629,6 +649,32 @@ "@napi-rs/nice-win32-x64-msvc": ["@napi-rs/nice-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg=="], + "@napi-rs/snappy-android-arm-eabi": ["@napi-rs/snappy-android-arm-eabi@7.2.2", "", { "os": "android", "cpu": "arm" }, "sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw=="], + + "@napi-rs/snappy-android-arm64": ["@napi-rs/snappy-android-arm64@7.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-2R/A3qok+nGtpVK8oUMcrIi5OMDckGYNoBLFyli3zp8w6IArPRfg1yOfVUcHvpUDTo9T7LOS1fXgMOoC796eQw=="], + + "@napi-rs/snappy-darwin-arm64": ["@napi-rs/snappy-darwin-arm64@7.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-USgArHbfrmdbuq33bD5ssbkPIoT7YCXCRLmZpDS6dMDrx+iM7eD2BecNbOOo7/v1eu6TRmQ0xOzeQ6I/9FIi5g=="], + + "@napi-rs/snappy-darwin-x64": ["@napi-rs/snappy-darwin-x64@7.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-0APDu8iO5iT0IJKblk2lH0VpWSl9zOZndZKnBYIc+ei1npw2L5QvuErFOTeTdHBtzvUHASB+9bvgaWnQo4PvTQ=="], + + "@napi-rs/snappy-freebsd-x64": ["@napi-rs/snappy-freebsd-x64@7.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRTCJsuzy0o/B0Hnp9CwNB5V6cOJ4wedDTWEthsdKHSsQlO7WU9W1yP7H3Qv3Ccp/ZfMyrmG98Ad7u7lG58WXA=="], + + "@napi-rs/snappy-linux-arm-gnueabihf": ["@napi-rs/snappy-linux-arm-gnueabihf@7.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-v1uzm8+6uYjasBPcFkv90VLZ+WhLzr/tnfkZ/iD9mHYiULqkqpRuC8zvc3FZaJy5wLQE9zTDkTJN1IvUcZ+Vcg=="], + + "@napi-rs/snappy-linux-arm64-gnu": ["@napi-rs/snappy-linux-arm64-gnu@7.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrEMa5pBScs4GXWOn6ZYXfQ72IzoolZw5txqUHVGs8eK4g1HR9HTHhb2oY5ySNaKakG5sOgMsb1rwaEnjhChmQ=="], + + "@napi-rs/snappy-linux-arm64-musl": ["@napi-rs/snappy-linux-arm64-musl@7.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3orWZo9hUpGQcB+3aTLW7UFDqNCQfbr0+MvV67x8nMNYj5eAeUtMmUE/HxLznHO4eZ1qSqiTwLbVx05/Socdlw=="], + + "@napi-rs/snappy-linux-x64-gnu": ["@napi-rs/snappy-linux-x64-gnu@7.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-jZt8Jit/HHDcavt80zxEkDpH+R1Ic0ssiVCoueASzMXa7vwPJeF4ZxZyqUw4qeSy7n8UUExomu8G8ZbP6VKhgw=="], + + "@napi-rs/snappy-linux-x64-musl": ["@napi-rs/snappy-linux-x64-musl@7.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Dh96IXgcZrV39a+Tej/owcd9vr5ihiZ3KRix11rr1v0MWtVb61+H1GXXlz6+Zcx9y8jM1NmOuiIuJwkV4vZ4WA=="], + + "@napi-rs/snappy-win32-arm64-msvc": ["@napi-rs/snappy-win32-arm64-msvc@7.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-9No0b3xGbHSWv2wtLEn3MO76Yopn1U2TdemZpCaEgOGccz1V+a/1d16Piz3ofSmnA13HGFz3h9NwZH9EOaIgYA=="], + + "@napi-rs/snappy-win32-ia32-msvc": ["@napi-rs/snappy-win32-ia32-msvc@7.2.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QiGe+0G86J74Qz1JcHtBwM3OYdTni1hX1PFyLRo3HhQUSpmi13Bzc1En7APn+6Pvo7gkrcy81dObGLDSxFAkQQ=="], + + "@napi-rs/snappy-win32-x64-msvc": ["@napi-rs/snappy-win32-x64-msvc@7.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-a43cyx1nK0daw6BZxVcvDEXxKMFLSBSDTAhsFD0VqSKcC7MGUBMaqyoWUcMiI7LBSz4bxUmxDWKfCYzpEmeb3w=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -683,6 +729,26 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.2", "", { "os": "android", "cpu": "arm" }, "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.2", "", { "os": "android", "cpu": "arm64" }, "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw=="], @@ -761,6 +827,8 @@ "@stock-bot/http-client": ["@stock-bot/http-client@workspace:libs/http-client"], + "@stock-bot/logger": ["@stock-bot/logger@workspace:libs/logger"], + "@stock-bot/market-data-gateway": ["@stock-bot/market-data-gateway@workspace:apps/core-services/market-data-gateway"], "@stock-bot/types": ["@stock-bot/types@workspace:libs/types"], @@ -867,6 +935,8 @@ "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + "@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -927,6 +997,10 @@ "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], @@ -969,6 +1043,8 @@ "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + "btoa": ["btoa@1.2.1", "", { "bin": { "btoa": "bin/btoa.js" } }, "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -1021,12 +1097,18 @@ "collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="], + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], @@ -1123,6 +1205,8 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], @@ -1221,10 +1305,14 @@ "feature-store": ["feature-store@workspace:apps/data-services/feature-store"], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + "file-stream-rotator": ["file-stream-rotator@0.6.1", "", { "dependencies": { "moment": "^2.29.1" } }, "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], @@ -1235,6 +1323,8 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -1487,6 +1577,8 @@ "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -1535,6 +1627,10 @@ "log4js": ["log4js@6.9.1", "", { "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", "flatted": "^3.2.7", "rfdc": "^1.3.0", "streamroller": "^3.1.5" } }, "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g=="], + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "luxon": ["luxon@3.5.0", "", {}, "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="], @@ -1587,6 +1683,8 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1651,6 +1749,8 @@ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], @@ -1661,6 +1761,8 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -1743,6 +1845,8 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "protobufjs": ["protobufjs@7.5.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -1765,7 +1869,7 @@ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -1841,6 +1945,8 @@ "sigstore": ["sigstore@3.1.0", "", { "dependencies": { "@sigstore/bundle": "^3.1.0", "@sigstore/core": "^2.0.0", "@sigstore/protobuf-specs": "^0.4.0", "@sigstore/sign": "^3.1.0", "@sigstore/tuf": "^3.1.0", "@sigstore/verify": "^2.1.0" } }, "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q=="], + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -1849,6 +1955,8 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "snappy": ["snappy@7.2.2", "", { "optionalDependencies": { "@napi-rs/snappy-android-arm-eabi": "7.2.2", "@napi-rs/snappy-android-arm64": "7.2.2", "@napi-rs/snappy-darwin-arm64": "7.2.2", "@napi-rs/snappy-darwin-x64": "7.2.2", "@napi-rs/snappy-freebsd-x64": "7.2.2", "@napi-rs/snappy-linux-arm-gnueabihf": "7.2.2", "@napi-rs/snappy-linux-arm64-gnu": "7.2.2", "@napi-rs/snappy-linux-arm64-musl": "7.2.2", "@napi-rs/snappy-linux-x64-gnu": "7.2.2", "@napi-rs/snappy-linux-x64-musl": "7.2.2", "@napi-rs/snappy-win32-arm64-msvc": "7.2.2", "@napi-rs/snappy-win32-ia32-msvc": "7.2.2", "@napi-rs/snappy-win32-x64-msvc": "7.2.2" } }, "sha512-iADMq1kY0v3vJmGTuKcFWSXt15qYUz7wFkArOrsSg0IFfI3nJqIJvK2/ZbEIndg7erIJLtAVX2nSOqPz7DcwbA=="], + "socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="], "socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="], @@ -1881,6 +1989,8 @@ "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], @@ -1923,6 +2033,8 @@ "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], @@ -1939,6 +2051,8 @@ "trading-dashboard": ["trading-dashboard@workspace:apps/interface-services/trading-dashboard"], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1985,6 +2099,10 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url-polyfill": ["url-polyfill@1.1.13", "", {}, "sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], @@ -2013,6 +2131,14 @@ "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-daily-rotate-file": ["winston-daily-rotate-file@4.7.1", "", { "dependencies": { "file-stream-rotator": "^0.6.1", "object-hash": "^2.0.1", "triple-beam": "^1.3.0", "winston-transport": "^4.4.0" }, "peerDependencies": { "winston": "^3" } }, "sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA=="], + + "winston-loki": ["winston-loki@6.1.3", "", { "dependencies": { "async-exit-hook": "2.0.1", "btoa": "^1.2.1", "protobufjs": "^7.2.4", "url-polyfill": "^1.1.12", "winston-transport": "^4.3.0" }, "optionalDependencies": { "snappy": "^7.2.2" } }, "sha512-DjWtJ230xHyYQWr9mZJa93yhwHttn3JEtSYWP8vXZWJOahiQheUhf+88dSIidbGXB3u0oLweV6G1vkL/ouT62Q=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], @@ -2131,6 +2257,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2183,6 +2311,8 @@ "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "karma/@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "karma/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "karma/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], @@ -2229,10 +2359,14 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], @@ -2325,6 +2459,8 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/libs/logger/README.md b/libs/logger/README.md new file mode 100644 index 0000000..8c10d65 --- /dev/null +++ b/libs/logger/README.md @@ -0,0 +1,342 @@ +# @stock-bot/logger + +Enhanced logging library with Loki integration for the Stock Bot platform. + +## Features + +- 🎯 **Multiple Log Levels**: debug, info, warn, error, http +- 🌐 **Loki Integration**: Centralized logging with Grafana visualization +- 📁 **File Logging**: Daily rotating log files with compression +- 🎨 **Console Logging**: Colored, formatted console output +- 📊 **Structured Logging**: JSON-formatted logs with metadata +- ⚡ **Performance Optimized**: Batching and async logging +- 🔐 **Security**: Automatic sensitive data masking +- 🎭 **Express Middleware**: Request/response logging +- 📈 **Business Events**: Specialized logging for trading operations + +## Installation + +```bash +cd libs/logger +npm install +``` + +## Basic Usage + +### Simple Logging + +```typescript +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('my-service'); + +logger.info('Service started'); +logger.warn('This is a warning'); +logger.error('An error occurred', new Error('Something went wrong')); +``` + +### With Context + +```typescript +import { Logger } from '@stock-bot/logger'; + +const logger = new Logger('trading-service', { + userId: '12345', + sessionId: 'abc-def-ghi' +}); + +logger.info('Trade executed', { + symbol: 'AAPL', + quantity: 100, + price: 150.25 +}); +``` + +### Performance Logging + +```typescript +import { getLogger, createTimer } from '@stock-bot/logger'; + +const logger = getLogger('data-processor'); +const timer = createTimer('data-processing'); + +// ... do some work ... + +const timing = timer.end(); +logger.performance('Data processing completed', timing); +``` + +### Business Events + +```typescript +import { getLogger, createBusinessEvent } from '@stock-bot/logger'; + +const logger = getLogger('order-service'); + +logger.business('Order placed', createBusinessEvent( + 'order', + 'place', + { + entity: 'order-123', + result: 'success', + symbol: 'TSLA', + amount: 50000 + } +)); +``` + +### Security Events + +```typescript +import { getLogger, createSecurityEvent } from '@stock-bot/logger'; + +const logger = getLogger('auth-service'); + +logger.security('Failed login attempt', createSecurityEvent( + 'authentication', + { + user: 'john@example.com', + result: 'failure', + ip: '192.168.1.100', + severity: 'medium' + } +)); +``` + +## Express Middleware + +### Basic Request Logging + +```typescript +import express from 'express'; +import { loggingMiddleware } from '@stock-bot/logger'; + +const app = express(); + +app.use(loggingMiddleware({ + serviceName: 'api-gateway', + skipPaths: ['/health', '/metrics'] +})); +``` + +### Error Logging + +```typescript +import { errorLoggingMiddleware, getLogger } from '@stock-bot/logger'; + +const logger = getLogger('api-gateway'); + +// Add after your routes but before error handlers +app.use(errorLoggingMiddleware(logger)); +``` + +### Request-scoped Logger + +```typescript +import { createRequestLogger, getLogger } from '@stock-bot/logger'; + +const baseLogger = getLogger('api-gateway'); + +app.use((req, res, next) => { + req.logger = createRequestLogger(req, baseLogger); + next(); +}); + +app.get('/api/data', (req, res) => { + req.logger.info('Processing data request'); + // ... handle request ... +}); +``` + +## Configuration + +The logger uses configuration from `@stock-bot/config`. Key environment variables: + +```bash +# Logging +LOG_LEVEL=info +LOG_CONSOLE=true +LOG_FILE=true +LOG_FILE_PATH=./logs + +# Loki +LOKI_HOST=localhost +LOKI_PORT=3100 +LOKI_BATCH_SIZE=1024 +LOKI_FLUSH_INTERVAL_MS=5000 +``` + +## Advanced Usage + +### Child Loggers + +```typescript +const parentLogger = getLogger('trading-service'); +const orderLogger = parentLogger.child({ + module: 'order-processing', + orderId: '12345' +}); + +orderLogger.info('Order validated'); // Will include parent context +``` + +### Custom Transports + +```typescript +import { createLogger } from '@stock-bot/logger'; + +const logger = createLogger('custom-service', { + level: 'debug', + enableLoki: true, + enableFile: false, + enableConsole: true +}); +``` + +### Sensitive Data Masking + +```typescript +import { sanitizeMetadata, maskSensitiveData } from '@stock-bot/logger'; + +const unsafeData = { + username: 'john', + password: 'secret123', + apiKey: 'abc123def456' +}; + +const safeData = sanitizeMetadata(unsafeData); +// { username: 'john', password: '[REDACTED]', apiKey: '[REDACTED]' } + +const message = maskSensitiveData('User API key: abc123def456'); +// 'User API key: [API_KEY]' +``` + +### Log Throttling + +```typescript +import { LogThrottle } from '@stock-bot/logger'; + +const throttle = new LogThrottle(10, 60000); // 10 logs per minute + +if (throttle.shouldLog('error-key')) { + logger.error('This error will be throttled'); +} +``` + +## Viewing Logs + +### Grafana Dashboard + +1. Start the monitoring stack: `docker-compose up grafana loki` +2. Open Grafana at http://localhost:3000 +3. Use the "Stock Bot Logs" dashboard +4. Query logs with LogQL: `{service="your-service"}` + +### Log Files + +When file logging is enabled, logs are written to: +- `./logs/{service-name}-YYYY-MM-DD.log` - All logs +- `./logs/{service-name}-error-YYYY-MM-DD.log` - Error logs only + +## Best Practices + +1. **Use appropriate log levels**: + - `debug`: Detailed development information + - `info`: General operational messages + - `warn`: Potential issues + - `error`: Actual errors requiring attention + +2. **Include context**: Always provide relevant metadata + ```typescript + logger.info('Trade executed', { symbol, quantity, price, orderId }); + ``` + +3. **Use structured logging**: Avoid string concatenation + ```typescript + // Good + logger.info('User logged in', { userId, ip, userAgent }); + + // Avoid + logger.info(`User ${userId} logged in from ${ip}`); + ``` + +4. **Handle sensitive data**: Use sanitization utilities + ```typescript + const safeMetadata = sanitizeMetadata(requestData); + logger.info('API request', safeMetadata); + ``` + +5. **Use correlation IDs**: Track requests across services + ```typescript + const logger = getLogger('service').child({ + correlationId: req.headers['x-correlation-id'] + }); + ``` + +## Integration with Services + +To use in your service: + +1. Add dependency to your service's `package.json`: + ```json + { + "dependencies": { + "@stock-bot/logger": "workspace:*" + } + } + ``` + +2. Update your service's `tsconfig.json` references: + ```json + { + "references": [ + { "path": "../../../libs/logger" } + ] + } + ``` + +3. Import and use: + ```typescript + import { getLogger } from '@stock-bot/logger'; + + const logger = getLogger('my-service'); + ``` + +## Performance Considerations + +- Logs are batched and sent asynchronously to Loki +- File logging uses daily rotation to prevent large files +- Console logging can be disabled in production +- Use log throttling for high-frequency events +- Sensitive data is automatically masked + +## Troubleshooting + +### Logs not appearing in Loki + +1. Check Loki connection: + ```bash + curl http://localhost:3100/ready + ``` + +2. Verify environment variables: + ```bash + echo $LOKI_HOST $LOKI_PORT + ``` + +3. Check container logs: + ```bash + docker logs stock-bot-loki + ``` + +### High memory usage + +- Reduce `LOKI_BATCH_SIZE` if batching too many logs +- Decrease `LOKI_FLUSH_INTERVAL_MS` to flush more frequently +- Disable file logging if not needed + +### Missing logs + +- Check log level configuration +- Verify service name matches expectations +- Ensure proper error handling around logger calls diff --git a/libs/logger/package.json b/libs/logger/package.json new file mode 100644 index 0000000..eb1906e --- /dev/null +++ b/libs/logger/package.json @@ -0,0 +1,26 @@ +{ + "name": "@stock-bot/logger", + "version": "1.0.0", + "description": "Enhanced logging library with Loki integration for stock-bot services", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "test": "jest" + }, + "dependencies": { + "@stock-bot/types": "workspace:*", + "@stock-bot/config": "workspace:*", + "winston": "^3.11.0", + "winston-loki": "^6.0.8", + "winston-daily-rotate-file": "^4.7.1" + }, + "devDependencies": { + "@types/jest": "^29.5.2", + "@types/node": "^20.5.0", + "jest": "^29.5.0", + "typescript": "^5.4.5" + } +} diff --git a/libs/logger/src/index.ts b/libs/logger/src/index.ts new file mode 100644 index 0000000..32f95fc --- /dev/null +++ b/libs/logger/src/index.ts @@ -0,0 +1,43 @@ +/** + * @stock-bot/logger - Enhanced logging library with Loki integration + * + * Main exports for the logger library + */ + +// Core logger classes and functions +export { Logger, createLogger, getLogger, shutdownLoggers } from './logger'; + +// Utility functions +export { + createTimer, + formatError, + sanitizeMetadata, + generateCorrelationId, + extractHttpMetadata, + createBusinessEvent, + createSecurityEvent, + maskSensitiveData, + calculateLogSize, + LogThrottle +} from './utils'; + +// Express middleware +export { + loggingMiddleware, + errorLoggingMiddleware, + createRequestLogger +} from './middleware'; + +// Type exports +export type { + LogLevel, + LogContext, + LogMetadata, + LoggerOptions, + LokiTransportOptions, + PerformanceTimer, + LokiLogEntry, + StructuredLog +} from './types'; + +export type { LoggingMiddlewareOptions } from './middleware'; diff --git a/libs/logger/src/logger.ts b/libs/logger/src/logger.ts new file mode 100644 index 0000000..3b4a451 --- /dev/null +++ b/libs/logger/src/logger.ts @@ -0,0 +1,358 @@ +/** + * Enhanced logger with Loki integration for Stock Bot platform + * + * Features: + * - Multiple log levels (debug, info, warn, error) + * - Console and file logging + * - Loki integration for centralized logging + * - Structured logging with metadata + * - Performance optimized with batching + * - Service-specific context + */ + +import winston from 'winston'; +import LokiTransport from 'winston-loki'; +import DailyRotateFile from 'winston-daily-rotate-file'; +import { loggingConfig, lokiConfig } from '@stock-bot/config'; +import type { LogLevel, LogContext, LogMetadata } from './types'; + +// Global logger instances cache +const loggerInstances = new Map(); + +/** + * Create or retrieve a logger instance for a specific service + */ +export function createLogger(serviceName: string, options?: { + level?: LogLevel; + enableLoki?: boolean; + enableFile?: boolean; + enableConsole?: boolean; +}): winston.Logger { + const key = `${serviceName}-${JSON.stringify(options || {})}`; + + if (loggerInstances.has(key)) { + return loggerInstances.get(key)!; + } + + const logger = buildLogger(serviceName, options); + loggerInstances.set(key, logger); + + return logger; +} + +/** + * Build a winston logger with all configured transports + */ +function buildLogger(serviceName: string, options?: { + level?: LogLevel; + enableLoki?: boolean; + enableFile?: boolean; + enableConsole?: boolean; +}): winston.Logger { + const { + level = loggingConfig.LOG_LEVEL as LogLevel, + enableLoki = true, + enableFile = loggingConfig.LOG_FILE, + enableConsole = loggingConfig.LOG_CONSOLE + } = options || {}; // Base logger configuration + const transports: winston.transport[] = []; + + // Console transport + if (enableConsole) { + transports.push(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple(), + winston.format.printf(({ timestamp, level, service, message, metadata }) => { + const meta = metadata && Object.keys(metadata).length > 0 + ? `\n${JSON.stringify(metadata, null, 2)}` + : ''; + return `${timestamp} [${level}] [${service}] ${message}${meta}`; + }) + ) + })); + } + + // File transport with daily rotation + if (enableFile) { + // General log file + transports.push(new DailyRotateFile({ + filename: `${loggingConfig.LOG_FILE_PATH}/${serviceName}-%DATE%.log`, + datePattern: loggingConfig.LOG_FILE_DATE_PATTERN, + zippedArchive: true, + maxSize: loggingConfig.LOG_FILE_MAX_SIZE, + maxFiles: loggingConfig.LOG_FILE_MAX_FILES, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ) + })); + + // Separate error log file + if (loggingConfig.LOG_ERROR_FILE) { + transports.push(new DailyRotateFile({ + level: 'error', + filename: `${loggingConfig.LOG_FILE_PATH}/${serviceName}-error-%DATE%.log`, + datePattern: loggingConfig.LOG_FILE_DATE_PATTERN, + zippedArchive: true, + maxSize: loggingConfig.LOG_FILE_MAX_SIZE, + maxFiles: loggingConfig.LOG_FILE_MAX_FILES, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ) + })); + } + } + + // Loki transport for centralized logging + if (enableLoki && lokiConfig.LOKI_HOST) { + try { + const lokiTransport = new LokiTransport({ + host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`, + labels: { + service: serviceName, + environment: lokiConfig.LOKI_ENVIRONMENT_LABEL, + ...(lokiConfig.LOKI_DEFAULT_LABELS ? JSON.parse(lokiConfig.LOKI_DEFAULT_LABELS) : {}) + }, + json: true, + batching: true, + interval: lokiConfig.LOKI_FLUSH_INTERVAL_MS, + timeout: lokiConfig.LOKI_PUSH_TIMEOUT, + basicAuth: lokiConfig.LOKI_USERNAME && lokiConfig.LOKI_PASSWORD + ? `${lokiConfig.LOKI_USERNAME}:${lokiConfig.LOKI_PASSWORD}` + : undefined, + onConnectionError: (err) => { + console.error('Loki connection error:', err); + } + }); + + transports.push(lokiTransport); + } catch (error) { + console.warn('Failed to initialize Loki transport:', error); + } + } + + const loggerConfig: winston.LoggerOptions = { + level, + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), + winston.format.errors({ stack: true }), + winston.format.metadata({ + fillExcept: ['message', 'level', 'timestamp', 'service'] + }), + winston.format.json() + ), + defaultMeta: { + service: serviceName, + environment: loggingConfig.LOG_ENVIRONMENT, + version: loggingConfig.LOG_SERVICE_VERSION + }, + transports + }; + + return winston.createLogger(loggerConfig); +} + +/** + * Enhanced Logger class with convenience methods + */ +export class Logger { + private winston: winston.Logger; + private serviceName: string; + private context: LogContext; + + constructor(serviceName: string, context: LogContext = {}, options?: { + level?: LogLevel; + enableLoki?: boolean; + enableFile?: boolean; + enableConsole?: boolean; + }) { + this.serviceName = serviceName; + this.context = context; + this.winston = createLogger(serviceName, options); + } + + /** + * Debug level logging + */ + debug(message: string, metadata?: LogMetadata): void { + this.log('debug', message, metadata); + } + + /** + * Info level logging + */ + info(message: string, metadata?: LogMetadata): void { + this.log('info', message, metadata); + } + + /** + * Warning level logging + */ + warn(message: string, metadata?: LogMetadata): void { + this.log('warn', message, metadata); + } + + /** + * Error level logging + */ + error(message: string, error?: Error | any, metadata?: LogMetadata): void { + const logData: LogMetadata = { ...metadata }; + + if (error) { + if (error instanceof Error) { + logData.error = { + name: error.name, + message: error.message, + stack: loggingConfig.LOG_ERROR_STACK ? error.stack : undefined + }; + } else { + logData.error = error; + } + } + + this.log('error', message, logData); + } + + /** + * HTTP request logging + */ + http(message: string, requestData?: { + method?: string; + url?: string; + statusCode?: number; + responseTime?: number; + userAgent?: string; + ip?: string; + }): void { + if (!loggingConfig.LOG_HTTP_REQUESTS) return; + + this.log('http', message, { + request: requestData, + type: 'http_request' + }); + } + + /** + * Performance/timing logging + */ + performance(message: string, timing: { + operation: string; + duration: number; + startTime?: number; + endTime?: number; + }): void { + if (!loggingConfig.LOG_PERFORMANCE) return; + + this.log('info', message, { + performance: timing, + type: 'performance' + }); + } + + /** + * Business event logging + */ + business(message: string, event: { + type: string; + entity?: string; + action?: string; + result?: 'success' | 'failure' | 'partial'; + amount?: number; + symbol?: string; + }): void { + this.log('info', message, { + business: event, + type: 'business_event' + }); + } + + /** + * Security event logging + */ + security(message: string, event: { + type: 'authentication' | 'authorization' | 'access' | 'vulnerability'; + user?: string; + resource?: string; + action?: string; + result?: 'success' | 'failure'; + ip?: string; + severity?: 'low' | 'medium' | 'high' | 'critical'; + }): void { + this.log('warn', message, { + security: event, + type: 'security_event' + }); + } + + /** + * Create a child logger with additional context + */ + child(context: LogContext): Logger { + return new Logger(this.serviceName, { ...this.context, ...context }); + } + + /** + * Add persistent context to this logger + */ + addContext(context: LogContext): void { + this.context = { ...this.context, ...context }; + } + + /** + * Internal logging method + */ + private log(level: LogLevel, message: string, metadata?: LogMetadata): void { + const logData = { + ...this.context, + ...metadata, + timestamp: new Date().toISOString() + }; + + this.winston.log(level, message, logData); + } + + /** + * Get the underlying winston logger + */ + getWinstonLogger(): winston.Logger { + return this.winston; + } + /** + * Gracefully close all transports + */ + async close(): Promise { + return new Promise((resolve) => { + this.winston.end(() => { + resolve(); + }); + }); + } +} + +/** + * Create a default logger instance for a service + */ +export function getLogger(serviceName: string, context?: LogContext): Logger { + return new Logger(serviceName, context); +} + +/** + * Shutdown all logger instances gracefully + */ +export async function shutdownLoggers(): Promise { + const closePromises = Array.from(loggerInstances.values()).map(logger => + new Promise((resolve) => { + logger.end(() => { + resolve(); + }); + }) + ); + + await Promise.all(closePromises); + loggerInstances.clear(); +} + +// Export types for convenience +export type { LogLevel, LogContext, LogMetadata } from './types'; diff --git a/libs/logger/src/middleware.ts b/libs/logger/src/middleware.ts new file mode 100644 index 0000000..35631e5 --- /dev/null +++ b/libs/logger/src/middleware.ts @@ -0,0 +1,200 @@ +/** + * Hono middleware for request logging + */ + +import type { Context, Next } from 'hono'; +import { Logger } from './logger'; +import { generateCorrelationId, createTimer } from './utils'; + +export interface LoggingMiddlewareOptions { + logger?: Logger; + serviceName?: string; + skipPaths?: string[]; + skipSuccessfulRequests?: boolean; + logRequestBody?: boolean; + logResponseBody?: boolean; + maxBodySize?: number; +} + +/** + * Hono middleware for HTTP request/response logging + */ +export function loggingMiddleware(options: LoggingMiddlewareOptions = {}) { + const { + serviceName = 'unknown-service', + skipPaths = ['/health', '/metrics', '/favicon.ico'], + skipSuccessfulRequests = false, + logRequestBody = false, + logResponseBody = false, + maxBodySize = 1024 + } = options; + + // Create logger if not provided + const logger = options.logger || new Logger(serviceName); + + return async (c: Context, next: Next) => { + const url = new URL(c.req.url); + const path = url.pathname; + + // Skip certain paths + if (skipPaths.some(skipPath => path.startsWith(skipPath))) { + return next(); + } // Generate correlation ID + const correlationId = generateCorrelationId(); + // Store correlation ID in context for later use + c.set('correlationId', correlationId); + // Set correlation ID as response header + c.header('x-correlation-id', correlationId); + + // Start timer + const timer = createTimer(`${c.req.method} ${path}`); // Extract request metadata + const requestMetadata: any = { + method: c.req.method, + url: c.req.url, + path: path, + userAgent: c.req.header('user-agent'), + contentType: c.req.header('content-type'), + contentLength: c.req.header('content-length'), + ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown' + }; + + // Add request body if enabled + if (logRequestBody) { + try { + const body = await c.req.text(); + if (body) { + requestMetadata.body = body.length > maxBodySize + ? body.substring(0, maxBodySize) + '...[truncated]' + : body; + } + } catch (error) { + // Body might not be available or already consumed + } + } // Log request start + logger.http('HTTP Request started', { + method: c.req.method, + url: c.req.url, + userAgent: c.req.header('user-agent'), + ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown' + }); + + // Process request + await next(); + + // Calculate response time + const timing = timer.end(); + + // Get response information + const response = c.res; + const status = response.status; + + // Determine log level based on status code + const isError = status >= 400; + const isSuccess = status >= 200 && status < 300; + + // Skip successful requests if configured + if (skipSuccessfulRequests && isSuccess) { + return; + } + + const responseMetadata: any = { + correlationId, + request: requestMetadata, + response: { + statusCode: status, + statusText: response.statusText, + contentLength: response.headers.get('content-length'), + contentType: response.headers.get('content-type') + }, + performance: timing + }; + + // Add response body if enabled + if (logResponseBody) { + try { + const responseBody = await response.clone().text(); + if (responseBody) { + responseMetadata.response.body = responseBody.length > maxBodySize + ? responseBody.substring(0, maxBodySize) + '...[truncated]' + : responseBody; + } + } catch (error) { + // Response body might not be available + } + } // Log based on status code + if (isError) { + logger.error('HTTP Request failed', undefined, { + correlationId, + request: requestMetadata, + response: { + statusCode: status, + statusText: response.statusText, + contentLength: response.headers.get('content-length'), + contentType: response.headers.get('content-type') + }, + performance: timing + }); + } else { + logger.http('HTTP Request completed', { + method: c.req.method, + url: c.req.url, + statusCode: status, + responseTime: timing.duration, + userAgent: c.req.header('user-agent'), + ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown' + }); + } + }; +} + +/** + * Error logging middleware for Hono + */ +export function errorLoggingMiddleware(logger?: Logger) { + return async (c: Context, next: Next) => { + const errorLogger = logger || new Logger('error-handler'); + + try { + await next(); + } catch (err) { + const correlationId = c.get('correlationId') || c.req.header('x-correlation-id'); + const url = new URL(c.req.url); + + const requestMetadata = { + method: c.req.method, + url: c.req.url, + path: url.pathname, + userAgent: c.req.header('user-agent'), + ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown' + }; + + errorLogger.error('Unhandled HTTP error', err instanceof Error ? err : new Error(String(err)), { + correlationId, + request: requestMetadata, + response: { + statusCode: c.res.status + } + }); + + // Re-throw the error so Hono can handle it + throw err; + } + }; +} + +/** + * Create a child logger with request context for Hono + */ +export function createRequestLogger(c: Context, baseLogger: Logger): Logger { + const correlationId = c.get('correlationId') || c.req.header('x-correlation-id'); + const url = new URL(c.req.url); + + return baseLogger.child({ + correlationId, + requestId: correlationId, + method: c.req.method, + path: url.pathname, + userAgent: c.req.header('user-agent'), + ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown' + }); +} diff --git a/libs/logger/src/types.ts b/libs/logger/src/types.ts new file mode 100644 index 0000000..8f545b3 --- /dev/null +++ b/libs/logger/src/types.ts @@ -0,0 +1,119 @@ +/** + * Type definitions for the logger library + */ + +// Standard log levels +export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly'; + +// Context that persists across log calls +export interface LogContext { + [key: string]: any; + requestId?: string; + sessionId?: string; + userId?: string; + traceId?: string; + spanId?: string; + correlationId?: string; +} + +// Metadata for individual log entries +export interface LogMetadata { + [key: string]: any; + correlationId?: string; + error?: { + name: string; + message: string; + stack?: string; + } | any;request?: { + method?: string; + url?: string; + path?: string; + statusCode?: number; + responseTime?: number; + userAgent?: string; + ip?: string; + body?: string; + headers?: Record; + contentType?: string; + contentLength?: string; + }; + performance?: { + operation: string; + duration: number; + startTime?: number; + endTime?: number; + }; + business?: { + type: string; + entity?: string; + action?: string; + result?: 'success' | 'failure' | 'partial'; + amount?: number; + symbol?: string; + }; + security?: { + type: 'authentication' | 'authorization' | 'access' | 'vulnerability'; + user?: string; + resource?: string; + action?: string; + result?: 'success' | 'failure'; + ip?: string; + severity?: 'low' | 'medium' | 'high' | 'critical'; + }; + type?: 'http_request' | 'performance' | 'business_event' | 'security_event' | 'system' | 'custom'; +} + +// Logger configuration options +export interface LoggerOptions { + level?: LogLevel; + enableLoki?: boolean; + enableFile?: boolean; + enableConsole?: boolean; +} + +// Loki-specific configuration +export interface LokiTransportOptions { + host: string; + port?: number; + url?: string; + username?: string; + password?: string; + tenantId?: string; + labels?: Record; + batchSize?: number; + interval?: number; + timeout?: number; + json?: boolean; + batching?: boolean; + basicAuth?: string; +} + +// Performance timer utility type +export interface PerformanceTimer { + operation: string; + startTime: number; + end(): { operation: string; duration: number; startTime: number; endTime: number }; +} + +// Log entry structure for Loki +export interface LokiLogEntry { + timestamp: string; + level: LogLevel; + message: string; + service: string; + environment?: string; + version?: string; + metadata?: LogMetadata; +} + +// Structured log format +export interface StructuredLog { + timestamp: string; + level: LogLevel; + service: string; + message: string; + context?: LogContext; + metadata?: LogMetadata; + environment?: string; + version?: string; +} diff --git a/libs/logger/src/utils.ts b/libs/logger/src/utils.ts new file mode 100644 index 0000000..33d5837 --- /dev/null +++ b/libs/logger/src/utils.ts @@ -0,0 +1,230 @@ +/** + * Utility functions for logging operations + */ + +import type { PerformanceTimer, LogMetadata } from './types'; + +/** + * Create a performance timer to measure operation duration + */ +export function createTimer(operation: string): PerformanceTimer { + const startTime = Date.now(); + + return { + operation, + startTime, + end() { + const endTime = Date.now(); + return { + operation, + duration: endTime - startTime, + startTime, + endTime + }; + } + }; +} + +/** + * Format error for logging + */ +export function formatError(error: Error | any): LogMetadata['error'] { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + stack: error.stack + }; + } + + if (typeof error === 'object' && error !== null) { + return { + name: error.constructor?.name || 'UnknownError', + message: error.message || error.toString(), + ...error + }; + } + + return { + name: 'UnknownError', + message: String(error) + }; +} + +/** + * Sanitize sensitive data from log metadata + */ +export function sanitizeMetadata(metadata: LogMetadata): LogMetadata { + const sensitiveKeys = [ + 'password', 'token', 'secret', 'key', 'apiKey', 'api_key', + 'authorization', 'auth', 'credential', 'credentials' + ]; + + function sanitizeValue(value: any): any { + if (typeof value === 'string') { + return '[REDACTED]'; + } + if (Array.isArray(value)) { + return value.map(sanitizeValue); + } + if (typeof value === 'object' && value !== null) { + const sanitized: any = {}; + for (const [key, val] of Object.entries(value)) { + const lowerKey = key.toLowerCase(); + if (sensitiveKeys.some(sensitive => lowerKey.includes(sensitive))) { + sanitized[key] = '[REDACTED]'; + } else { + sanitized[key] = sanitizeValue(val); + } + } + return sanitized; + } + return value; + } + + const sanitized: LogMetadata = {}; + for (const [key, value] of Object.entries(metadata)) { + const lowerKey = key.toLowerCase(); + if (sensitiveKeys.some(sensitive => lowerKey.includes(sensitive))) { + sanitized[key] = '[REDACTED]'; + } else { + sanitized[key] = sanitizeValue(value); + } + } + + return sanitized; +} + +/** + * Generate a correlation ID for request tracking + */ +export function generateCorrelationId(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +} + +/** + * Extract relevant HTTP request information for logging + */ +export function extractHttpMetadata(req: any): LogMetadata['request'] { + if (!req) return undefined; + + return { + method: req.method, + url: req.url || req.originalUrl, + userAgent: req.get?.('User-Agent') || req.headers?.['user-agent'], + ip: req.ip || req.connection?.remoteAddress || req.headers?.['x-forwarded-for'] + }; +} + +/** + * Create standardized business event metadata + */ +export function createBusinessEvent( + type: string, + action: string, + options?: { + entity?: string; + result?: 'success' | 'failure' | 'partial'; + amount?: number; + symbol?: string; + [key: string]: any; + } +): LogMetadata { + return { + business: { + type, + action, + ...options + }, + type: 'business_event' + }; +} + +/** + * Create standardized security event metadata + */ +export function createSecurityEvent( + type: 'authentication' | 'authorization' | 'access' | 'vulnerability', + options?: { + user?: string; + resource?: string; + action?: string; + result?: 'success' | 'failure'; + ip?: string; + severity?: 'low' | 'medium' | 'high' | 'critical'; + [key: string]: any; + } +): LogMetadata { + return { + security: { + type, + ...options + }, + type: 'security_event' + }; +} + +/** + * Mask sensitive data in strings + */ +export function maskSensitiveData(str: string): string { + // Credit card numbers + str = str.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '****-****-****-****'); + + // Email addresses + str = str.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '***@***.***'); + + // API keys (common patterns) + str = str.replace(/\b[A-Za-z0-9]{32,}\b/g, '[API_KEY]'); + + // JWT tokens + str = str.replace(/eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g, '[JWT_TOKEN]'); + + return str; +} + +/** + * Calculate log entry size for batching + */ +export function calculateLogSize(entry: any): number { + return JSON.stringify(entry).length; +} + +/** + * Throttle logging to prevent spam + */ +export class LogThrottle { + private counters = new Map(); + private readonly maxLogs: number; + private readonly windowMs: number; + + constructor(maxLogs = 100, windowMs = 60000) { + this.maxLogs = maxLogs; + this.windowMs = windowMs; + } + + shouldLog(key: string): boolean { + const now = Date.now(); + const counter = this.counters.get(key); + + if (!counter || now - counter.lastReset > this.windowMs) { + this.counters.set(key, { count: 1, lastReset: now }); + return true; + } + + if (counter.count >= this.maxLogs) { + return false; + } + + counter.count++; + return true; + } + + reset(key?: string): void { + if (key) { + this.counters.delete(key); + } else { + this.counters.clear(); + } + } +} diff --git a/libs/logger/tsconfig.json b/libs/logger/tsconfig.json new file mode 100644 index 0000000..2c42d5f --- /dev/null +++ b/libs/logger/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": [ + "src/**/*" + ], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "references": [ + { "path": "../config" }, + { "path": "../types" } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 0dd0f7f..39011a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,12 +39,12 @@ "exclude": [ "node_modules", "dist" - ], - "references": [ + ], "references": [ { "path": "./libs/api-client" }, { "path": "./libs/config" }, { "path": "./libs/event-bus" }, { "path": "./libs/http-client" }, + { "path": "./libs/logger" }, { "path": "./libs/types" }, { "path": "./libs/utils" }, ]