├── .4485dcc8b9598a622b1c702e3665de2991b0f959-audit.json ├── .gitignore ├── .prettierrc ├── README.md ├── lib ├── index.ts └── logging │ ├── logger.interceptor.ts │ ├── logger.interface.ts │ ├── logger.service.spec.ts │ └── logger.service.ts ├── nest-cli.json ├── package-lock.json ├── package.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── webpack.config.js /.4485dcc8b9598a622b1c702e3665de2991b0f959-audit.json: -------------------------------------------------------------------------------- 1 | { 2 | "keep": { 3 | "days": true, 4 | "amount": 10 5 | }, 6 | "auditLog": ".4485dcc8b9598a622b1c702e3665de2991b0f959-audit.json", 7 | "files": [ 8 | { 9 | "date": 1620520942080, 10 | "name": "app-2021-05-09.log", 11 | "hash": "79abd6d4af577d8e530c4224493b2434" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | ### VisualStudio template 53 | ## Ignore Visual Studio temporary files, build results, and 54 | ## files generated by popular Visual Studio add-ons. 55 | ## 56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 57 | 58 | # User-specific files 59 | *.suo 60 | *.user 61 | *.userosscache 62 | *.sln.docstates 63 | 64 | # User-specific files (MonoDevelop/Xamarin Studio) 65 | *.userprefs 66 | 67 | # Build results 68 | [Dd]ebug/ 69 | [Dd]ebugPublic/ 70 | [Rr]elease/ 71 | [Rr]eleases/ 72 | x64/ 73 | x86/ 74 | bld/ 75 | [Bb]in/ 76 | [Oo]bj/ 77 | [Ll]og/ 78 | 79 | # Visual Studio 2015 cache/options directory 80 | .vs/ 81 | # Uncomment if you have tasks that create the project"s static files in wwwroot 82 | #wwwroot/ 83 | 84 | # MSTest test Results 85 | [Tt]est[Rr]esult*/ 86 | [Bb]uild[Ll]og.* 87 | 88 | # NUNIT 89 | *.VisualState.xml 90 | TestResult.xml 91 | 92 | # Build Results of an ATL Project 93 | [Dd]ebugPS/ 94 | [Rr]eleasePS/ 95 | dlldata.c 96 | 97 | # Benchmark Results 98 | BenchmarkDotNet.Artifacts/ 99 | 100 | # .NET Core 101 | project.lock.json 102 | project.fragment.lock.json 103 | artifacts/ 104 | **/Properties/launchSettings.json 105 | 106 | *_i.c 107 | *_p.c 108 | *_i.h 109 | *.ilk 110 | *.meta 111 | *.obj 112 | *.pch 113 | *.pdb 114 | *.pgc 115 | *.pgd 116 | *.rsp 117 | *.sbr 118 | *.tlb 119 | *.tli 120 | *.tlh 121 | *.tmp 122 | *.tmp_proj 123 | *.log 124 | *.vspscc 125 | *.vssscc 126 | .builds 127 | *.pidb 128 | *.svclog 129 | *.scc 130 | 131 | # Chutzpah Test files 132 | _Chutzpah* 133 | 134 | # Visual C++ cache files 135 | ipch/ 136 | *.aps 137 | *.ncb 138 | *.opendb 139 | *.opensdf 140 | *.sdf 141 | *.cachefile 142 | *.VC.db 143 | *.VC.VC.opendb 144 | 145 | # Visual Studio profiler 146 | *.psess 147 | *.vsp 148 | *.vspx 149 | *.sap 150 | 151 | # Visual Studio Trace Files 152 | *.e2e 153 | 154 | # TFS 2012 Local Workspace 155 | $tf/ 156 | 157 | # Guidance Automation Toolkit 158 | *.gpState 159 | 160 | # ReSharper is a .NET coding add-in 161 | _ReSharper*/ 162 | *.[Rr]e[Ss]harper 163 | *.DotSettings.user 164 | 165 | # JustCode is a .NET coding add-in 166 | .JustCode 167 | 168 | # TeamCity is a build add-in 169 | _TeamCity* 170 | 171 | # DotCover is a Code Coverage Tool 172 | *.dotCover 173 | 174 | # AxoCover is a Code Coverage Tool 175 | .axoCover/* 176 | !.axoCover/settings.json 177 | 178 | # Visual Studio code coverage results 179 | *.coverage 180 | *.coveragexml 181 | 182 | # NCrunch 183 | _NCrunch_* 184 | .*crunch*.local.xml 185 | nCrunchTemp_* 186 | 187 | # MightyMoose 188 | *.mm.* 189 | AutoTest.Net/ 190 | 191 | # Web workbench (sass) 192 | .sass-cache/ 193 | 194 | # Installshield output folder 195 | [Ee]xpress/ 196 | 197 | # DocProject is a documentation generator add-in 198 | DocProject/buildhelp/ 199 | DocProject/Help/*.HxT 200 | DocProject/Help/*.HxC 201 | DocProject/Help/*.hhc 202 | DocProject/Help/*.hhk 203 | DocProject/Help/*.hhp 204 | DocProject/Help/Html2 205 | DocProject/Help/html 206 | 207 | # Click-Once directory 208 | publish/ 209 | 210 | # Publish Web Output 211 | *.[Pp]ublish.xml 212 | *.azurePubxml 213 | # Note: Comment the next line if you want to checkin your web deploy settings, 214 | # but database connection strings (with potential passwords) will be unencrypted 215 | *.pubxml 216 | *.publishproj 217 | 218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 219 | # checkin your Azure Web App publish settings, but sensitive information contained 220 | # in these scripts will be unencrypted 221 | PublishScripts/ 222 | 223 | # NuGet Packages 224 | *.nupkg 225 | # The packages folder can be ignored because of Package Restore 226 | **/[Pp]ackages/* 227 | # except build/, which is used as an MSBuild target. 228 | !**/[Pp]ackages/build/ 229 | # Uncomment if necessary however generally it will be regenerated when needed 230 | #!**/[Pp]ackages/repositories.config 231 | # NuGet v3"s project.json files produces more ignorable files 232 | *.nuget.props 233 | *.nuget.targets 234 | 235 | # Microsoft Azure Build Output 236 | csx/ 237 | *.build.csdef 238 | 239 | # Microsoft Azure Emulator 240 | ecf/ 241 | rcf/ 242 | 243 | # Windows Store app package directories and files 244 | AppPackages/ 245 | BundleArtifacts/ 246 | Package.StoreAssociation.xml 247 | _pkginfo.txt 248 | *.appx 249 | 250 | # Visual Studio cache files 251 | # files ending in .cache can be ignored 252 | *.[Cc]ache 253 | # but keep track of directories ending in .cache 254 | !*.[Cc]ache/ 255 | 256 | # Others 257 | ClientBin/ 258 | ~$* 259 | *~ 260 | *.dbmdl 261 | *.dbproj.schemaview 262 | *.jfm 263 | *.pfx 264 | *.publishsettings 265 | orleans.codegen.cs 266 | 267 | # Since there are multiple workflows, uncomment next line to ignore bower_components 268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 269 | #bower_components/ 270 | 271 | # RIA/Silverlight projects 272 | Generated_Code/ 273 | 274 | # Backup & report files from converting an old project file 275 | # to a newer Visual Studio version. Backup files are not needed, 276 | # because we have git ;-) 277 | _UpgradeReport_Files/ 278 | Backup*/ 279 | UpgradeLog*.XML 280 | UpgradeLog*.htm 281 | 282 | # SQL Server files 283 | *.mdf 284 | *.ldf 285 | *.ndf 286 | 287 | # Business Intelligence projects 288 | *.rdl.data 289 | *.bim.layout 290 | *.bim_*.settings 291 | 292 | # Microsoft Fakes 293 | FakesAssemblies/ 294 | 295 | # GhostDoc plugin setting file 296 | *.GhostDoc.xml 297 | 298 | # Node.js Tools for Visual Studio 299 | .ntvs_analysis.dat 300 | node_modules/ 301 | 302 | # Typescript v1 declaration files 303 | typings/ 304 | 305 | # Visual Studio 6 build log 306 | *.plg 307 | 308 | # Visual Studio 6 workspace options file 309 | *.opt 310 | 311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 312 | *.vbw 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # JetBrains Rider 330 | .idea/ 331 | *.sln.iml 332 | 333 | # CodeRush 334 | .cr/ 335 | 336 | # Python Tools for Visual Studio (PTVS) 337 | __pycache__/ 338 | *.pyc 339 | 340 | # Cake - Uncomment if you are using it 341 | # tools/** 342 | # !tools/packages.config 343 | 344 | # Tabs Studio 345 | *.tss 346 | 347 | # Telerik"s JustMock configuration file 348 | *.jmconfig 349 | 350 | # BizTalk build output 351 | *.btp.cs 352 | *.btm.cs 353 | *.odx.cs 354 | *.xsd.cs 355 | 356 | # OpenCover UI analysis results 357 | OpenCover/ 358 | coverage/ 359 | 360 | ### macOS template 361 | # General 362 | .DS_Store 363 | .AppleDouble 364 | .LSOverride 365 | 366 | # Icon must end with two \r 367 | Icon 368 | 369 | # Thumbnails 370 | ._* 371 | 372 | # Files that might appear in the root of a volume 373 | .DocumentRevisions-V100 374 | .fseventsd 375 | .Spotlight-V100 376 | .TemporaryItems 377 | .Trashes 378 | .VolumeIcon.icns 379 | .com.apple.timemachine.donotpresent 380 | 381 | # Directories potentially created on remote AFP share 382 | .AppleDB 383 | .AppleDesktop 384 | Network Trash Folder 385 | Temporary Items 386 | .apdisk 387 | 388 | ======= 389 | # Local 390 | docker-compose.yml 391 | .env 392 | 393 | logs/ 394 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Install 2 | ``` 3 | npm i nest-logger 4 | ``` 5 | 6 | # TL;DR 7 | 8 | Add this in your logger module's useFactory, inject LoggerService and start logging beautiful log entries. 9 | 10 | ```javascript 11 | const options: LoggerOptions = { 12 | fileOptions: { 13 | filename: `logs/my-service-%DATE%.log`, 14 | }, 15 | colorize: config.colorize, 16 | }; 17 | const loggers = LoggerService.getLoggers( 18 | [LoggerTransport.CONSOLE, LoggerTransport.ROTATE], 19 | options, 20 | ); 21 | 22 | return new LoggerService( 23 | config.logLevel, 24 | loggers, 25 | ); 26 | ``` 27 | 28 | # Detailed usage examples 29 | 30 | Use in your project by creating a logger.module.ts with content like this: 31 | 32 | ```javascript 33 | import { Module } from "@nestjs/common"; 34 | import { ConfigModule } from "../config/config.module"; 35 | import { LoggerService, LoggerOptions } from "nest-logger"; 36 | import { ConfigService } from "../config/config.service"; 37 | 38 | @Module({ 39 | imports: [ConfigModule], 40 | providers: [ 41 | { 42 | provide: LoggerService, 43 | useFactory: (config: ConfigService) => { 44 | // getLoggers() is a helper function to get configured console and/or rotate logger transports. 45 | // It takes takes two parameters: 46 | // 1: Appenders where to log to: console or rotate or both in array 47 | // (eg. [LoggerTransport.CONSOLE, LoggerTransport.ROTATE]) 48 | // 2: Logger options object that contains the following properties: 49 | // timeFormat?: winston's time format syntax. Defaults to "HH:mm:ss". 50 | // colorize?: whether to colorize the log output. Defaults to true. 51 | // consoleOptions?: see Winston's ConsoleTransportOptions interface 52 | // fileOptions?: see Winston Daily Rotate File's DailyRotateFile.DailyRotateFileTransportOptions 53 | const options: LoggerOptions = { 54 | fileOptions: { 55 | filename: `${config.logger.path}/${config.serviceName}-%DATE%.log`, 56 | }, 57 | colorize: config.colorize, 58 | }; 59 | const loggers = LoggerService.getLoggers( 60 | config.logAppenders, 61 | options, 62 | ); 63 | // LoggerService constructor will take two parameters: 64 | // 1. Log level: debug, info, warn or error 65 | // 2. List of logger transport objects. 66 | return new LoggerService( 67 | config.logLevel, 68 | loggers, 69 | ); 70 | }, 71 | inject: [ConfigService], 72 | }, 73 | ], 74 | exports: [LoggerService], 75 | }) 76 | export class LoggerModule {} 77 | ``` 78 | 79 | ## Overriding transport options per logger 80 | You can do it like this if you want different options for console and file: 81 | ```javascript 82 | const loggers = [ 83 | LoggerService.console({ 84 | timeFormat: "HH:mm", 85 | consoleOptions: { 86 | level: "info", 87 | }, 88 | }), 89 | LoggerService.rotate({ 90 | colorize: false, 91 | fileOptions: { 92 | filename: `${config.logger.path}/${config.serviceName}-%DATE%.log`, 93 | level: "error", 94 | }, 95 | }), 96 | ]; 97 | return new LoggerService( 98 | config.logger.defaultLevel, 99 | loggers, 100 | ); 101 | ``` 102 | 103 | ## Overriding formatter function 104 | 105 | Write a function that returns logform.Format object (it's the same what Winston uses): 106 | 107 | ```javascript 108 | const customFormatter = (options: LoggerOptions) => { 109 | const format = winston.format.printf(info => { 110 | const level = options.colorize ? this.colorizeLevel(info.level) : `[${info.level.toUpperCase()}]`.padEnd(7); 111 | return `${info.timestamp} ${context}${level}${reqId} ${info.message}`; 112 | }); 113 | 114 | return winston.format.combine( 115 | winston.format.timestamp({ 116 | format: options.timeFormat, 117 | }), 118 | format, 119 | ); 120 | } 121 | ``` 122 | 123 | Pass the formatter function as the third param of LoggerService's constructor 124 | ```javascript 125 | 126 | return new LoggerService( 127 | config.logLevel, 128 | loggers, 129 | customFormatter 130 | ); 131 | ``` 132 | 133 | ## Overriding winston logger options 134 | 135 | Replace level parameter in the first argument of LoggerService's constructor. 136 | Note that the transports option will be set from the loggers (second constructor parameter) so there's no use to override those here. 137 | ```javascript 138 | const myCustomLevels = { 139 | levels: { 140 | info: 0, 141 | warning: 1, 142 | error: 2, 143 | apocalypse: 3 144 | }, 145 | colors: { 146 | info: 'blue', 147 | warning: 'green', 148 | error: 'yellow', 149 | apocalypse: 'red' 150 | } 151 | }; 152 | const loggerOptions: winston.LoggerOptions = { 153 | level: config.logLevel, 154 | levels: customLevels, 155 | } 156 | 157 | return new LoggerService( 158 | loggerOptions, 159 | loggers, 160 | customFormatter 161 | ); 162 | ``` 163 | 164 | ## Using the logger 165 | 166 | Import logger module wherever you need it: 167 | 168 | ```javascript 169 | ... 170 | import { LoggerModule } from "../logging/logger.module"; 171 | 172 | @Module({ 173 | imports: [ 174 | LoggerModule, 175 | DBModule, 176 | ], 177 | controllers: [ItemController], 178 | providers: [ItemService], 179 | }) 180 | export class ItemModule {} 181 | ``` 182 | 183 | And log stuff: 184 | ```javascript 185 | import { LoggerService } from "nest-logger"; 186 | constructor(private readonly logger: LoggerService) {} 187 | 188 | public logStuff() { 189 | this.logger.debug(`Found ${result.rowCount} items from db`, ItemService.name); 190 | this.logger.error(`Error while getting items from db`, err.stack, ItemService.name); 191 | } 192 | ``` 193 | 194 | # Release Notes 195 | 196 | ## 7.0.0 197 | - NestJS 7.6.11 -> 7.6.15 198 | - Merge 1.2.1 -> 2.1.1 199 | - RxJS 6.6.3 -> 7.0.0 200 | - etc. libs to the latest 201 | - Moment only needed in devDependencies 202 | 203 | ## 6.2.0 204 | - NestJS 7.4.4 -> 7.6.11 205 | - Axios 0.20.0 -> 0.21.1 206 | 207 | ## 6.1.0 208 | - Pass Winston logger options as an optional constructor parameter for LoggerService 209 | - Pass custom formatter function as an optional constructor parameter for LoggerService 210 | - NestJS 7.0.9 -> 7.4.4 211 | - Moment 2.25.3 -> 2.29.0 212 | - rxjs 6.5.4 -> 6.6.3 213 | - Typescript 3.8.3 -> 4.0.3 214 | - Winston 3.2.1 -> 3.3.3 215 | - Winston Daily Rotate File 4.4.2 -> 4.5.0 216 | 217 | ## 6.0.0 218 | - NestJS 6.10.14 -> 7.0.9 219 | - Updated other deps to the latest 220 | 221 | ## 5.0.1 222 | - Nullpointer fix to init loggers without passing any options (credits to OpportunityLiu) 223 | 224 | ## 5.0.0 225 | - Support for all winston logger options for console and rotate transports 226 | 227 | ## 4.0.1 228 | - Fixed a bug where default options were overridden after the first transport creation 229 | 230 | ## 4.0.0 231 | - More configurable way of initializing the logger 232 | 233 | Options can be passed as an object 234 | Colors can be disabled/enabled from the options 235 | - RxJs 6.5.2 -> 6.5.4 236 | - Nest 6.10.6 -> 6.10.14 237 | - Winston Daily Rotate File 4.3.0 -> 4.4.1 238 | 239 | ## 3.0.0 240 | - Log Map objects as key-value-pairs 241 | - Dependency upgrades 242 | - NestJS 6.3.2 -> 6.10.6 243 | - Winston Daily Rotate File 3.9.0 -> 4.3.0 244 | - Typescript 3.5.2 -> 3.7.3 245 | 246 | ## 2.1.0 247 | - NestJS 6.2.4 -> 6.3.2 248 | 249 | ## 2.0.0 250 | - Nest 5.6.2 -> 6.2.4 251 | - RxJs 6.4.0 -> 6.5.2 252 | - Winston Daily Rotate File 3.6.0 -> 3.9.0 253 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logging/logger.service"; 2 | export * from "./logging/logger.interceptor"; 3 | export * from "./logging/logger.interface"; -------------------------------------------------------------------------------- /lib/logging/logger.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, HttpService, CallHandler } from "@nestjs/common"; 2 | import { Observable } from "rxjs"; 3 | import { LoggerService } from "./logger.service"; 4 | import { Request } from "express"; 5 | 6 | @Injectable() 7 | export class LoggerInterceptor implements NestInterceptor { 8 | constructor(private readonly logger: LoggerService, private readonly httpService: HttpService) {} 9 | intercept(context: ExecutionContext, next: CallHandler): Observable { 10 | const request: Request = context.switchToHttp().getRequest(); 11 | const requestId = request.header("x-request-id"); 12 | this.logger.setRequestId(requestId); 13 | if (requestId) { 14 | this.httpService.axiosRef.defaults.headers.common["x-request-id"] = requestId; 15 | } 16 | return next.handle(); 17 | } 18 | } -------------------------------------------------------------------------------- /lib/logging/logger.interface.ts: -------------------------------------------------------------------------------- 1 | import * as Transport from "winston-transport"; 2 | import DailyRotateFile = require("winston-daily-rotate-file"); 3 | import { ConsoleTransportOptions } from "winston/lib/winston/transports"; 4 | 5 | export enum LoggerTransport { 6 | CONSOLE = "console", 7 | ROTATE = "rotate", 8 | } 9 | 10 | export type LogLevel = "emerg" | "alert" | "crit" | "error" | "warning" | "notice" | "info" | "debug"; 11 | 12 | export interface ConfiguredTransport { 13 | transport: Transport; 14 | options: LoggerOptions; 15 | } 16 | 17 | export interface LoggerOptions { 18 | timeFormat?: string; 19 | colorize?: boolean; 20 | consoleOptions?: ConsoleTransportOptions; 21 | fileOptions?: DailyRotateFile.DailyRotateFileTransportOptions; 22 | } -------------------------------------------------------------------------------- /lib/logging/logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from "./logger.service"; 2 | import * as fs from "fs"; 3 | import * as rimraf from "rimraf"; 4 | import * as moment from "moment"; 5 | import { LoggerTransport } from "./logger.interface"; 6 | 7 | describe("LoggerService", () => { 8 | let logger: LoggerService; 9 | let noColorsLogger: LoggerService; 10 | let overrideLevelLogger: LoggerService; 11 | const filePath = "logs"; 12 | const serviceName = "LoggerLib"; 13 | const noColorService = "NoColor"; 14 | const overrideService = "Override"; 15 | const today = moment().format("YYYY-MM-DD"); 16 | const overriddenToday = moment().format("YYYYMMDD"); 17 | const logfile = `${filePath}/${serviceName}-${today}.log`; 18 | const noColorsLogFile = `${filePath}/${noColorService}-${today}.log`; 19 | const overrideLogFile = `${filePath}/${overrideService}-${overriddenToday}.log`; 20 | 21 | beforeAll(async () => { 22 | rimraf.sync(filePath); 23 | logger = new LoggerService( 24 | "debug", 25 | LoggerService.getLoggers( 26 | [LoggerTransport.CONSOLE, LoggerTransport.ROTATE], 27 | { 28 | fileOptions: { 29 | filename: `${filePath}/${serviceName}-%DATE%.log`, 30 | }, 31 | }), 32 | ); 33 | noColorsLogger = new LoggerService( 34 | "debug", 35 | [ 36 | LoggerService.console({ colorize: true }), 37 | LoggerService.rotate({ 38 | colorize: false, 39 | fileOptions: { 40 | filename: `${filePath}/${noColorService}-%DATE%.log`, 41 | }, 42 | }), 43 | ], 44 | ); 45 | 46 | overrideLevelLogger = new LoggerService( 47 | "debug", 48 | [ 49 | LoggerService.console({ 50 | colorize: true, 51 | consoleOptions: { 52 | level: "info", 53 | }, 54 | }), 55 | LoggerService.rotate({ 56 | colorize: false, 57 | timeFormat: "YYYYMMDD", 58 | fileOptions: { 59 | filename: `${filePath}/${overrideService}-%DATE%.log`, 60 | datePattern: "YYYYMMDD", 61 | level: "info", 62 | }, 63 | }), 64 | ], 65 | ); 66 | }); 67 | 68 | describe("log", () => { 69 | it("should create transport with no options", () => { 70 | const consoleTransport = LoggerService.console(); 71 | const rotateTransport = LoggerService.rotate(); 72 | const allTransports = LoggerService.getLoggers([LoggerTransport.CONSOLE, LoggerTransport.ROTATE]); 73 | 74 | expect(consoleTransport).toBeTruthy(); 75 | expect(rotateTransport).toBeTruthy(); 76 | expect(allTransports).toHaveLength(2); 77 | }); 78 | 79 | it("should print log for every level", (done) => { 80 | const object = { 81 | message: "This is the message", 82 | status: "200", 83 | values: ["foo", "bar", "zap"], 84 | sub: { 85 | hero: "He-Man", 86 | }, 87 | }; 88 | 89 | const map = new Map(); 90 | map.set("foo", "bar"); 91 | 92 | logger.info(object, "LoggerServiceTest"); 93 | logger.info(map, "LoggerServiceTest"); 94 | logger.log("test log with info level", "LoggerServiceTest"); 95 | logger.warn("test log with warn level", "LoggerServiceTest"); 96 | logger.error("test log with error level", new Error().stack, "LoggerServiceTest"); 97 | logger.setRequestId("abc123"); 98 | logger.debug("test log with request id", "LoggerServiceTest"); 99 | logger.debug("test log with long filename", "LoggerServiceTestWithLongFilename"); 100 | logger.setContext("TestContext"); 101 | logger.info("test log with predefined context"); 102 | logger.info("test log with predefined context overridden", "OverriddenContext"); 103 | expect(fs.existsSync(logfile)).toBe(true); 104 | 105 | setTimeout(() => { 106 | const log = fs.readFileSync(logfile).toString(); 107 | expect(log.split("\n").length).toBe(30); 108 | done(); 109 | }, 600); 110 | }); 111 | 112 | it("should print log for every level without colors", (done) => { 113 | const object = { 114 | message: "This is the message", 115 | status: "200", 116 | values: ["foo", "bar", "zap"], 117 | sub: { 118 | hero: "He-Man", 119 | }, 120 | }; 121 | 122 | const map = new Map(); 123 | map.set("foo", "bar"); 124 | 125 | noColorsLogger.info(object, "NoColorTest"); 126 | noColorsLogger.info(map, "NoColorTest"); 127 | noColorsLogger.log("test log with info level", "NoColorTest"); 128 | noColorsLogger.warn("test log with warn level", "NoColorTest"); 129 | noColorsLogger.error("test log with error level", new Error().stack, "NoColorTest"); 130 | noColorsLogger.setRequestId("abc123"); 131 | noColorsLogger.debug("test log with request id", "NoColorTest"); 132 | noColorsLogger.debug("test log with long filename", "NoColorTestWithLongFilename"); 133 | noColorsLogger.setContext("TestContext"); 134 | noColorsLogger.info("test log with predefined context"); 135 | noColorsLogger.info("test log with predefined context overridden", "NoColorTest"); 136 | expect(fs.existsSync(noColorsLogFile)).toBe(true); 137 | 138 | setTimeout(() => { 139 | const log = fs.readFileSync(noColorsLogFile).toString(); 140 | expect(log.split("\n").length).toBe(29); 141 | expect(log.length).toBe(1358); 142 | done(); 143 | }, 600); 144 | }); 145 | 146 | it("should log only info level logs", (done) => { 147 | overrideLevelLogger.debug("Debug level log should not be visible", "OverrideLevel"); 148 | overrideLevelLogger.info("Info level log should be visible", "OverrideLevel"); 149 | expect(fs.existsSync(overrideLogFile)).toBe(true); 150 | 151 | setTimeout(() => { 152 | const log = fs.readFileSync(overrideLogFile).toString(); 153 | expect(log.replace(/(\r\n|\n|\r)/gm, "")).toBe(`${overriddenToday} [OverrideLevel] [INFO] Info level log should be visible`); 154 | done(); 155 | }, 600); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /lib/logging/logger.service.ts: -------------------------------------------------------------------------------- 1 | import * as clc from "cli-color"; 2 | import * as winston from "winston"; 3 | import * as logform from "logform"; 4 | import * as DailyRotateFile from "winston-daily-rotate-file"; 5 | import { LoggerOptions, LoggerTransport, ConfiguredTransport, LogLevel } from "./logger.interface"; 6 | import { ConsoleTransportOptions } from "winston/lib/winston/transports"; 7 | 8 | export class LoggerService { 9 | 10 | public static DEFAULT_TIME_FORMAT = "HH:mm:ss"; 11 | public static DEFAULT_LEVEL: LogLevel = "info"; 12 | public static DEFAULT_FILENAME = "-"; 13 | 14 | private static DEFAULT_FILE_OPTIONS: DailyRotateFile.DailyRotateFileTransportOptions = { 15 | filename: LoggerService.DEFAULT_FILENAME, 16 | datePattern: "YYYY-MM-DD", 17 | zippedArchive: false, 18 | maxFiles: "10d", 19 | options: { flags: "a", mode: "0776" }, 20 | }; 21 | 22 | private static DEFAULT_CONSOLE_OPTIONS: ConsoleTransportOptions = {}; 23 | 24 | private static DEFAULT_LOGGER_OPTIONS: LoggerOptions = { 25 | timeFormat: LoggerService.DEFAULT_TIME_FORMAT, 26 | fileOptions: LoggerService.DEFAULT_FILE_OPTIONS, 27 | consoleOptions: LoggerService.DEFAULT_CONSOLE_OPTIONS, 28 | colorize: true, 29 | }; 30 | 31 | private logger: winston.Logger; 32 | private requestId: string; 33 | private context: string; 34 | 35 | constructor( 36 | level: string | winston.LoggerOptions, 37 | loggers: ConfiguredTransport[], 38 | formatter?: (options: LoggerOptions) => logform.Format 39 | ) { 40 | loggers.forEach(logger => logger.transport.format = formatter ? formatter(logger.options) : this.defaultFormatter(logger.options)); 41 | // TODO: fix level => loggerOptions in the next major release 42 | if (typeof level === "string" || level instanceof String) { 43 | this.logger = winston.createLogger({ 44 | level: level as string, 45 | transports: loggers.map(l => l.transport), 46 | }); 47 | } else { 48 | const winstonLoggerOptions: winston.LoggerOptions = level; 49 | winstonLoggerOptions.transports = loggers.map(l => l.transport); 50 | this.logger = winston.createLogger(winstonLoggerOptions); 51 | } 52 | } 53 | 54 | public static getLoggers(transportNames: LoggerTransport[], options?: LoggerOptions) : ConfiguredTransport[] { 55 | const loggers = [] as ConfiguredTransport[]; 56 | if (transportNames.indexOf(LoggerTransport.CONSOLE) >= 0) { 57 | loggers.push(LoggerService.console(options)); 58 | } 59 | if (transportNames.indexOf(LoggerTransport.ROTATE) >= 0) { 60 | loggers.push(LoggerService.rotate(options)); 61 | } 62 | return loggers; 63 | } 64 | 65 | public static console(options?: LoggerOptions): ConfiguredTransport { 66 | const defaultOptions = Object.assign({}, LoggerService.DEFAULT_LOGGER_OPTIONS); 67 | const consoleLoggerOptions = Object.assign(defaultOptions, options); 68 | const consoleTransportOptions = Object.assign(defaultOptions.consoleOptions, consoleLoggerOptions.consoleOptions); 69 | const transport = new winston.transports.Console(consoleTransportOptions); 70 | return { transport, options: consoleLoggerOptions }; 71 | } 72 | 73 | public static rotate(options?: LoggerOptions): ConfiguredTransport { 74 | const defaultOptions = Object.assign({}, LoggerService.DEFAULT_LOGGER_OPTIONS); 75 | const fileLoggerOptions = Object.assign(defaultOptions, options); 76 | const fileTransportOptions = Object.assign(defaultOptions.fileOptions, fileLoggerOptions.fileOptions); 77 | 78 | if (fileTransportOptions.filename === LoggerService.DEFAULT_FILENAME) { 79 | fileTransportOptions.filename = `app-%DATE%.log`; 80 | } 81 | 82 | const transport = new DailyRotateFile(fileTransportOptions); 83 | return { transport, options: fileLoggerOptions }; 84 | } 85 | 86 | setRequestId(id: string) { 87 | this.requestId = id; 88 | } 89 | 90 | getRequestId() { 91 | return this.requestId; 92 | } 93 | 94 | setContext(ctx: string) { 95 | this.context = ctx; 96 | } 97 | 98 | log(msg: any, context?: string) { 99 | this.info(this.dataToString(msg), context); 100 | } 101 | 102 | debug(msg: any, context?: string) { 103 | this.logger.debug(this.dataToString(msg), [{ context, reqId: this.requestId }]); 104 | } 105 | 106 | info(msg: any, context?: string) { 107 | this.logger.info(this.dataToString(msg), [{ context, reqId: this.requestId }]); 108 | } 109 | 110 | warn(msg: any, context?: string) { 111 | this.logger.warn(this.dataToString(msg), [{ context, reqId: this.requestId }]); 112 | } 113 | 114 | error(msg: any, trace?: string, context?: string) { 115 | this.logger.error(this.dataToString(msg), [{ context }]); 116 | this.logger.error(trace, [{ context, reqId: this.requestId }]); 117 | } 118 | 119 | private dataToString(msg: any) { 120 | // Support for Map objects 121 | if (typeof msg.entries === "function" && typeof msg.forEach === "function") { 122 | const elements = []; 123 | msg.forEach((value: any, key: any) => elements.push(`${key}:${value}`)); 124 | return elements; 125 | } else { 126 | return msg; 127 | } 128 | } 129 | 130 | private defaultFormatter(options: LoggerOptions) { 131 | const colorize = options.colorize; 132 | const format = winston.format.printf(info => { 133 | const level = colorize ? this.colorizeLevel(info.level) : `[${info.level.toUpperCase()}]`.padEnd(7); 134 | let message = info.message; 135 | if (typeof info.message === "object") { 136 | message = JSON.stringify(message, null, 3); 137 | } 138 | let reqId: string = ""; 139 | let context: string = ""; 140 | if (info["0"]) { 141 | const meta = info["0"]; 142 | if (meta.reqId) { 143 | reqId = colorize ? clc.cyan(`[${meta.reqId}]`) : `[${meta.reqId}]`; 144 | } 145 | 146 | const ctx = meta.context || this.context || null; 147 | if (ctx) { 148 | context = `[${ctx.substr(0, 20)}]`.padEnd(32); 149 | if (colorize) { 150 | context = clc.blackBright(context); 151 | } 152 | } 153 | } 154 | 155 | return `${info.timestamp} ${context}${level}${reqId} ${message}`; 156 | }); 157 | 158 | return winston.format.combine( 159 | winston.format.timestamp({ 160 | format: options.timeFormat, 161 | }), 162 | format, 163 | ); 164 | } 165 | 166 | private colorizeLevel(level: string) { 167 | let colorFunc: (msg: string) => string; 168 | switch (level) { 169 | case "debug": 170 | colorFunc = (msg) => clc.blue(msg); 171 | break; 172 | case "info": 173 | colorFunc = (msg) => clc.green(msg); 174 | break; 175 | case "warn": 176 | colorFunc = (msg) => clc.yellow(msg); 177 | break; 178 | case "error": 179 | colorFunc = (msg) => clc.red(msg); 180 | break; 181 | } 182 | 183 | // 17 because of the color bytes 184 | return colorFunc(`[${level.toUpperCase()}]`).padEnd(17); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "lib" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-logger", 3 | "version": "7.0.0", 4 | "description": "Logger library for Nest apps", 5 | "author": { 6 | "name": "Jukka Hell", 7 | "url": "https://www.maksien.fi" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Juuso Kosonen" 12 | } 13 | ], 14 | "license": "MIT", 15 | "keywords": [ 16 | "nest", 17 | "nestjs", 18 | "winston", 19 | "daily-rotate", 20 | "logger", 21 | "logging" 22 | ], 23 | "main": "dist/index.js", 24 | "types": "dist/index.d.ts", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/jukkahell/nestlogger.git" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "scripts": { 33 | "format": "prettier --write \"lib/**/*.ts\"", 34 | "lint": "tslint --fix lib/**/*.ts -p tsconfig.json -c tslint.json", 35 | "build": "rimraf dist && tsc", 36 | "test": "jest", 37 | "prepublishOnly": "npm run build", 38 | "test:watch": "jest --watch", 39 | "test:cov": "jest --coverage", 40 | "webpack": "webpack --config webpack.config.js" 41 | }, 42 | "dependencies": { 43 | "@nestjs/common": "7.6.15", 44 | "@nestjs/core": "7.6.15", 45 | "@nestjs/testing": "7.6.15", 46 | "cli-color": "2.0.0", 47 | "event-stream": "4.0.1", 48 | "merge": "2.1.1", 49 | "reflect-metadata": "0.1.13", 50 | "rxjs": "7.0.0", 51 | "typescript": "4.2.4", 52 | "winston": "3.3.3", 53 | "winston-daily-rotate-file": "4.5.5" 54 | }, 55 | "devDependencies": { 56 | "@types/cli-color": "2.0.0", 57 | "@types/express": "4.17.11", 58 | "@types/jest": "26.0.23", 59 | "@types/node": "15.0.2", 60 | "@types/rimraf": "3.0.0", 61 | "@types/supertest": "2.0.11", 62 | "jest": "26.6.3", 63 | "moment": "2.29.1", 64 | "nodemon": "2.0.7", 65 | "prettier": "2.2.1", 66 | "rimraf": "3.0.2", 67 | "supertest": "6.1.3", 68 | "ts-jest": "26.5.6", 69 | "ts-loader": "9.1.2", 70 | "ts-node": "9.1.1", 71 | "tsconfig-paths": "3.9.0", 72 | "tslint": "6.1.3", 73 | "webpack": "5.36.2", 74 | "webpack-cli": "4.7.0", 75 | "webpack-node-externals": "3.0.0" 76 | }, 77 | "jest": { 78 | "moduleFileExtensions": [ 79 | "js", 80 | "json", 81 | "ts" 82 | ], 83 | "rootDir": "lib", 84 | "testRegex": ".spec.ts$", 85 | "transform": { 86 | "^.+\\.(t|j)s$": "ts-jest" 87 | }, 88 | "coverageDirectory": "../coverage", 89 | "testEnvironment": "node" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "lib": ["es2017", "es6", "es7", "dom"], 9 | "allowSyntheticDefaultImports": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./lib" 16 | }, 17 | "include": [ 18 | "lib/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "eofline": false, 11 | "quotemark": [ 12 | true, 13 | "double" 14 | ], 15 | "indent": false, 16 | "member-access": [ 17 | false 18 | ], 19 | "ordered-imports": [ 20 | false 21 | ], 22 | "max-line-length": [ 23 | true, 24 | 150 25 | ], 26 | "member-ordering": [ 27 | false 28 | ], 29 | "curly": false, 30 | "interface-name": [ 31 | false 32 | ], 33 | "array-type": [ 34 | false 35 | ], 36 | "no-empty-interface": false, 37 | "no-empty": false, 38 | "arrow-parens": false, 39 | "object-literal-sort-keys": false, 40 | "no-unused-expression": false, 41 | "max-classes-per-file": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | false 46 | ], 47 | "one-line": [ 48 | false 49 | ], 50 | "one-variable-per-declaration": [ 51 | false 52 | ] 53 | }, 54 | "rulesDirectory": [] 55 | } 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | --------------------------------------------------------------------------------