├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── jest.config.js ├── lib ├── bc-serverless-logging-framework.d.ts ├── bc-serverless-logging-framework.js ├── bc-serverless-logging-framework.test.d.ts ├── bc-serverless-logging-framework.test.js └── bc-serverless-logging-framework │ ├── constants.d.ts │ ├── constants.js │ ├── destinations │ ├── consoleLog.d.ts │ ├── consoleLog.js │ ├── http.d.ts │ ├── http.js │ ├── index.d.ts │ ├── index.js │ ├── sqs.d.ts │ ├── sqs.js │ └── tests │ │ ├── consoleLog.test.d.ts │ │ └── consoleLog.test.js │ ├── enums │ ├── ConsoleLevels.d.ts │ ├── ConsoleLevels.js │ ├── Levels.d.ts │ └── Levels.js │ ├── errors │ ├── DestinationConfigError.d.ts │ ├── DestinationConfigError.js │ ├── LoggingFrameworkDestinationConfigError.d.ts │ └── LoggingFrameworkDestinationConfigError.js │ ├── types.d.ts │ ├── types.js │ └── util │ ├── createLog.d.ts │ ├── createLog.js │ ├── decircularize.d.ts │ ├── decircularize.js │ ├── tests │ ├── applyDefaultProps.test.d.ts │ ├── applyDefaultProps.test.js │ ├── createLog.test.d.ts │ ├── createLog.test.js │ ├── decircularize.test.d.ts │ ├── decircularize.test.js │ ├── initLog.test.d.ts │ ├── initLog.test.js │ ├── runTransports.test.d.ts │ ├── runTransports.test.js │ ├── setupTransport.test.d.ts │ └── setupTransport.test.js │ ├── transport.d.ts │ └── transport.js ├── package.json ├── prettier.config.js ├── src ├── bc-serverless-logging-framework.test.ts ├── bc-serverless-logging-framework.ts └── bc-serverless-logging-framework │ ├── constants.ts │ ├── destinations │ ├── consoleLog.ts │ ├── http.ts │ ├── index.ts │ ├── sqs.ts │ └── tests │ │ └── consoleLog.test.ts │ ├── enums │ ├── ConsoleLevels.ts │ └── Levels.ts │ ├── errors │ └── LoggingFrameworkDestinationConfigError.ts │ ├── types.ts │ └── util │ ├── createLog.ts │ ├── decircularize.ts │ ├── tests │ ├── applyDefaultProps.test.ts │ ├── createLog.test.ts │ ├── decircularize.test.ts │ ├── initLog.test.ts │ ├── runTransports.test.ts │ └── setupTransport.test.ts │ └── transport.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | .vscode -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | jest: true 4 | }, 5 | env: { 6 | browser: false, 7 | node: true, 8 | es6: true, 9 | jest: true 10 | }, 11 | extends: ['eslint:recommended', 'plugin:jest/recommended'], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | sourceType: 'module', 15 | ecmaVersion: 2020 16 | }, 17 | plugins: ['@typescript-eslint', 'prettier'], 18 | rules: { 19 | indent: ['error', 2, { SwitchCase: 1 }], 20 | 'linebreak-style': ['error', 'unix'], 21 | semi: ['error', 'never'], 22 | 'eol-last': ['error', 'always'], 23 | 'no-console': 'off', 24 | 'prettier/prettier': 'error' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | playground* 3 | coverage -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "typescript" 4 | ], 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | } 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bc-serverless-logging-framework 2 | 3 | An easy-to-use logging framework that simplifies logging within serverless architectures. 4 | 5 | ## Background 6 | 7 | There are already some really great logging libraries out there -- examples including [pino](https://www.npmjs.com/package/pino), [log4js](https://www.npmjs.com/package/log4js), [morgan](https://www.npmjs.com/package/morgan), and [winston](https://www.npmjs.com/package/winston). 8 | 9 | Unfortunately, we encountered a use case that even these libraries did not seem to handle gracefully, and it's becoming a bigger and bigger use case every day -- **serverless**. 10 | 11 | Serverless architectures typically require a robust logging implementation involving one or more external target destinations, as opposed to a logging implementation in a typical "serverful" environment which sometimes is as simple as logging to one or more files. 12 | 13 | Many projects already have a way of sending to external resources, but it is difficult to find any way to queue up sending logs to external resources and wait for the logs to send before closing out your process. This scenario is absolutely crucial for a serverless function such as AWS Lambda, as closing the process before logs finish sending to external resources might cause logs to sometimes not make it. 14 | 15 | At [Big Compass](https://www.bigcompass.com/) we strive to add value to the cloud community so have decided to implement `bc-serverless-logging-framework`: our own JSON logging solution that works easily not only on serverless architectures but on traditional systems as well. 16 | 17 | ## Installation 18 | 19 | Below details the installation methods for this framework. Both methods end in the same result, and do not need any environmental configurations. 20 | 21 | Since this framework is managed by you, and typically deployed as a dependency in your applications, you decide if you deploy this with any public facing resources. 22 | 23 | No IAM roles or policies are specifically needed to install this framework in your applications. No sensitive information is stored as a part of this installation either. You choose where you want to send logs. If you connect to an external target, be sure to follow best practices for storing your credentials securely. Tools like AWS Secrets Manager can help. 24 | 25 | This installation is completely free. You only pay for the applications/microservices you deploy this framework on. 26 | 27 | ### High Availability 28 | Since this logging framework gets installed within your applications (ideally as a dependency), you choose how you want to deploy your applications. It is recommended to deploy any application with this logging framework installed on it in a highly available environment using a multi-AZ approach if you are deploying this framework on an application that gets deployed in a VPC on AWS. For serverless applications, it's recommended to go with the out-of-the-box high availability of AWS Lambda that does not get deployed in a VPC. 29 | 30 | ### Skills Required 31 | Before installing this dependency, it is recommended you have skills with: 32 | 1. NodeJS 33 | 2. Git CLI 34 | 3. NPM 35 | 4. Deploying to AWS requires an AWS account 36 | 37 | ### NPM/Yarn Dependency (Recommended Deployment Option) 38 | 39 | #### Prerequisites 40 | 1. Either Yarn or NPM is installed for installing the framework as a dependency 41 | 42 | This project is not currently registered with NPM, but can be added as a dependency in your applications in less than a minute using NPM by referencing this repository directly. 43 | 44 | npm: 45 | 46 | ``` 47 | npm install --save https://github.com/BigCompass/bc-serverless-logging-framework.git 48 | ``` 49 | 50 | yarn: 51 | 52 | ``` 53 | yarn add https://github.com/BigCompass/bc-serverless-logging-framework.git 54 | ``` 55 | 56 | ### Clone the Repository 57 | 58 | #### Prerequisites 59 | 1. Git CLI is installed on your server/machine 60 | 61 | You may also clone the repository and include this framework in your applications by using a local version of the code. It is still recommended to manage this framework from a central location and as a dependency in your microservices/applications. This installation technique takes less than a minute. 62 | 63 | ``` 64 | git clone git@github.com:BigCompass/bc-serverless-logging-framework.git 65 | ``` 66 | ### Installation Time 67 | Although you can include this logging framework as a dependency in your application in less than a minute using one of the techniques described above, you still have to deploy the application this framework is installed on. The timeframe to deploy your application depends on the application's complexity. In any scenario, it is recommended to use DevOps best practices and automate deployments using a CI/CD pipeline that integrates with your source control to help speed up deployment time. 68 | 69 | ### Data Storage 70 | This logging framework sends potentially sensitive log data to your various logging targets. Be sure to protect your sensitive data in your logging targets such as ELK, Splunk, and other logging systems. 71 | Notes: 72 | * Customer data can be stored in the logging target system such as ELK, Splunk, or other target logging system. 73 | * Be sure to protect your sensitive logging information where you send your logs 74 | 75 | #### Storing Data At Rest 76 | Be sure to follow best practices for storing your data at rest. With the Serverless Logging Framework, you decide where to send your logs. If your logs are sent to Amazon S3 for example, you can encrypt the data at rest using Amazon S3 server-side encryption. You can also encrypt the log data before it is sent out using your own encryption key. 77 | 78 | Make sure any stored data and any backup data is encrypted at rest to protect your sensitive logging data. 79 | 80 | ### Log High Availability 81 | Once again, you can choose where to send your logs using the Serverless Logging Framework. No matter where you send your logs, if your logs are critical to your business continuity, create a highly available environment for your logs. For example, if you deploy ELK on Amazon EC2, deploy ELK in a clustered environment on multiple EC2's across AZ's behind a load balancer so that you can maximize uptime and the availability of your logging data. If you send your logs to Amazon RDS, you can create an RDS instance in a multi-AZ configuration when creating your RDS instance. 82 | 83 | #### Deployment Configurations 84 | If you choose to send your logs to AWS, you have 3 options for storing your logs: 85 | 1. Single-AZ 86 | 1. Good for low cost environments that can go down without impacting the business 87 | 2. Multi-AZ 88 | 2. Best for data storage that needs to be highly available within a single region 89 | 3. Multi-region 90 | 3. Best for data storage that needs to be highly available across regions so the data can be close to the regions where it is accessed 91 | 92 | #### Backup and Recovery 93 | Your logs can be sent to any system of your choosing using the Serverless Logging Framework. Whatever logging target you choose, ensure that you have the ability to set Recovery Time Objectives (RTO) and Recovery Point Objectives (RPO). Without proper architecture and data storage high availability and backup, your logging data could be lost in the event of an AWS region going down, an AWS AZ going down, an EC2 instance interruption, and many other scenarios that can affect your logging data. 94 | 95 | To combat this, we recommend setting an RTO of 12 hours and an RPO of 1 hour. Both of these might depend on your business use case and can be tailored depending on your data impact. For example, recovering your data up to 1 hour in the past may be too aggressive, or it may not be good enough if your business relies on the data, so you can increase or decrease the RTO and RPO accordingly. 96 | 97 | To hit your RTO and RPO, ensure that your data is backed up automatically. In AWS, you can do this on EBS volumes attached to EC2, or on Amazon RDS instances. That way, if you are backing up your data and an instance fails, a region goes down, or something else occurs that affects your data, you will be able to restore from a backup. 98 | 99 | This also leads to a proper DR plan for your logs. Ensure you have a good plan in place in the event catastrophic failure occurs. For example, you can use another AZ in AWS if a single AZ goes down. 100 | 101 | ### Keys 102 | No keys are necessary for deploying this logging framework, although you may use a key to connect to your desired logging target. Be sure to follow AWS best practices to secure your sensitive keys by using a secure key store such as Secrets Manager. 103 | 104 | ### Upgrades 105 | If you use this logging framework as recommended as a dependency in your applications and you reference this source repository, you can upgrade in 2 ways: 106 | 1. Reference this GitHub repository without referencing a specific commit. Then you can specify when you want your dependency management system to retrieve the latest updates from the master branch of this repository 107 | 2. Reference a specific commit in this repository, and manually change the commit reference when you want to upgrade 108 | 109 | ### Managing AWS Service Limits 110 | If you install this Serverless Logging Framework on AWS Lambda, ensure that you are familiar with the AWS Lambda service limits. The same applies for logging targets, such as SQS. You can find the AWS service limits for each service [here](https://docs.aws.amazon.com/general/latest/gr/aws-service-information.html). Be sure to include throttling in your applications if you will hit the upper boundaries of the AWS services limits. 111 | 112 | ## Support 113 | Feel free to make feature requests and open issues if you find any bugs in the Serverless Logging Framework. Also, we would love if you helped us support the Serverless Logging Framework by contributing. With that in mind, we cannot promise any support SLA, but will do our best to support our community of loggers! 114 | 115 | ## Current Features 116 | 117 | `bc-serverless-logging-framework` currently supports the following features: 118 | 119 | - Support for sending logs to: 120 | - SQS 121 | - Console 122 | - HTTP 123 | - Custom-defined destinations 124 | - Default log levels, with the ability to define custom ones 125 | - Add default properties to every log, both static properties and dynamic ones 126 | - The ability to wait for all asynchronous logs to finish sending, which is important for most serverless functions. 127 | - The ability to create a logger based off of an existing logger (e.g. clone a logger), and add new properties to it 128 | 129 | ## Future features 130 | 131 | - Support for more destinations, such as logging to one or multiple files 132 | - Retries with exponential backoff when saving to external endpoints such as HTTP or SQS 133 | - Batching logs when saving to external endpoints such as HTTP or SQS 134 | - Integration with streaming services such as AWS Kinesis and Kafka 135 | 136 | ## Standard Usage 137 | 138 | The easiest way to get started with `bc-serverless-logging-framework` is to simply create a logger! Use `bcLogger.create` to get going: 139 | 140 | `logger.js` 141 | 142 | ```js 143 | import { bcLogger } from 'bc-serverless-logging-framework' 144 | 145 | export const logger = bcLogger.create({ 146 | // Designate default props to add to each log 147 | props: { 148 | appName: 'my-special-app', 149 | timestamp: () => new Date().toISOString() 150 | }, 151 | 152 | transports: [ 153 | // Log info-level logs and above to the console. 154 | { 155 | level: bcLogger.levels.info, 156 | destination: bcLogger.destinations.consoleLog() 157 | }, 158 | 159 | // Log error-level logs to some API 160 | { 161 | level: bcLogger.levels.error, 162 | destination: bcLogger.destinations.http({ 163 | url: 'https://my-elk-stack.com', 164 | method: 'POST', 165 | headers: { 166 | 'x-api-key': process.env.API_KEY 167 | } 168 | }) 169 | } 170 | ] 171 | }) 172 | ``` 173 | 174 | `index.js` 175 | 176 | ```js 177 | import { logger } from './logger.js' 178 | 179 | /* 180 | 181 | Logs nothing to the console, because the logger has no transports logging at the debug level. 182 | 183 | */ 184 | logger.debug('this will not go anywhere.') 185 | 186 | /* 187 | 188 | logs the following json to the console: 189 | { 190 | "level": "info", 191 | "message": "test info message", 192 | "appName": "my-special-app", 193 | "timestamp": "" 194 | } 195 | 196 | */ 197 | logger.info('test info message') 198 | 199 | /* 200 | 201 | Logs the following json to the console: 202 | { 203 | "level": "warn", 204 | "message": "A warning occurred. View warning body for more details", 205 | "appName": "my-special-app", 206 | "timestamp": "", 207 | "warning": { 208 | "code": "MAX_EMAIL_THRESHOLD_90", 209 | "message": "Max email threshold almost reached -- 90% of free tier used this month" 210 | } 211 | } 212 | 213 | */ 214 | logger.warn('A warning occurred. View warning body for more details', { 215 | warning: { 216 | code: 'MAX_EMAIL_THRESHOLD_90', 217 | message: 218 | 'Max email threshold almost reached -- 90% of free tier used this month' 219 | } 220 | }) 221 | 222 | /* 223 | 224 | Logs the following JSON to the console and also sends it in a POST request to https://my-elk-stack.com: 225 | 226 | { 227 | "level": "error", 228 | "message": "Error occurred in app", 229 | "appName": "my-special-app", 230 | "timestamp": "", 231 | "error": { 232 | "name": "BadError", 233 | "message": "A bad error occurred", 234 | "stack": "" 235 | } 236 | } 237 | 238 | */ 239 | logger.error('Error occurred in app', new BadError('A bad error occurred')) 240 | 241 | /* 242 | 243 | Logs don't have to include a string argument -- they can simply contain an error or regular object. 244 | 245 | logger.critical() in this case will log the following JSON to the console 246 | and also send it in a POST request to https://my-elk-stack.com: 247 | 248 | { 249 | "level": "critical", 250 | "appName": "my-special-app", 251 | "timestamp": "", 252 | "error": { 253 | "name": "AppExplosionError", 254 | "message": "A very terrible error occurred.", 255 | "stack": "" 256 | } 257 | } 258 | 259 | */ 260 | logger.critical(new AppExplosionError('A very terrible error occurred.')) 261 | ``` 262 | 263 | ## Serverless Usage: AWS Lambda 264 | 265 | Using in AWS Lambda is very similar to the above code with one caveat: if any logs result in asynchronous promises (e.g. sending logs to an HTTP or SQS destination), it is very important to use `logger.flush()` at the end of the lambda handler. 266 | 267 | Example `handler.js`: 268 | 269 | ```js 270 | import { logger } from './logger.js' 271 | 272 | export const handler = async (event) => { 273 | try { 274 | // Example log. This will send to an HTTP endpoint 275 | logger.info('Starting process...') 276 | 277 | // Run code 278 | // ... 279 | } catch (err) { 280 | // Handle errors 281 | // ... 282 | } finally { 283 | // This will ensure all logs arrive to the HTTP endpoint before exiting the function. 284 | await logger.flush() 285 | } 286 | } 287 | ``` 288 | 289 | **Note**: It's very important to use `await` in the example above. 290 | 291 | This will work: 292 | 293 | ```js 294 | await logger.flush() 295 | ``` 296 | 297 | This will **not** work: 298 | 299 | ```js 300 | logger.flush() 301 | ``` 302 | 303 | ## bc-serverless-logging-framework Destinations 304 | 305 | `bc-serverless-logging-framework` contains the following pre-defined destinations: 306 | 307 | - `http` 308 | - `sqs` 309 | - `consoleLog` 310 | 311 | More destinations are being planned to implement, and `bc-serverless-logging-framework` is structured with ease of new destination implementation in mind. 312 | 313 | ## bc-serverless-logging-framework Transports 314 | 315 | `bc-serverless-logging-framework` uses the concept of transports in order to send log messages to one or more destinations. 316 | 317 | A transport contains the following properties: 318 | 319 | - `destination`: a `bc-serverless-logging-framework` destination (see: above). Required unless a `handler` property is defined. 320 | - `handler`: a custom function for handling a `log` object. Required if a `destination` is not defined. 321 | - `level`: Optional property to defined which levels the transport will run for. When specifying a level, the transport will run for the specified levels and all levels above it. For example, if specifying `bcLogger.levels.warn` as the transport level, the transport will run for `logger.warn()`, `logger.error()`, and `logger.critical()`. If not specified, the log will log at all levels. 322 | 323 | ### bc-serverless-logging-framework Transports Example 324 | 325 | ```js 326 | const logger = bcLogger.create({ 327 | transports: [ 328 | // Log to sqs at warn-level and above 329 | { 330 | level: bcLogger.levels.warn, 331 | destination: bcLogger.destinations.sqs({ 332 | queueUrl: 'http://example-queue.com' 333 | }) 334 | }, 335 | 336 | // Log to an http endpoint for all log levels 337 | { 338 | destination: bcLogger.destinations.http({ 339 | url: 'http://my-log-endpoint.com', 340 | method: 'POST' 341 | }) 342 | }, 343 | 344 | // Log to the console for info level and above 345 | { 346 | level: bcLogger.levels.info, 347 | destination: bcLogger.destinations.consoleLog() 348 | }, 349 | 350 | // Run a custom log handler 351 | { 352 | level: bcLogger.levels.critical, 353 | handler(log) { 354 | console.error('CRITICAL ERROR!') 355 | console.error(log) 356 | } 357 | } 358 | ] 359 | }) 360 | ``` 361 | 362 | ## Default Logger props 363 | 364 | A common use case for logging is to have default properties in each log message. For example, including a `project`, `function`, and/or `jobId` in your logs to add to traceability. Or, adding a timestamp to each log. 365 | 366 | `bc-serverless-logging-framework` allows default props to be added easily both when creating the logger and after the logger has already been created. 367 | 368 | Example setting default props: 369 | 370 | ```js 371 | import { v4 as uuidv4 } from 'uuid' 372 | const jobId = uuidv4() 373 | 374 | const logger = bcLogger.create({ 375 | props: { 376 | project: 'my-example-project', 377 | jobId 378 | } 379 | }) 380 | ``` 381 | 382 | ## Computed props 383 | 384 | `bc-serverless-logging-framework` allows for computed props as well, represented as functions that have access to the properties within the log object. A common computed property to add to a logger is a `timestamp()` function, but they can also be used to format specific properties or make complicated computations on log property values. 385 | 386 | Example: 387 | 388 | ```js 389 | import { v4 as uuidv4 } from 'uuid' 390 | const jobId = uuidv4() 391 | 392 | const logger = bcLogger.create({ 393 | props: { 394 | project: 'my-example-project', 395 | jobId 396 | }, 397 | computed: { 398 | // Add a timestamp to every log 399 | timestamp: () => new Date().toISOString() 400 | 401 | // Format success property to be 1 or 0 402 | success: log => log.success ? 1 : 0 403 | 404 | // Attach flag if error occurred 405 | errorOccurred(log) { 406 | if (log.level === bcLogger.levels.error || log.level === bcLogger.levels.critical) { 407 | return true 408 | } 409 | } 410 | } 411 | }) 412 | 413 | logger.info('Things are going well.', { success: true }) 414 | // -> { 415 | // "level": "info", 416 | // "message": "Things are going well.", 417 | // "timestamp": "", 418 | // "success": 1 419 | // } 420 | 421 | logger.error('An error occurred.', { success: false }) 422 | // -> { 423 | // "level": "error", 424 | // "message": "An error occurred.", 425 | // "timestamp": "", 426 | // "success": 0, 427 | // "errorOccurred": true 428 | // } 429 | ``` 430 | 431 | ## Log Levels 432 | 433 | #### Default log levels 434 | 435 | `bc-serverless-logging-framework` uses the following log levels by default: 436 | 437 | - `debug` 438 | - `info` 439 | - `warn` 440 | - `error` 441 | - `critical` 442 | 443 | When creating a logger, a function is added for each level and can be used as so: 444 | 445 | ```js 446 | const logger = bcLogger.create({ ... }) 447 | 448 | logger.debug('This is a debug message') 449 | logger.info('This is an info message') 450 | logger.warn('This is a warn message') 451 | // etc. 452 | ``` 453 | 454 | All default log levels are accessible directly in the `bcLogger.levels` object. E.g., `bcLogger.levels.info` is set to "info", `bcLogger.levels.error` is set to "error", etc. 455 | 456 | Although default log levels are not expected to change, it is recommended to use these predefined strings if referencing default log levels in your code. 457 | 458 | #### Custom log levels 459 | 460 | `bc-serverless-logging-framework` can also use custom log levels by passing them into the `bcLogger.create()` function: 461 | 462 | ```js 463 | const logger = bcLogger.create({ 464 | levels: [ 465 | bcLogger.levels.info, 466 | 'custom_level', 467 | bcLogger.levels.error, 468 | bcLogger.levels.critical, 469 | 'custom_level_2' 470 | ] 471 | }) 472 | 473 | /* 474 | Results in the following functions defined in logger: 475 | 476 | logger.info() 477 | logger.custom_level() 478 | logger.error() 479 | logger.critical() 480 | logger.custom_level_2() 481 | */ 482 | ``` 483 | 484 | ## Child Loggers 485 | 486 | Sometimes it can make sense to make a new logger based on a parent logger. With `bc-serverless-logging-framework` it's possible to do just that. Simply create a top-level logger and then call `logger.child()` to get a clone of the logger. 487 | 488 | You can pass a configuration into the `child()` function to result in a logger with a configuration merged with the parent's. 489 | 490 | Example: 491 | 492 | ```js 493 | const logger = bcLogger.create({ 494 | props: { 495 | loggerName: 'parent-logger', 496 | test: true 497 | }, 498 | computed: { 499 | timestamp: () => new Date().toISOString() 500 | }, 501 | transports: [ 502 | { 503 | destination: bcLogger.destinations.consoleLog() 504 | } 505 | ] 506 | }) 507 | 508 | logger.info('This is from the parent logger!') 509 | // -> { 510 | // "level": "info", 511 | // "message": "This is from the parent logger!", 512 | // "loggerName": "parent-logger", 513 | // "test": true, 514 | // "timestamp": "2020-09-21T16:39:48.945Z" 515 | // } 516 | 517 | const childLogger = logger.child({ 518 | props: { 519 | loggerName: 'child-logger', 520 | isChild: true 521 | }, 522 | computed: { 523 | randomDecimal: () => Math.random() 524 | } 525 | }) 526 | 527 | childLogger.info('This is from the child logger!') 528 | // -> { 529 | // "level": "info", 530 | // "message": "This is from the child logger!", 531 | // "loggerName": "child-logger", 532 | // "test": true, 533 | // "isChild": true, 534 | // "timestamp": "2020-09-21T16:39:48.948Z", 535 | // "randomDecimal": 0.9632872942532387 536 | // } 537 | ``` 538 | 539 | As a second argument, `child()` takes in a `BCLoggerChildOptions` argument. This argument is a configuration with the following properties: 540 | 541 | | Property | Details | 542 | | ------------------- | -------------------------------------------- | 543 | | `replaceTransports` | Replace the parent logger's transports array | 544 | | `replaceProps` | Replace the parent logger's props object | 545 | | `replaceComputed` | Replace the parent logger's computed object | 546 | 547 | 548 | ## Potential Errors 549 | 550 | The following errors can commonly happen when configuring the logging library: 551 | 552 | ### No transports configured 553 | 554 | If no transports are configured, the library will log a warning. The logging framework cannot function if no transports are set up. 555 | 556 | For example, for the following code: 557 | 558 | ``` 559 | export const logger = bcLogger.create({ 560 | transports: [] 561 | }) 562 | ``` 563 | 564 | The logging framework will log "Warning: no transports added to bcLogger. Logging functionality is disabled." as a warning to the console. 565 | 566 | ### Configuration issue with Transport Destination 567 | 568 | A destination requires configuration. For example, if logging to SQS, an SQS URL and other SQS-specific configurations must be provided. If some or all of the configurations are missing, a `LoggingFrameworkDestinationConfigError` is thrown by the `bcLogger.create()` function. 569 | 570 | ### Error sending message to a destination 571 | 572 | Should there be an issue sending a message to a destination, an error message is logged to the console. For example, if a log fails to send to an HTTP endpoint, the following message is logged: 573 | 574 | > Error occurred sending log message to endpoint: {endpoint url}. {JSON containing log object and the error that occurred} 575 | ## Architecture 576 | ### Traditional Logging Architecture 577 | Traditional logging architecture might look like the below diagram. It is very simple and straightforward. 578 | ![image](https://user-images.githubusercontent.com/5343588/109753355-57eb7f80-7b9f-11eb-8e3c-2357017edd32.png) 579 | ### Serverless Logging Architecture 580 | The architecture of serverless and microservices logging looks like this, and this is exactly how the Big Compass Serverless Logging Framework can be installed on the various serverless services to help standardize and send logs to a logging target. 581 | Notes: 582 | * Customer data can be stored in the logging target system such as ELK, Splunk, or other target logging system. 583 | * Be sure to protect your sensitive logging information where you send your logs 584 | ![image](https://user-images.githubusercontent.com/5343588/109753449-82d5d380-7b9f-11eb-8f2d-15072da67aee.png) 585 | Specifically, it is recommended that the Serverless Logging Framework is installed as a Lambda Layer for deploying to AWS Lambda. 586 | ![Logging Architecture - SLF Architecture (2)](https://user-images.githubusercontent.com/5343588/129807570-04f292b7-baaf-43f1-b7c1-240ee4d3082f.png) 587 | 588 | ## AWS Best Practices 589 | * Follow [IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) when deploying to AWS 590 | * Do not use the root user for any deployments to AWS 591 | * Follow IAM best practices for providing least privilege access to AWS users and roles assumed by services 592 | 593 | ## Support 594 | This framework is open source, developed by Big Compass and supported by the community. Feel free to make feature requests for support or reach out to Big Compass for logging help. 595 | 596 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | testMatch: [ 4 | '**/__tests__/**/*.+(ts|tsx|js)', 5 | '**/?(*.)+(spec|test).+(ts|tsx|js)' 6 | ], 7 | transform: { 8 | '^.+\\.(ts|tsx)$': 'ts-jest' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework.d.ts: -------------------------------------------------------------------------------- 1 | import { Levels } from './bc-serverless-logging-framework/enums/Levels'; 2 | import { Logger, LagerConfiguration } from './bc-serverless-logging-framework/types'; 3 | export declare const bcLogger: { 4 | destinations: { 5 | sqs: (config: import("./bc-serverless-logging-framework/types").SQSDestinationConfig) => import("./bc-serverless-logging-framework/types").Destination; 6 | http: (config: import("axios").AxiosRequestConfig) => import("./bc-serverless-logging-framework/types").Destination; 7 | consoleLog: (config?: import("./bc-serverless-logging-framework/types").ConsoleLogDestinationConfig | undefined) => import("./bc-serverless-logging-framework/types").Destination; 8 | }; 9 | levels: typeof Levels; 10 | /** 11 | * Return a logger object based on configuration 12 | * 13 | */ 14 | create({ levels, props, computed, transports, errorKey, propsRoot }?: LagerConfiguration): Logger; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.bcLogger = void 0; 7 | const Levels_1 = require("./bc-serverless-logging-framework/enums/Levels"); 8 | const destinations_1 = require("./bc-serverless-logging-framework/destinations"); 9 | const createLog_1 = require("./bc-serverless-logging-framework/util/createLog"); 10 | const transport_1 = require("./bc-serverless-logging-framework/util/transport"); 11 | const lodash_set_1 = __importDefault(require("lodash.set")); 12 | // Shim for promise.allSettled 13 | require('promise.allsettled').shim(); 14 | const promises = []; 15 | exports.bcLogger = { 16 | destinations: destinations_1.destinations, 17 | levels: Levels_1.Levels, 18 | /** 19 | * Return a logger object based on configuration 20 | * 21 | */ 22 | create({ levels, props, computed, transports, errorKey, propsRoot } = {}) { 23 | let configuredProperties = props ? Object.assign({}, props) : {}; 24 | // Set defaults if not provided 25 | if (!(levels === null || levels === void 0 ? void 0 : levels.length)) { 26 | levels = Object.values(exports.bcLogger.levels); 27 | } 28 | if (!errorKey) { 29 | errorKey = 'error'; 30 | } 31 | if (!(transports === null || transports === void 0 ? void 0 : transports.length)) { 32 | console.warn('Warning: no transports added to bcLogger. Logging functionality is disabled'); 33 | } 34 | if (props && propsRoot) { 35 | const rootedProps = {}; 36 | lodash_set_1.default(rootedProps, propsRoot, props); 37 | props = rootedProps; 38 | } 39 | // Set level index onto transport. Log a warning if using a level that doesn't exist 40 | transports === null || transports === void 0 ? void 0 : transports.forEach((transport) => { 41 | transport_1.setupTransport(transport, levels); 42 | }); 43 | // Set up logger 44 | const logger = { 45 | // Function to set new props after creating a logger 46 | props(newProps) { 47 | if (propsRoot) { 48 | lodash_set_1.default(props, propsRoot, Object.assign(Object.assign({}, props === null || props === void 0 ? void 0 : props.propsRoot), newProps)); 49 | } 50 | else { 51 | props = Object.assign(Object.assign({}, props), newProps); 52 | } 53 | configuredProperties = Object.assign(Object.assign({}, configuredProperties), newProps); 54 | return this; 55 | }, 56 | // Function to return a new logger inheriting from this one 57 | child(childConfig, options) { 58 | var _a, _b; 59 | if (!childConfig) { 60 | childConfig = {}; 61 | } 62 | const conf = {}; 63 | conf.levels = (_a = childConfig.levels) !== null && _a !== void 0 ? _a : levels; 64 | conf.propsRoot = (_b = childConfig.propsRoot) !== null && _b !== void 0 ? _b : propsRoot; 65 | conf.props = configuredProperties; 66 | if (childConfig.props) { 67 | conf.props = (options === null || options === void 0 ? void 0 : options.replaceProps) ? childConfig.props 68 | : Object.assign(Object.assign({}, configuredProperties), childConfig.props); 69 | } 70 | if (computed && childConfig.computed) { 71 | conf.computed = (options === null || options === void 0 ? void 0 : options.replaceComputed) ? childConfig.computed 72 | : Object.assign(Object.assign({}, computed), childConfig.computed); 73 | } 74 | else if (childConfig.computed) { 75 | conf.computed = childConfig.computed; 76 | } 77 | else if (computed) { 78 | conf.computed = computed; 79 | } 80 | if (transports && childConfig.transports) { 81 | conf.transports = (options === null || options === void 0 ? void 0 : options.replaceTransports) ? childConfig.transports 82 | : [...transports, ...childConfig.transports]; 83 | } 84 | else if (transports) { 85 | conf.transports = transports; 86 | } 87 | else if (childConfig.transports) { 88 | conf.transports = childConfig.transports; 89 | } 90 | return exports.bcLogger.create(conf); 91 | }, 92 | /** 93 | * Wait for transport promises to finish 94 | */ 95 | flush() { 96 | return Promise.allSettled(promises); 97 | } 98 | }; 99 | // Set up logger to run transports at each log level 100 | levels.forEach((level, i) => { 101 | logger[level] = (...args) => { 102 | // Create the log object based on arguments/logger props 103 | const log = createLog_1.createLog(level, args, props, computed, propsRoot, errorKey); 104 | // Run transports for level and push any promises to the promises array 105 | const transportPromises = transport_1.runTransports(log, i, transports); 106 | promises.push(...transportPromises); 107 | }; 108 | }); 109 | return logger; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const bc_serverless_logging_framework_1 = require("./bc-serverless-logging-framework"); 13 | const spies = { 14 | console: { 15 | info: jest.spyOn(console, 'info').mockImplementation(), 16 | warn: jest.spyOn(console, 'warn').mockImplementation(), 17 | error: jest.spyOn(console, 'error').mockImplementation() 18 | } 19 | }; 20 | beforeEach(() => { 21 | spies.console.info.mockClear(); 22 | spies.console.warn.mockClear(); 23 | spies.console.error.mockClear(); 24 | }); 25 | it('creates a logger with default levels as functions', () => { 26 | const logger = bc_serverless_logging_framework_1.bcLogger.create(); 27 | for (let level of Object.values(bc_serverless_logging_framework_1.bcLogger.levels)) { 28 | expect(typeof logger[level]).toBe('function'); 29 | } 30 | }); 31 | it('create a logger with specified levels when provided', () => { 32 | const logger = bc_serverless_logging_framework_1.bcLogger.create({ 33 | levels: ['test_level_1', 'test_level_2', bc_serverless_logging_framework_1.bcLogger.levels.info] 34 | }); 35 | expect(typeof logger.test_level_1).toBe('function'); 36 | expect(typeof logger.test_level_2).toBe('function'); 37 | expect(typeof logger[bc_serverless_logging_framework_1.bcLogger.levels.info]).toBe('function'); 38 | }); 39 | it('logs with default props when specified', () => { 40 | // Setup 41 | const logger = bc_serverless_logging_framework_1.bcLogger.create({ 42 | props: { 43 | testprop1: 'abc', 44 | testprop2: 123, 45 | testprop3: { 46 | test: true 47 | }, 48 | testprop4(log) { 49 | return `Log message contains the following text: ${log.message}. Level: ${log.level}`; 50 | } 51 | }, 52 | transports: [ 53 | { 54 | destination: bc_serverless_logging_framework_1.bcLogger.destinations.consoleLog() 55 | } 56 | ] 57 | }); 58 | // Run 59 | logger.info('test message 123'); 60 | // Assert 61 | const [logData] = spies.console.info.mock.calls[0]; 62 | expect(JSON.parse(logData)).toMatchObject({ 63 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 64 | message: 'test message 123', 65 | testprop1: 'abc', 66 | testprop2: 123, 67 | testprop3: { 68 | test: true 69 | }, 70 | testprop4: 'Log message contains the following text: test message 123. Level: info' 71 | }); 72 | }); 73 | it('sets new default props onto the logger using the props() method', () => { 74 | // Setup 75 | const logger = bc_serverless_logging_framework_1.bcLogger.create({ 76 | transports: [ 77 | { 78 | destination: bc_serverless_logging_framework_1.bcLogger.destinations.consoleLog() 79 | } 80 | ] 81 | }); 82 | // Run 83 | logger.props({ testprop: '1234' }); 84 | logger.info('test message'); 85 | // Assert 86 | const [logData] = spies.console.info.mock.calls[0]; 87 | expect(JSON.parse(logData)).toMatchObject({ 88 | message: 'test message', 89 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 90 | testprop: '1234' 91 | }); 92 | }); 93 | it('utilizes transport level correctly', () => { 94 | // Setup 95 | const logger = bc_serverless_logging_framework_1.bcLogger.create({ 96 | transports: [ 97 | { 98 | level: bc_serverless_logging_framework_1.bcLogger.levels.warn, 99 | destination: bc_serverless_logging_framework_1.bcLogger.destinations.consoleLog() 100 | } 101 | ] 102 | }); 103 | // Run 104 | logger.info('test info message'); 105 | logger.warn('test warn message'); 106 | logger.error('test error message'); 107 | // Assert 108 | const [warnLogData] = spies.console.warn.mock.calls[0]; 109 | expect(JSON.parse(warnLogData)).toEqual({ 110 | message: 'test warn message', 111 | level: bc_serverless_logging_framework_1.bcLogger.levels.warn 112 | }); 113 | const [errorLogData] = spies.console.error.mock.calls[0]; 114 | expect(JSON.parse(errorLogData)).toEqual({ 115 | message: 'test error message', 116 | level: bc_serverless_logging_framework_1.bcLogger.levels.error 117 | }); 118 | expect(spies.console.info).not.toHaveBeenCalled(); 119 | }); 120 | it('logs a warning if an invalid level is used for a transport', () => { 121 | bc_serverless_logging_framework_1.bcLogger.create({ 122 | transports: [ 123 | { 124 | level: 'dummy_level', 125 | destination: bc_serverless_logging_framework_1.bcLogger.destinations.consoleLog() 126 | } 127 | ] 128 | }); 129 | expect(spies.console.warn).toHaveBeenCalledWith(`Invalid level detected in transport: dummy_level. This transport will run for all log levels. Valid levels: ${Object.values(bc_serverless_logging_framework_1.bcLogger.levels)}`); 130 | }); 131 | it('handles rejected promises gracefully when running flush()', () => __awaiter(void 0, void 0, void 0, function* () { 132 | const logger = bc_serverless_logging_framework_1.bcLogger.create({ 133 | transports: [ 134 | { 135 | handler: (log) => (log.fail ? Promise.reject() : Promise.resolve()) 136 | } 137 | ] 138 | }); 139 | logger.info('test message'); 140 | logger.error('this message will fail to send.', { fail: true }); 141 | const results = yield logger.flush(); 142 | expect(results.find((result) => result.status === 'rejected')).toBeDefined(); 143 | })); 144 | it('loads errorKey correctly', () => { 145 | const logger = bc_serverless_logging_framework_1.bcLogger.create({ 146 | transports: [ 147 | { 148 | destination: bc_serverless_logging_framework_1.bcLogger.destinations.consoleLog() 149 | } 150 | ], 151 | errorKey: 'my_error_object' 152 | }); 153 | const err = new Error('test error'); 154 | logger.error('An error occurred', err); 155 | const [errorLogData] = spies.console.error.mock.calls[0]; 156 | expect(JSON.parse(errorLogData)).toEqual({ 157 | level: bc_serverless_logging_framework_1.bcLogger.levels.error, 158 | message: 'An error occurred', 159 | my_error_object: { 160 | name: err.name, 161 | message: err.message, 162 | stack: err.stack 163 | } 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/constants.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * When a transport levelNumber is set to this, 3 | * the transport will run at all levels 4 | */ 5 | export declare const TRANSPORT_LEVEL_ALL: number; 6 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.TRANSPORT_LEVEL_ALL = void 0; 4 | /** 5 | * When a transport levelNumber is set to this, 6 | * the transport will run at all levels 7 | */ 8 | exports.TRANSPORT_LEVEL_ALL = -1; 9 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/consoleLog.d.ts: -------------------------------------------------------------------------------- 1 | import { Destination, ConsoleLogDestinationConfig } from '../types'; 2 | export declare const consoleLog: (config?: ConsoleLogDestinationConfig | undefined) => Destination; 3 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/consoleLog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.consoleLog = void 0; 4 | const ConsoleLevels_1 = require("../enums/ConsoleLevels"); 5 | const Levels_1 = require("../enums/Levels"); 6 | const decircularize_1 = require("../util/decircularize"); 7 | exports.consoleLog = (config) => ({ 8 | send(log) { 9 | if (!log) { 10 | return; 11 | } 12 | // Retrieve the log function 13 | let logFn; 14 | let level = ''; 15 | if (log.consoleLevel || (config === null || config === void 0 ? void 0 : config.consoleLevel)) { 16 | level = log.consoleLevel || (config === null || config === void 0 ? void 0 : config.consoleLevel); 17 | } 18 | else if (log.level) { 19 | level = log.level; 20 | } 21 | switch (level) { 22 | case Levels_1.Levels.debug: 23 | case ConsoleLevels_1.ConsoleLevels.debug: 24 | logFn = console.debug; 25 | break; 26 | case Levels_1.Levels.info: 27 | case ConsoleLevels_1.ConsoleLevels.info: 28 | logFn = console.info; 29 | break; 30 | case Levels_1.Levels.warn: 31 | case ConsoleLevels_1.ConsoleLevels.warn: 32 | logFn = console.warn; 33 | break; 34 | case Levels_1.Levels.error: 35 | case Levels_1.Levels.critical: 36 | case ConsoleLevels_1.ConsoleLevels.error: 37 | logFn = console.error; 38 | break; 39 | default: 40 | logFn = console.log; 41 | } 42 | // Log to the console 43 | logFn(JSON.stringify(decircularize_1.decircularize(log))); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/http.d.ts: -------------------------------------------------------------------------------- 1 | import { Destination } from '../types'; 2 | import { AxiosRequestConfig } from 'axios'; 3 | export declare const http: (config: AxiosRequestConfig) => Destination; 4 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/http.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.http = void 0; 7 | /** 8 | * Destination for sending logs to an HTTP endpoint 9 | */ 10 | const LoggingFrameworkDestinationConfigError_1 = require("../errors/LoggingFrameworkDestinationConfigError"); 11 | const axios_1 = __importDefault(require("axios")); 12 | exports.http = (config) => { 13 | if (!config) { 14 | throw new LoggingFrameworkDestinationConfigError_1.LoggingFrameworkDestinationConfigError('Axios configuration is required for http destination'); 15 | } 16 | return { 17 | send(log) { 18 | config.data = log; 19 | return axios_1.default(config).catch((error) => { 20 | console.error(`Error occurred sending log message to endpoint: ${config.url}`, { log, error }); 21 | }); 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare const destinations: { 2 | sqs: (config: import("../types").SQSDestinationConfig) => import("../types").Destination; 3 | http: (config: import("axios").AxiosRequestConfig) => import("../types").Destination; 4 | consoleLog: (config?: import("../types").ConsoleLogDestinationConfig | undefined) => import("../types").Destination; 5 | }; 6 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.destinations = void 0; 4 | const sqs_1 = require("./sqs"); 5 | const http_1 = require("./http"); 6 | const consoleLog_1 = require("./consoleLog"); 7 | exports.destinations = { 8 | sqs: sqs_1.sqs, 9 | http: http_1.http, 10 | consoleLog: consoleLog_1.consoleLog 11 | }; 12 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/sqs.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Destination for sending directly to an SQS queue 3 | */ 4 | import { Destination, SQSDestinationConfig } from '../types'; 5 | export declare const sqs: (config: SQSDestinationConfig) => Destination; 6 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/sqs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Destination for sending directly to an SQS queue 4 | */ 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.sqs = void 0; 7 | const AWS = require('aws-sdk'); 8 | const LoggingFrameworkDestinationConfigError_1 = require("../errors/LoggingFrameworkDestinationConfigError"); 9 | const decircularize_1 = require("../util/decircularize"); 10 | exports.sqs = (config) => { 11 | if (!config) { 12 | throw new LoggingFrameworkDestinationConfigError_1.LoggingFrameworkDestinationConfigError('No SQS Destination config supplied'); 13 | } 14 | let { sqsOptions, queueUrl } = config; 15 | // Set url, throw error if not provided 16 | if (!queueUrl) { 17 | throw new LoggingFrameworkDestinationConfigError_1.LoggingFrameworkDestinationConfigError('SQS Queue URL is required.'); 18 | } 19 | // Setup default sqs options 20 | sqsOptions = sqsOptions || {}; 21 | if (!sqsOptions.apiVersion) { 22 | sqsOptions.apiVersion = '2012-11-05'; 23 | } 24 | if (!sqsOptions.region) { 25 | sqsOptions.region = 'us-east-1'; 26 | } 27 | const sqs = new AWS.SQS(sqsOptions); 28 | return { 29 | send(log) { 30 | return sqs 31 | .sendMessage({ 32 | MessageBody: JSON.stringify(decircularize_1.decircularize(log)), 33 | QueueUrl: queueUrl 34 | }) 35 | .promise() 36 | .catch((error) => { 37 | console.error('Error occurred sending log message to SQS.', { 38 | log, 39 | error 40 | }); 41 | }); 42 | } 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/tests/consoleLog.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/destinations/tests/consoleLog.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const consoleLog_1 = require("../consoleLog"); 4 | const ConsoleLevels_1 = require("../../enums/ConsoleLevels"); 5 | const bc_serverless_logging_framework_1 = require("../../../bc-serverless-logging-framework"); 6 | const destinations = { 7 | // Will log based on log level 8 | normal: consoleLog_1.consoleLog(), 9 | // Will log based on log level 10 | weird: consoleLog_1.consoleLog({ consoleLevel: null }), 11 | // Will always send debug logs 12 | debug: consoleLog_1.consoleLog({ consoleLevel: ConsoleLevels_1.ConsoleLevels.debug }), 13 | // Will always send info logs 14 | info: consoleLog_1.consoleLog({ consoleLevel: ConsoleLevels_1.ConsoleLevels.info }), 15 | // Will always send warn logs 16 | warn: consoleLog_1.consoleLog({ consoleLevel: ConsoleLevels_1.ConsoleLevels.warn }), 17 | // Will always send error logs 18 | error: consoleLog_1.consoleLog({ consoleLevel: ConsoleLevels_1.ConsoleLevels.error }) 19 | }; 20 | const spies = { 21 | console: { 22 | debug: jest.spyOn(console, 'debug').mockImplementation(), 23 | info: jest.spyOn(console, 'info').mockImplementation(), 24 | warn: jest.spyOn(console, 'warn').mockImplementation(), 25 | error: jest.spyOn(console, 'error').mockImplementation(), 26 | log: jest.spyOn(console, 'log').mockImplementation() 27 | } 28 | }; 29 | const assertCalls = (logFn) => { 30 | for (let key in spies.console) { 31 | const spy = spies.console[key]; 32 | const callsExpected = key === logFn ? 1 : 0; 33 | expect(spy).toHaveBeenCalledTimes(callsExpected); 34 | spy.mockClear(); 35 | } 36 | }; 37 | it('logs as expected for each bcLogger level', () => { 38 | destinations.normal.send({ 39 | level: bc_serverless_logging_framework_1.bcLogger.levels.debug, 40 | message: 'test message' 41 | }); 42 | assertCalls('debug'); 43 | destinations.normal.send({ 44 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 45 | message: 'test message' 46 | }); 47 | assertCalls('info'); 48 | destinations.normal.send({ 49 | level: bc_serverless_logging_framework_1.bcLogger.levels.warn, 50 | message: 'test message' 51 | }); 52 | assertCalls('warn'); 53 | destinations.normal.send({ 54 | level: bc_serverless_logging_framework_1.bcLogger.levels.error, 55 | message: 'test message' 56 | }); 57 | assertCalls('error'); 58 | destinations.normal.send({ 59 | level: bc_serverless_logging_framework_1.bcLogger.levels.critical, 60 | message: 'test message' 61 | }); 62 | assertCalls('error'); 63 | destinations.normal.send({ 64 | level: 'random_log_level', 65 | message: 'test message' 66 | }); 67 | assertCalls('log'); 68 | }); 69 | it('logs correctly using consoleLevel', () => { 70 | destinations.normal.send({ 71 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 72 | consoleLevel: 'error', 73 | message: 'test message' 74 | }); 75 | assertCalls('error'); 76 | destinations.weird.send({ 77 | level: bc_serverless_logging_framework_1.bcLogger.levels.warn, 78 | message: 'test message' 79 | }); 80 | assertCalls('warn'); 81 | destinations.debug.send({ 82 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 83 | consoleLevel: 'error', 84 | message: 'test message' 85 | }); 86 | assertCalls('error'); 87 | destinations.debug.send({ 88 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 89 | message: 'test message' 90 | }); 91 | assertCalls('debug'); 92 | destinations.info.send({ 93 | level: bc_serverless_logging_framework_1.bcLogger.levels.warn, 94 | message: 'test message' 95 | }); 96 | assertCalls('info'); 97 | destinations.warn.send({ 98 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 99 | message: 'test message' 100 | }); 101 | assertCalls('warn'); 102 | destinations.error.send({ 103 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 104 | message: 'test message' 105 | }); 106 | assertCalls('error'); 107 | destinations.debug.send({ 108 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 109 | consoleLevel: 'dumb_console_level', 110 | message: 'test message' 111 | }); 112 | assertCalls('log'); 113 | }); 114 | it('works gracefully for odd scenarios', () => { 115 | destinations.normal.send(); 116 | assertCalls(''); // expects no log to have happened 117 | destinations.normal.send({}); 118 | assertCalls('log'); 119 | destinations.debug.send({ 120 | consoleLevel: undefined 121 | }); 122 | assertCalls('debug'); 123 | destinations.weird.send({ 124 | consoleLevel: null 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/enums/ConsoleLevels.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum ConsoleLevels { 2 | debug = "debug", 3 | info = "info", 4 | warn = "warn", 5 | error = "error" 6 | } 7 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/enums/ConsoleLevels.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ConsoleLevels = void 0; 4 | /* eslint-disable no-unused-vars */ 5 | var ConsoleLevels; 6 | (function (ConsoleLevels) { 7 | ConsoleLevels["debug"] = "debug"; 8 | ConsoleLevels["info"] = "info"; 9 | ConsoleLevels["warn"] = "warn"; 10 | ConsoleLevels["error"] = "error"; 11 | })(ConsoleLevels = exports.ConsoleLevels || (exports.ConsoleLevels = {})); 12 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/enums/Levels.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum Levels { 2 | debug = "debug", 3 | info = "info", 4 | warn = "warn", 5 | error = "error", 6 | critical = "critical" 7 | } 8 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/enums/Levels.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Levels = void 0; 4 | /* eslint-disable no-unused-vars */ 5 | var Levels; 6 | (function (Levels) { 7 | Levels["debug"] = "debug"; 8 | Levels["info"] = "info"; 9 | Levels["warn"] = "warn"; 10 | Levels["error"] = "error"; 11 | Levels["critical"] = "critical"; 12 | })(Levels = exports.Levels || (exports.Levels = {})); 13 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/errors/DestinationConfigError.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error to throw when there is a problem 3 | * configuration a destination 4 | */ 5 | export declare class LoggingFrameworkDestinationConfigError extends Error { 6 | constructor(message: any); 7 | } 8 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/errors/DestinationConfigError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.LoggingFrameworkDestinationConfigError = void 0; 4 | /** 5 | * Error to throw when there is a problem 6 | * configuration a destination 7 | */ 8 | class LoggingFrameworkDestinationConfigError extends Error { 9 | constructor(message) { 10 | super(message); 11 | this.message = message; 12 | } 13 | } 14 | exports.LoggingFrameworkDestinationConfigError = LoggingFrameworkDestinationConfigError; 15 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/errors/LoggingFrameworkDestinationConfigError.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error to throw when there is a problem 3 | * configuration a destination 4 | */ 5 | export declare class LoggingFrameworkDestinationConfigError extends Error { 6 | constructor(message: any); 7 | } 8 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/errors/LoggingFrameworkDestinationConfigError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.LoggingFrameworkDestinationConfigError = void 0; 4 | /** 5 | * Error to throw when there is a problem 6 | * configuration a destination 7 | */ 8 | class LoggingFrameworkDestinationConfigError extends Error { 9 | constructor(message) { 10 | super(message); 11 | this.message = message; 12 | } 13 | } 14 | exports.LoggingFrameworkDestinationConfigError = LoggingFrameworkDestinationConfigError; 15 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/types.d.ts: -------------------------------------------------------------------------------- 1 | import * as SQS from 'aws-sdk/clients/sqs'; 2 | export interface LagerConfiguration { 3 | levels?: Array; 4 | props?: LogProps; 5 | propsRoot?: string; 6 | computed?: LogComputedProps; 7 | transports?: Array; 8 | errorKey?: string; 9 | } 10 | export interface LagerChildOptions { 11 | replaceTransports?: boolean; 12 | replaceProps?: boolean; 13 | replaceComputed?: boolean; 14 | } 15 | export interface Log { 16 | level?: string; 17 | message?: string; 18 | [x: string]: any; 19 | } 20 | export interface LogProps { 21 | [x: string]: any; 22 | } 23 | export interface LogComputedProps { 24 | [x: string]: Function; 25 | } 26 | export interface Logger { 27 | props: Function; 28 | flush: Function; 29 | [x: string]: Function; 30 | } 31 | export interface Transport { 32 | destination?: Destination; 33 | handler?: Function; 34 | level?: string; 35 | levelNumber?: number; 36 | } 37 | export interface Destination { 38 | send(log?: Log): void | Promise; 39 | } 40 | export interface ConsoleType { 41 | debug: Function; 42 | info: Function; 43 | warn: Function; 44 | error: Function; 45 | } 46 | export interface ConsoleLogDestinationConfig { 47 | consoleLevel?: string | null; 48 | } 49 | export interface SQSDestinationConfig { 50 | sqsOptions?: SQS.Types.ClientConfiguration; 51 | queueUrl: string; 52 | } 53 | export interface RunTransportsConfig { 54 | log: Log; 55 | level: string; 56 | transports: Array; 57 | } 58 | export interface HttpDestinationOptions { 59 | logProperty?: string; 60 | } 61 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/createLog.d.ts: -------------------------------------------------------------------------------- 1 | import { Log, LogProps, LogComputedProps } from '../types'; 2 | /** 3 | * Apply default log props to a provided log object 4 | * @param {Log} log - The log object to update 5 | * @param {LogProps} props - The log props object 6 | * @returns {Log} the resulting log object 7 | */ 8 | export declare const applyDefaultProps: (log: Log, props?: LogProps | undefined, propsRoot?: string | undefined) => Log; 9 | /** 10 | * Apply default log props to a provided log object 11 | * @param {Log} log - The log object to update 12 | * @param {LogComputedProps} computed - Object containing computed functions to run 13 | * @returns {Log} the resulting log object 14 | */ 15 | export declare const applyComputedProps: (log: Log, computed?: LogComputedProps | undefined, propsRoot?: string | undefined) => Log; 16 | /** 17 | * Initialize a log object with the provided log arguments 18 | * @param {string} level - the log level 19 | * @param {Array<*>} args - the log arguments 20 | * @param {string} errorKey - The name of the error property to add for an error argument 21 | * @returns {Log} The new log object 22 | */ 23 | export declare const initLog: (level: string, args: Array, errorKey?: string | undefined) => Log; 24 | /** 25 | * Create a new log object 26 | * @param {string} level - The log level 27 | * @param {Array<*>} args - The log arguments 28 | * @param {LogProps} logProps - The log props to apply to the log 29 | * @param {string} errorKey - The name of an error object to use in the log. Defaults to "error" 30 | */ 31 | export declare const createLog: (level: string, args: Array, logProps?: LogProps | undefined, computed?: LogComputedProps | undefined, propsRoot?: string | undefined, errorKey?: string | undefined) => Log; 32 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/createLog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.createLog = exports.initLog = exports.applyComputedProps = exports.applyDefaultProps = void 0; 7 | const lodash_get_1 = __importDefault(require("lodash.get")); 8 | const lodash_set_1 = __importDefault(require("lodash.set")); 9 | /** 10 | * Apply default log props to a provided log object 11 | * @param {Log} log - The log object to update 12 | * @param {LogProps} props - The log props object 13 | * @returns {Log} the resulting log object 14 | */ 15 | exports.applyDefaultProps = (log, props, propsRoot) => { 16 | if (props) { 17 | for (let [propName, prop] of Object.entries(props)) { 18 | // Reset prop name if propsRoot was passed in 19 | if (propsRoot) { 20 | propName = `${propsRoot}.${propName}`; 21 | } 22 | // Apply default props 23 | if (lodash_get_1.default(log, propName) === undefined) { 24 | if (typeof prop === 'function') { 25 | lodash_set_1.default(log, propName, prop(log)); 26 | } 27 | else { 28 | lodash_set_1.default(log, propName, prop); 29 | } 30 | } 31 | } 32 | } 33 | return log; 34 | }; 35 | /** 36 | * Apply default log props to a provided log object 37 | * @param {Log} log - The log object to update 38 | * @param {LogComputedProps} computed - Object containing computed functions to run 39 | * @returns {Log} the resulting log object 40 | */ 41 | exports.applyComputedProps = (log, computed, propsRoot) => { 42 | if (computed) { 43 | for (let [propName, fn] of Object.entries(computed)) { 44 | // Reset prop name if propsRoot was passed in 45 | if (propsRoot) { 46 | propName = `${propsRoot}.${propName}`; 47 | } 48 | // Apply computed props 49 | if (typeof fn === 'function') { 50 | lodash_set_1.default(log, propName, fn(log)); 51 | } 52 | } 53 | } 54 | return log; 55 | }; 56 | /** 57 | * Initialize a log object with the provided log arguments 58 | * @param {string} level - the log level 59 | * @param {Array<*>} args - the log arguments 60 | * @param {string} errorKey - The name of the error property to add for an error argument 61 | * @returns {Log} The new log object 62 | */ 63 | exports.initLog = (level, args, errorKey) => { 64 | const log = { level }; 65 | const meta = { 66 | hasError: false, 67 | hasMessage: false, 68 | hasObject: false 69 | }; 70 | if (!errorKey) { 71 | errorKey = 'error'; 72 | } 73 | // Load provided arguemnts into log object 74 | for (let arg of args) { 75 | if (arg instanceof Error && !meta.hasError) { 76 | // Errors are pushed to the errors array 77 | log[errorKey] = { 78 | message: arg.message, 79 | name: arg.name, 80 | stack: arg.stack 81 | }; 82 | meta.hasError = true; 83 | } 84 | else if (typeof arg === 'object' && !meta.hasObject) { 85 | // Object values are added to the log object 86 | Object.assign(log, arg); 87 | meta.hasObject = true; 88 | } 89 | else if (!meta.hasMessage) { 90 | // Primitive values are passed into the message property 91 | log.message = arg; 92 | meta.hasMessage = true; 93 | } 94 | } 95 | return log; 96 | }; 97 | /** 98 | * Create a new log object 99 | * @param {string} level - The log level 100 | * @param {Array<*>} args - The log arguments 101 | * @param {LogProps} logProps - The log props to apply to the log 102 | * @param {string} errorKey - The name of an error object to use in the log. Defaults to "error" 103 | */ 104 | exports.createLog = (level, args, logProps, computed, propsRoot, errorKey) => { 105 | // Initialize the log object with provided aruguments 106 | const log = exports.initLog(level, args, errorKey); 107 | // Load default props into log object 108 | exports.applyDefaultProps(log, logProps, propsRoot); 109 | // Load computed props into log object 110 | exports.applyComputedProps(log, computed, propsRoot); 111 | return log; 112 | }; 113 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/decircularize.d.ts: -------------------------------------------------------------------------------- 1 | export declare const decircularize: (obj: Object) => any; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/decircularize.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.decircularize = void 0; 7 | const json_stringify_safe_1 = __importDefault(require("json-stringify-safe")); 8 | exports.decircularize = (obj) => JSON.parse(json_stringify_safe_1.default(obj)); 9 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/applyDefaultProps.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/applyDefaultProps.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const createLog_1 = require("../createLog"); 4 | let log; 5 | beforeEach(() => { 6 | log = { message: 'test 123', level: 'info' }; 7 | }); 8 | it('does nothing if no props are provided', () => { 9 | createLog_1.applyDefaultProps(log); 10 | expect(log).toEqual({ 11 | message: 'test 123', 12 | level: 'info' 13 | }); 14 | }); 15 | it('applies default props to log', () => { 16 | createLog_1.applyDefaultProps(log, { 17 | testprop: 'abc123', 18 | data: (log) => `message sent: ${log.message}` 19 | }); 20 | expect(log).toEqual({ 21 | message: 'test 123', 22 | level: 'info', 23 | testprop: 'abc123', 24 | data: 'message sent: test 123' 25 | }); 26 | }); 27 | it('does not apply a prop if it already is in the log object', () => { 28 | log.testprop = 'test prop 123'; 29 | createLog_1.applyDefaultProps(log, { testprop: 'some other string' }); 30 | expect(log).toEqual({ 31 | message: 'test 123', 32 | level: 'info', 33 | testprop: 'test prop 123' 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/createLog.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/createLog.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | const util = __importStar(require("../createLog")); 23 | // setup spies/mocks 24 | const mockData = { 25 | level: 'info', 26 | args: ['test message'], 27 | logProps: { test: 'test123' }, 28 | computed: {}, 29 | errorKey: 'error', 30 | log: { message: '123' } 31 | }; 32 | const spies = { 33 | initLog: jest.spyOn(util, 'initLog').mockReturnValue(mockData.log), 34 | applyDefaultProps: jest 35 | .spyOn(util, 'applyDefaultProps') 36 | .mockImplementation((log) => { 37 | log.testChange = true; 38 | return log; 39 | }) 40 | }; 41 | // Reset spies/mock data for each test 42 | beforeEach(() => { 43 | Object.values(spies).forEach((spy) => spy.mockClear()); 44 | mockData.log = { message: '123' }; 45 | }); 46 | it('calls helper functions as expected and returns log object', () => { 47 | const log = util.createLog(mockData.level, mockData.args, mockData.logProps, mockData.computed, undefined, // propsRoot 48 | mockData.errorKey); 49 | expect(spies.initLog).toHaveBeenCalledWith(...[mockData.level, mockData.args, mockData.errorKey]); 50 | expect(log).toBeDefined(); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/decircularize.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/decircularize.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const decircularize_1 = require("../decircularize"); 4 | it('decircularizes an object that references itself', () => { 5 | const obj = { a: 1, b: 2, selfRef: {} }; 6 | obj.selfRef = obj; 7 | const result = decircularize_1.decircularize(obj); 8 | expect(result).toEqual({ 9 | a: 1, 10 | b: 2, 11 | selfRef: '[Circular ~]' 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/initLog.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/initLog.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const bc_serverless_logging_framework_1 = require("../../../bc-serverless-logging-framework"); 4 | const createLog_1 = require("../createLog"); 5 | it('handles blank input gracefully', () => { 6 | const result = createLog_1.initLog('', []); 7 | expect(result).toEqual({ level: '' }); 8 | }); 9 | it('initializes the log object correctly given the provided arguments', () => { 10 | const result = createLog_1.initLog(bc_serverless_logging_framework_1.bcLogger.levels.info, [ 11 | 'test message 123', 12 | { testprop: 'test 123' } 13 | ]); 14 | expect(result).toEqual({ 15 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 16 | message: 'test message 123', 17 | testprop: 'test 123' 18 | }); 19 | }); 20 | it('sets an error correctly', () => { 21 | const err = new Error('test error'); 22 | const result = createLog_1.initLog(bc_serverless_logging_framework_1.bcLogger.levels.error, [err]); 23 | expect(result).toEqual({ 24 | level: bc_serverless_logging_framework_1.bcLogger.levels.error, 25 | error: { 26 | name: err.name, 27 | message: err.message, 28 | stack: err.stack 29 | } 30 | }); 31 | }); 32 | it('sets an error correctly when a custom error key is specified', () => { 33 | const err = new Error('test error'); 34 | const result = createLog_1.initLog(bc_serverless_logging_framework_1.bcLogger.levels.error, [err], 'errorObject'); 35 | expect(result).toEqual({ 36 | level: bc_serverless_logging_framework_1.bcLogger.levels.error, 37 | errorObject: { 38 | name: err.name, 39 | message: err.message, 40 | stack: err.stack 41 | } 42 | }); 43 | }); 44 | it('initializes a log containing only an object', () => { 45 | const result = createLog_1.initLog(bc_serverless_logging_framework_1.bcLogger.levels.warn, [{ stuff: 'test123' }]); 46 | expect(result).toEqual({ 47 | level: bc_serverless_logging_framework_1.bcLogger.levels.warn, 48 | stuff: 'test123' 49 | }); 50 | }); 51 | it('initializes a log with message in arguments array after object', () => { 52 | const result = createLog_1.initLog(bc_serverless_logging_framework_1.bcLogger.levels.warn, [ 53 | { stuff: 'test123' }, 54 | 'message' 55 | ]); 56 | expect(result).toEqual({ 57 | level: bc_serverless_logging_framework_1.bcLogger.levels.warn, 58 | message: 'message', 59 | stuff: 'test123' 60 | }); 61 | }); 62 | it('ignored duplicate messages, objects, and errors in the array', () => { 63 | const err1 = new Error('error 1'); 64 | const err2 = new Error('error 2'); 65 | const err3 = new Error('error 3'); 66 | const result = createLog_1.initLog(bc_serverless_logging_framework_1.bcLogger.levels.info, [ 67 | 'message 1', 68 | { object1: true }, 69 | 'message 2', 70 | 'message 3', 71 | { object2: true }, 72 | err1, 73 | err2, 74 | { object3: true }, 75 | err3 76 | ]); 77 | expect(result).toEqual({ 78 | level: bc_serverless_logging_framework_1.bcLogger.levels.info, 79 | message: 'message 1', 80 | object1: true, 81 | error: { 82 | name: err1.name, 83 | message: err1.message, 84 | stack: err1.stack 85 | } 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/runTransports.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/runTransports.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const transport_1 = require("../transport"); 13 | const handlers = { 14 | handler1: () => null, 15 | handler2: () => null, 16 | handler3: () => null 17 | }; 18 | const testLog = { 19 | level: 'test', 20 | message: 'test message' 21 | }; 22 | const spies = { 23 | handler1: jest.spyOn(handlers, 'handler1'), 24 | handler2: jest.spyOn(handlers, 'handler2'), 25 | handler3: jest.spyOn(handlers, 'handler3'), 26 | consoleWarning: jest.spyOn(console, 'warn').mockReturnValue() 27 | }; 28 | const clearSpies = () => Object.values(spies).forEach((spy) => spy.mockClear()); 29 | let transports; 30 | beforeEach(() => { 31 | transports = [ 32 | { 33 | handler: handlers.handler1 34 | }, 35 | { 36 | handler: handlers.handler2 37 | }, 38 | { 39 | levelNumber: 3, 40 | handler: handlers.handler3 41 | } 42 | ]; 43 | clearSpies(); 44 | }); 45 | it('Runs transport at specified levels only', () => { 46 | transport_1.runTransports(testLog, 1, transports); 47 | expect(spies.handler1).toHaveBeenCalled(); 48 | expect(spies.handler2).toHaveBeenCalled(); 49 | expect(spies.handler3).not.toHaveBeenCalled(); 50 | clearSpies(); 51 | transport_1.runTransports(testLog, 4, transports); 52 | expect(spies.handler1).toHaveBeenCalled(); 53 | expect(spies.handler2).toHaveBeenCalled(); 54 | expect(spies.handler3).toHaveBeenCalled(); 55 | clearSpies(); 56 | }); 57 | it('Runs transport if levelNumber not specified', () => { 58 | transport_1.runTransports(testLog, undefined, transports); 59 | expect(spies.handler1).toHaveBeenCalled(); 60 | expect(spies.handler2).toHaveBeenCalled(); 61 | expect(spies.handler3).toHaveBeenCalled(); 62 | }); 63 | it('Returns promises for any transports that result in promises', () => __awaiter(void 0, void 0, void 0, function* () { 64 | spies.handler1.mockReturnValueOnce(Promise.resolve('handler1_result')); 65 | spies.handler2.mockReturnValueOnce(Promise.resolve('handler2_result')); 66 | const results = transport_1.runTransports(testLog, 0, transports); 67 | expect(results.length).toBe(2); 68 | const promiseResults = yield Promise.all(results); 69 | expect(promiseResults).toEqual(['handler1_result', 'handler2_result']); 70 | })); 71 | it('logs a warning if a transport does not have a destination or handler', () => { 72 | // Add a transport without a destination or handler 73 | transports.push({}); 74 | // Run transports 75 | transport_1.runTransports(testLog, 0, transports); 76 | // Check that warning was logged for bad transport 77 | expect(spies.consoleWarning).toHaveBeenCalledWith(`Invalid Lager transport: {}. Skipping log`); 78 | }); 79 | it('runs a destination send() function as expected', () => { 80 | const testDestination = { 81 | send: () => undefined 82 | }; 83 | const destinationSpy = jest.spyOn(testDestination, 'send'); 84 | transports.push({ destination: testDestination }); 85 | transport_1.runTransports(testLog, 0, transports); 86 | expect(destinationSpy).toHaveBeenCalledWith(testLog); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/setupTransport.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/tests/setupTransport.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const transport_1 = require("../transport"); 4 | const constants_1 = require("../../constants"); 5 | const testLevels = ['test_level_1', 'test_level_2', 'test_level_3']; 6 | it('sets the level number correctly', () => { 7 | testLevels.forEach((level, i) => { 8 | const transport = { 9 | level 10 | }; 11 | expect(transport_1.setupTransport(transport, testLevels)).toEqual({ 12 | level, 13 | levelNumber: i 14 | }); 15 | }); 16 | }); 17 | it('sets levelNumber to TRANSPORT_LEVEL_ALL if the transport does not specify a level', () => { }); 18 | it('sets levelNumber to TRANSPORT_LEVEL_ALL for appropriate scenarios', () => { 19 | // transport does not specify a level 20 | expect(transport_1.setupTransport({}, testLevels)).toEqual({ 21 | levelNumber: constants_1.TRANSPORT_LEVEL_ALL 22 | }); 23 | // no levels provided 24 | expect(transport_1.setupTransport({ level: 'test_level_2' }, undefined)).toEqual({ 25 | level: 'test_level_2', 26 | levelNumber: constants_1.TRANSPORT_LEVEL_ALL 27 | }); 28 | expect(transport_1.setupTransport({ level: 'test_level_2' }, [])).toEqual({ 29 | level: 'test_level_2', 30 | levelNumber: constants_1.TRANSPORT_LEVEL_ALL 31 | }); 32 | // Transport specifies level not in levels array 33 | expect(transport_1.setupTransport({ level: 'test_level_4' }, testLevels)).toEqual({ 34 | level: 'test_level_4', 35 | levelNumber: constants_1.TRANSPORT_LEVEL_ALL 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/transport.d.ts: -------------------------------------------------------------------------------- 1 | import { Log, Transport } from '../types'; 2 | /** 3 | * Setup a transport 4 | * @param {Transport} transport - The transport to set up 5 | * @param {Array} levels - The available levels for the transport to run against 6 | */ 7 | export declare const setupTransport: (transport: Transport, levels: Array | undefined) => Transport; 8 | /** 9 | * Run a set of transports for a given log at a given level number, returning an array of promises for any transports that return promises 10 | * @param {Log} log - The log to run through transports 11 | * @param {number} levelNumber - The numeric notation of the level to run the transport for 12 | * @param {Array} transports - Array of transports to run 13 | * @returns {Arary} - Array of promises initiated by the runTransports function 14 | */ 15 | export declare const runTransports: (log: Log, levelNumber: number | undefined, transports: Array | undefined) => Promise[]; 16 | -------------------------------------------------------------------------------- /lib/bc-serverless-logging-framework/util/transport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.runTransports = exports.setupTransport = void 0; 4 | const constants_1 = require("../constants"); 5 | /** 6 | * Setup a transport 7 | * @param {Transport} transport - The transport to set up 8 | * @param {Array} levels - The available levels for the transport to run against 9 | */ 10 | exports.setupTransport = (transport, levels) => { 11 | // Set the levelNumber based on the transport 12 | // config and levels available to log to 13 | if ((transport === null || transport === void 0 ? void 0 : transport.level) && (levels === null || levels === void 0 ? void 0 : levels.length)) { 14 | const level = transport.level; 15 | transport.levelNumber = levels.indexOf(level); 16 | if (transport.levelNumber === -1) { 17 | console.warn(`Invalid level detected in transport: ${level}. This transport will run for all log levels. Valid levels: ${Object.values(levels)}`); 18 | } 19 | } 20 | else { 21 | transport.levelNumber = constants_1.TRANSPORT_LEVEL_ALL; 22 | } 23 | return transport; 24 | }; 25 | /** 26 | * Run a set of transports for a given log at a given level number, returning an array of promises for any transports that return promises 27 | * @param {Log} log - The log to run through transports 28 | * @param {number} levelNumber - The numeric notation of the level to run the transport for 29 | * @param {Array} transports - Array of transports to run 30 | * @returns {Arary} - Array of promises initiated by the runTransports function 31 | */ 32 | exports.runTransports = (log, levelNumber, transports) => { 33 | const transportPromises = []; 34 | // Filter transports based on level number passed in 35 | if (levelNumber !== undefined && (transports === null || transports === void 0 ? void 0 : transports.length)) { 36 | transports = transports.filter((transport) => !(transport === null || transport === void 0 ? void 0 : transport.levelNumber) || transport.levelNumber <= levelNumber); 37 | } 38 | // Run filtered transports 39 | transports === null || transports === void 0 ? void 0 : transports.forEach((transport) => { 40 | let result; 41 | // Run transport or log a warning if transport is misconfigured 42 | if (transport.destination) { 43 | result = transport.destination.send(log); 44 | } 45 | else if (transport.handler) { 46 | result = transport.handler(log); 47 | } 48 | else { 49 | console.warn(`Invalid Lager transport: ${JSON.stringify(transport)}. Skipping log`); 50 | } 51 | // Push result to promises array if result is a promise 52 | if (result instanceof Promise) { 53 | transportPromises.push(result); 54 | } 55 | }); 56 | return transportPromises; 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bc-serverless-logging-framework", 3 | "version": "1.0.2", 4 | "main": "lib/bc-serverless-logging-framework.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "build": "tsc", 9 | "test": "jest --coverage" 10 | }, 11 | "dependencies": { 12 | "aws-lambda": "^1.0.6", 13 | "aws-sdk": "^2.741.0", 14 | "axios": "^0.19.0", 15 | "glob": "^7.1.6", 16 | "json-stringify-safe": "^5.0.1", 17 | "lodash.clonedeep": "^4.5.0", 18 | "lodash.get": "^4.4.2", 19 | "lodash.set": "^4.3.2", 20 | "promise.allsettled": "^1.0.2", 21 | "require-all": "^3.0.0", 22 | "uuid": "^8.3.0" 23 | }, 24 | "author": "Jerry Dixon ", 25 | "devDependencies": { 26 | "@types/jest": "^26.0.13", 27 | "@types/json-stringify-safe": "^5.0.0", 28 | "@types/lodash.get": "^4.4.6", 29 | "@types/lodash.set": "^4.3.6", 30 | "@types/node": "^14.6.3", 31 | "@typescript-eslint/eslint-plugin": "^4.0.1", 32 | "@typescript-eslint/parser": "^4.0.1", 33 | "babel-eslint": "^10.1.0", 34 | "eslint": "^7.8.1", 35 | "eslint-config-prettier": "^6.11.0", 36 | "eslint-plugin-import": "^2.22.0", 37 | "eslint-plugin-jest": "^24.0.0", 38 | "eslint-plugin-prettier": "^3.1.4", 39 | "jest": "^26.4.2", 40 | "prettier": "^2.1.1", 41 | "ts-jest": "^26.3.0", 42 | "typescript": "^4.0.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'none' 5 | } 6 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework.test.ts: -------------------------------------------------------------------------------- 1 | import { bcLogger } from './bc-serverless-logging-framework' 2 | import { Log } from './bc-serverless-logging-framework/types' 3 | 4 | const spies = { 5 | console: { 6 | info: jest.spyOn(console, 'info').mockImplementation(), 7 | warn: jest.spyOn(console, 'warn').mockImplementation(), 8 | error: jest.spyOn(console, 'error').mockImplementation() 9 | } 10 | } 11 | 12 | beforeEach(() => { 13 | spies.console.info.mockClear() 14 | spies.console.warn.mockClear() 15 | spies.console.error.mockClear() 16 | }) 17 | 18 | it('creates a logger with default levels as functions', () => { 19 | const logger = bcLogger.create() 20 | for (let level of Object.values(bcLogger.levels)) { 21 | expect(typeof logger[level]).toBe('function') 22 | } 23 | }) 24 | 25 | it('create a logger with specified levels when provided', () => { 26 | const logger = bcLogger.create({ 27 | levels: ['test_level_1', 'test_level_2', bcLogger.levels.info] 28 | }) 29 | expect(typeof logger.test_level_1).toBe('function') 30 | expect(typeof logger.test_level_2).toBe('function') 31 | expect(typeof logger[bcLogger.levels.info]).toBe('function') 32 | }) 33 | 34 | it('logs with default props when specified', () => { 35 | // Setup 36 | const logger = bcLogger.create({ 37 | props: { 38 | testprop1: 'abc', 39 | testprop2: 123, 40 | testprop3: { 41 | test: true 42 | }, 43 | testprop4(log: Log) { 44 | return `Log message contains the following text: ${log.message}. Level: ${log.level}` 45 | } 46 | }, 47 | transports: [ 48 | { 49 | destination: bcLogger.destinations.consoleLog() 50 | } 51 | ] 52 | }) 53 | 54 | // Run 55 | logger.info('test message 123') 56 | 57 | // Assert 58 | const [logData] = spies.console.info.mock.calls[0] 59 | expect(JSON.parse(logData)).toMatchObject({ 60 | level: bcLogger.levels.info, 61 | message: 'test message 123', 62 | testprop1: 'abc', 63 | testprop2: 123, 64 | testprop3: { 65 | test: true 66 | }, 67 | testprop4: 68 | 'Log message contains the following text: test message 123. Level: info' 69 | }) 70 | }) 71 | 72 | it('sets new default props onto the logger using the props() method', () => { 73 | // Setup 74 | const logger = bcLogger.create({ 75 | transports: [ 76 | { 77 | destination: bcLogger.destinations.consoleLog() 78 | } 79 | ] 80 | }) 81 | 82 | // Run 83 | logger.props({ testprop: '1234' }) 84 | logger.info('test message') 85 | 86 | // Assert 87 | const [logData] = spies.console.info.mock.calls[0] 88 | expect(JSON.parse(logData)).toMatchObject({ 89 | message: 'test message', 90 | level: bcLogger.levels.info, 91 | testprop: '1234' 92 | }) 93 | }) 94 | 95 | it('utilizes transport level correctly', () => { 96 | // Setup 97 | const logger = bcLogger.create({ 98 | transports: [ 99 | { 100 | level: bcLogger.levels.warn, 101 | destination: bcLogger.destinations.consoleLog() 102 | } 103 | ] 104 | }) 105 | 106 | // Run 107 | logger.info('test info message') 108 | logger.warn('test warn message') 109 | logger.error('test error message') 110 | 111 | // Assert 112 | const [warnLogData] = spies.console.warn.mock.calls[0] 113 | expect(JSON.parse(warnLogData)).toEqual({ 114 | message: 'test warn message', 115 | level: bcLogger.levels.warn 116 | }) 117 | 118 | const [errorLogData] = spies.console.error.mock.calls[0] 119 | expect(JSON.parse(errorLogData)).toEqual({ 120 | message: 'test error message', 121 | level: bcLogger.levels.error 122 | }) 123 | 124 | expect(spies.console.info).not.toHaveBeenCalled() 125 | }) 126 | 127 | it('logs a warning if an invalid level is used for a transport', () => { 128 | bcLogger.create({ 129 | transports: [ 130 | { 131 | level: 'dummy_level', 132 | destination: bcLogger.destinations.consoleLog() 133 | } 134 | ] 135 | }) 136 | 137 | expect(spies.console.warn).toHaveBeenCalledWith( 138 | `Invalid level detected in transport: dummy_level. This transport will run for all log levels. Valid levels: ${Object.values( 139 | bcLogger.levels 140 | )}` 141 | ) 142 | }) 143 | 144 | it('handles rejected promises gracefully when running flush()', async () => { 145 | const logger = bcLogger.create({ 146 | transports: [ 147 | { 148 | handler: (log: Log) => (log.fail ? Promise.reject() : Promise.resolve()) 149 | } 150 | ] 151 | }) 152 | 153 | logger.info('test message') 154 | logger.error('this message will fail to send.', { fail: true }) 155 | 156 | const results = await logger.flush() 157 | expect( 158 | results.find((result: any) => result.status === 'rejected') 159 | ).toBeDefined() 160 | }) 161 | 162 | it('loads errorKey correctly', () => { 163 | const logger = bcLogger.create({ 164 | transports: [ 165 | { 166 | destination: bcLogger.destinations.consoleLog() 167 | } 168 | ], 169 | errorKey: 'my_error_object' 170 | }) 171 | 172 | const err = new Error('test error') 173 | 174 | logger.error('An error occurred', err) 175 | 176 | const [errorLogData] = spies.console.error.mock.calls[0] 177 | expect(JSON.parse(errorLogData)).toEqual({ 178 | level: bcLogger.levels.error, 179 | message: 'An error occurred', 180 | my_error_object: { 181 | name: err.name, 182 | message: err.message, 183 | stack: err.stack 184 | } 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework.ts: -------------------------------------------------------------------------------- 1 | import { Levels } from './bc-serverless-logging-framework/enums/Levels' 2 | import { destinations } from './bc-serverless-logging-framework/destinations' 3 | import { 4 | Logger, 5 | LogProps, 6 | LagerConfiguration, 7 | LagerChildOptions 8 | } from './bc-serverless-logging-framework/types' 9 | import { createLog } from './bc-serverless-logging-framework/util/createLog' 10 | import { 11 | setupTransport, 12 | runTransports 13 | } from './bc-serverless-logging-framework/util/transport' 14 | import _set from 'lodash.set' 15 | 16 | // Shim for promise.allSettled 17 | require('promise.allsettled').shim() 18 | 19 | const promises: Array> = [] 20 | 21 | export const bcLogger = { 22 | destinations, 23 | levels: Levels, 24 | 25 | /** 26 | * Return a logger object based on configuration 27 | * 28 | */ 29 | create({ 30 | levels, 31 | props, 32 | computed, 33 | transports, 34 | errorKey, 35 | propsRoot 36 | }: LagerConfiguration = {}) { 37 | let configuredProperties = props ? { ...props } : {} 38 | 39 | // Set defaults if not provided 40 | if (!levels?.length) { 41 | levels = Object.values(bcLogger.levels) 42 | } 43 | if (!errorKey) { 44 | errorKey = 'error' 45 | } 46 | 47 | if (!transports?.length) { 48 | console.warn( 49 | 'Warning: no transports added to bcLogger. Logging functionality is disabled' 50 | ) 51 | } 52 | 53 | if (props && propsRoot) { 54 | const rootedProps = {} 55 | _set(rootedProps, propsRoot, props) 56 | props = rootedProps 57 | } 58 | 59 | // Set level index onto transport. Log a warning if using a level that doesn't exist 60 | transports?.forEach((transport) => { 61 | setupTransport(transport, levels) 62 | }) 63 | 64 | // Set up logger 65 | const logger: Logger = { 66 | // Function to set new props after creating a logger 67 | props(newProps: LogProps): Logger { 68 | if (propsRoot) { 69 | _set(props as Object, propsRoot as string, { 70 | ...props?.propsRoot, 71 | ...newProps 72 | }) 73 | } else { 74 | props = { 75 | ...props, 76 | ...newProps 77 | } 78 | } 79 | 80 | configuredProperties = { 81 | ...configuredProperties, 82 | ...newProps 83 | } 84 | 85 | return this 86 | }, 87 | 88 | // Function to return a new logger inheriting from this one 89 | child(childConfig?: LagerConfiguration, options?: LagerChildOptions) { 90 | if (!childConfig) { 91 | childConfig = {} 92 | } 93 | const conf: LagerConfiguration = {} 94 | conf.levels = childConfig.levels ?? levels 95 | conf.propsRoot = childConfig.propsRoot ?? propsRoot 96 | conf.props = configuredProperties 97 | if (childConfig.props) { 98 | conf.props = options?.replaceProps 99 | ? childConfig.props 100 | : { ...configuredProperties, ...childConfig.props } 101 | } 102 | 103 | if (computed && childConfig.computed) { 104 | conf.computed = options?.replaceComputed 105 | ? childConfig.computed 106 | : { ...computed, ...childConfig.computed } 107 | } else if (childConfig.computed) { 108 | conf.computed = childConfig.computed 109 | } else if (computed) { 110 | conf.computed = computed 111 | } 112 | 113 | if (transports && childConfig.transports) { 114 | conf.transports = options?.replaceTransports 115 | ? childConfig.transports 116 | : [...transports, ...childConfig.transports] 117 | } else if (transports) { 118 | conf.transports = transports 119 | } else if (childConfig.transports) { 120 | conf.transports = childConfig.transports 121 | } 122 | 123 | return bcLogger.create(conf) 124 | }, 125 | 126 | /** 127 | * Wait for transport promises to finish 128 | */ 129 | flush(): Promise { 130 | return Promise.allSettled(promises) 131 | } 132 | } 133 | 134 | // Set up logger to run transports at each log level 135 | levels.forEach((level, i) => { 136 | logger[level] = (...args: Array) => { 137 | // Create the log object based on arguments/logger props 138 | const log = createLog(level, args, props, computed, propsRoot, errorKey) 139 | 140 | // Run transports for level and push any promises to the promises array 141 | const transportPromises = runTransports(log, i, transports) 142 | promises.push(...transportPromises) 143 | } 144 | }) 145 | return logger 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * When a transport levelNumber is set to this, 3 | * the transport will run at all levels 4 | */ 5 | export const TRANSPORT_LEVEL_ALL: number = -1 6 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/destinations/consoleLog.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLevels } from '../enums/ConsoleLevels' 2 | import { Destination, Log, ConsoleLogDestinationConfig } from '../types' 3 | import { Levels } from '../enums/Levels' 4 | import { decircularize } from '../util/decircularize' 5 | 6 | export const consoleLog = ( 7 | config?: ConsoleLogDestinationConfig 8 | ): Destination => ({ 9 | send(log?: Log) { 10 | if (!log) { 11 | return 12 | } 13 | // Retrieve the log function 14 | let logFn: Function 15 | let level = '' 16 | if (log.consoleLevel || config?.consoleLevel) { 17 | level = log.consoleLevel || config?.consoleLevel 18 | } else if (log.level) { 19 | level = log.level 20 | } 21 | 22 | switch (level) { 23 | case Levels.debug: 24 | case ConsoleLevels.debug: 25 | logFn = console.debug 26 | break 27 | case Levels.info: 28 | case ConsoleLevels.info: 29 | logFn = console.info 30 | break 31 | case Levels.warn: 32 | case ConsoleLevels.warn: 33 | logFn = console.warn 34 | break 35 | case Levels.error: 36 | case Levels.critical: 37 | case ConsoleLevels.error: 38 | logFn = console.error 39 | break 40 | default: 41 | logFn = console.log 42 | } 43 | 44 | // Log to the console 45 | logFn(JSON.stringify(decircularize(log))) 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/destinations/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Destination for sending logs to an HTTP endpoint 3 | */ 4 | import { LoggingFrameworkDestinationConfigError } from '../errors/LoggingFrameworkDestinationConfigError' 5 | import { Log, Destination } from '../types' 6 | import { AxiosRequestConfig } from 'axios' 7 | import axios from 'axios' 8 | 9 | export const http = (config: AxiosRequestConfig): Destination => { 10 | if (!config) { 11 | throw new LoggingFrameworkDestinationConfigError( 12 | 'Axios configuration is required for http destination' 13 | ) 14 | } 15 | 16 | return { 17 | send(log: Log) { 18 | config.data = log 19 | return axios(config).catch((error) => { 20 | console.error( 21 | `Error occurred sending log message to endpoint: ${config.url}`, 22 | { log, error } 23 | ) 24 | }) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/destinations/index.ts: -------------------------------------------------------------------------------- 1 | import { sqs } from './sqs' 2 | import { http } from './http' 3 | import { consoleLog } from './consoleLog' 4 | 5 | export const destinations = { 6 | sqs, 7 | http, 8 | consoleLog 9 | } 10 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/destinations/sqs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Destination for sending directly to an SQS queue 3 | */ 4 | 5 | const AWS = require('aws-sdk') 6 | import { LoggingFrameworkDestinationConfigError } from '../errors/LoggingFrameworkDestinationConfigError' 7 | import { Log, Destination, SQSDestinationConfig } from '../types' 8 | import * as SQS from 'aws-sdk/clients/sqs' 9 | import { decircularize } from '../util/decircularize' 10 | 11 | export const sqs = (config: SQSDestinationConfig): Destination => { 12 | if (!config) { 13 | throw new LoggingFrameworkDestinationConfigError( 14 | 'No SQS Destination config supplied' 15 | ) 16 | } 17 | let { sqsOptions, queueUrl } = config 18 | 19 | // Set url, throw error if not provided 20 | if (!queueUrl) { 21 | throw new LoggingFrameworkDestinationConfigError( 22 | 'SQS Queue URL is required.' 23 | ) 24 | } 25 | 26 | // Setup default sqs options 27 | sqsOptions = sqsOptions || {} 28 | if (!sqsOptions.apiVersion) { 29 | sqsOptions.apiVersion = '2012-11-05' 30 | } 31 | if (!sqsOptions.region) { 32 | sqsOptions.region = 'us-east-1' 33 | } 34 | const sqs: SQS = new AWS.SQS(sqsOptions) 35 | 36 | return { 37 | send(log: Log) { 38 | return sqs 39 | .sendMessage({ 40 | MessageBody: JSON.stringify(decircularize(log)), 41 | QueueUrl: queueUrl 42 | }) 43 | .promise() 44 | .catch((error) => { 45 | console.error('Error occurred sending log message to SQS.', { 46 | log, 47 | error 48 | }) 49 | }) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/destinations/tests/consoleLog.test.ts: -------------------------------------------------------------------------------- 1 | import { consoleLog } from '../consoleLog' 2 | import { ConsoleLevels } from '../../enums/ConsoleLevels' 3 | import { bcLogger } from '../../../bc-serverless-logging-framework' 4 | 5 | const destinations = { 6 | // Will log based on log level 7 | normal: consoleLog(), 8 | 9 | // Will log based on log level 10 | weird: consoleLog({ consoleLevel: null }), 11 | 12 | // Will always send debug logs 13 | debug: consoleLog({ consoleLevel: ConsoleLevels.debug }), 14 | 15 | // Will always send info logs 16 | info: consoleLog({ consoleLevel: ConsoleLevels.info }), 17 | 18 | // Will always send warn logs 19 | warn: consoleLog({ consoleLevel: ConsoleLevels.warn }), 20 | 21 | // Will always send error logs 22 | error: consoleLog({ consoleLevel: ConsoleLevels.error }) 23 | } 24 | 25 | const spies: any = { 26 | console: { 27 | debug: jest.spyOn(console, 'debug').mockImplementation(), 28 | info: jest.spyOn(console, 'info').mockImplementation(), 29 | warn: jest.spyOn(console, 'warn').mockImplementation(), 30 | error: jest.spyOn(console, 'error').mockImplementation(), 31 | log: jest.spyOn(console, 'log').mockImplementation() 32 | } 33 | } 34 | 35 | const assertCalls = (logFn: string) => { 36 | for (let key in spies.console) { 37 | const spy = spies.console[key] 38 | const callsExpected = key === logFn ? 1 : 0 39 | expect(spy).toHaveBeenCalledTimes(callsExpected) 40 | spy.mockClear() 41 | } 42 | } 43 | 44 | it('logs as expected for each bcLogger level', () => { 45 | destinations.normal.send({ 46 | level: bcLogger.levels.debug, 47 | message: 'test message' 48 | }) 49 | assertCalls('debug') 50 | 51 | destinations.normal.send({ 52 | level: bcLogger.levels.info, 53 | message: 'test message' 54 | }) 55 | assertCalls('info') 56 | 57 | destinations.normal.send({ 58 | level: bcLogger.levels.warn, 59 | message: 'test message' 60 | }) 61 | assertCalls('warn') 62 | 63 | destinations.normal.send({ 64 | level: bcLogger.levels.error, 65 | message: 'test message' 66 | }) 67 | assertCalls('error') 68 | 69 | destinations.normal.send({ 70 | level: bcLogger.levels.critical, 71 | message: 'test message' 72 | }) 73 | assertCalls('error') 74 | 75 | destinations.normal.send({ 76 | level: 'random_log_level', 77 | message: 'test message' 78 | }) 79 | assertCalls('log') 80 | }) 81 | 82 | it('logs correctly using consoleLevel', () => { 83 | destinations.normal.send({ 84 | level: bcLogger.levels.info, 85 | consoleLevel: 'error', 86 | message: 'test message' 87 | }) 88 | assertCalls('error') 89 | 90 | destinations.weird.send({ 91 | level: bcLogger.levels.warn, 92 | message: 'test message' 93 | }) 94 | assertCalls('warn') 95 | 96 | destinations.debug.send({ 97 | level: bcLogger.levels.info, 98 | consoleLevel: 'error', 99 | message: 'test message' 100 | }) 101 | assertCalls('error') 102 | 103 | destinations.debug.send({ 104 | level: bcLogger.levels.info, 105 | message: 'test message' 106 | }) 107 | assertCalls('debug') 108 | 109 | destinations.info.send({ 110 | level: bcLogger.levels.warn, 111 | message: 'test message' 112 | }) 113 | assertCalls('info') 114 | 115 | destinations.warn.send({ 116 | level: bcLogger.levels.info, 117 | message: 'test message' 118 | }) 119 | assertCalls('warn') 120 | 121 | destinations.error.send({ 122 | level: bcLogger.levels.info, 123 | message: 'test message' 124 | }) 125 | assertCalls('error') 126 | 127 | destinations.debug.send({ 128 | level: bcLogger.levels.info, 129 | consoleLevel: 'dumb_console_level', 130 | message: 'test message' 131 | }) 132 | assertCalls('log') 133 | }) 134 | 135 | it('works gracefully for odd scenarios', () => { 136 | destinations.normal.send() 137 | assertCalls('') // expects no log to have happened 138 | 139 | destinations.normal.send({}) 140 | assertCalls('log') 141 | 142 | destinations.debug.send({ 143 | consoleLevel: undefined 144 | }) 145 | assertCalls('debug') 146 | 147 | destinations.weird.send({ 148 | consoleLevel: null 149 | }) 150 | }) 151 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/enums/ConsoleLevels.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | export enum ConsoleLevels { 3 | debug = 'debug', 4 | info = 'info', 5 | warn = 'warn', 6 | error = 'error' 7 | } 8 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/enums/Levels.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | export enum Levels { 3 | debug = 'debug', 4 | info = 'info', 5 | warn = 'warn', 6 | error = 'error', 7 | critical = 'critical' 8 | } 9 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/errors/LoggingFrameworkDestinationConfigError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error to throw when there is a problem 3 | * configuration a destination 4 | */ 5 | export class LoggingFrameworkDestinationConfigError extends Error { 6 | constructor(message: any) { 7 | super(message) 8 | this.message = message 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/types.ts: -------------------------------------------------------------------------------- 1 | import * as SQS from 'aws-sdk/clients/sqs' 2 | 3 | export interface LagerConfiguration { 4 | levels?: Array 5 | props?: LogProps 6 | propsRoot?: string 7 | computed?: LogComputedProps 8 | transports?: Array 9 | errorKey?: string 10 | } 11 | 12 | export interface LagerChildOptions { 13 | replaceTransports?: boolean 14 | replaceProps?: boolean 15 | replaceComputed?: boolean 16 | } 17 | 18 | export interface Log { 19 | level?: string 20 | message?: string 21 | [x: string]: any 22 | } 23 | 24 | export interface LogProps { 25 | [x: string]: any 26 | } 27 | 28 | export interface LogComputedProps { 29 | [x: string]: Function 30 | } 31 | 32 | export interface Logger { 33 | props: Function 34 | flush: Function 35 | [x: string]: Function 36 | } 37 | 38 | export interface Transport { 39 | destination?: Destination 40 | handler?: Function 41 | level?: string 42 | levelNumber?: number 43 | } 44 | 45 | export interface Destination { 46 | // eslint-disable-next-line no-unused-vars 47 | send(log?: Log): void | Promise 48 | } 49 | 50 | export interface ConsoleType { 51 | debug: Function 52 | info: Function 53 | warn: Function 54 | error: Function 55 | } 56 | 57 | export interface ConsoleLogDestinationConfig { 58 | consoleLevel?: string | null 59 | } 60 | 61 | export interface SQSDestinationConfig { 62 | sqsOptions?: SQS.Types.ClientConfiguration 63 | queueUrl: string 64 | } 65 | 66 | export interface RunTransportsConfig { 67 | log: Log 68 | level: string 69 | transports: Array 70 | } 71 | 72 | export interface HttpDestinationOptions { 73 | logProperty?: string 74 | } 75 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/createLog.ts: -------------------------------------------------------------------------------- 1 | import { Log, LogProps, LogComputedProps } from '../types' 2 | import _get from 'lodash.get' 3 | import _set from 'lodash.set' 4 | 5 | /** 6 | * Apply default log props to a provided log object 7 | * @param {Log} log - The log object to update 8 | * @param {LogProps} props - The log props object 9 | * @returns {Log} the resulting log object 10 | */ 11 | export const applyDefaultProps = ( 12 | log: Log, 13 | props?: LogProps, 14 | propsRoot?: string 15 | ): Log => { 16 | if (props) { 17 | for (let [propName, prop] of Object.entries(props)) { 18 | // Reset prop name if propsRoot was passed in 19 | if (propsRoot) { 20 | propName = `${propsRoot}.${propName}` 21 | } 22 | 23 | // Apply default props 24 | if (_get(log, propName) === undefined) { 25 | if (typeof prop === 'function') { 26 | _set(log, propName, prop(log)) 27 | } else { 28 | _set(log, propName, prop) 29 | } 30 | } 31 | } 32 | } 33 | 34 | return log 35 | } 36 | 37 | /** 38 | * Apply default log props to a provided log object 39 | * @param {Log} log - The log object to update 40 | * @param {LogComputedProps} computed - Object containing computed functions to run 41 | * @returns {Log} the resulting log object 42 | */ 43 | export const applyComputedProps = ( 44 | log: Log, 45 | computed?: LogComputedProps, 46 | propsRoot?: string 47 | ) => { 48 | if (computed) { 49 | for (let [propName, fn] of Object.entries(computed)) { 50 | // Reset prop name if propsRoot was passed in 51 | if (propsRoot) { 52 | propName = `${propsRoot}.${propName}` 53 | } 54 | 55 | // Apply computed props 56 | if (typeof fn === 'function') { 57 | _set(log, propName, fn(log)) 58 | } 59 | } 60 | } 61 | 62 | return log 63 | } 64 | 65 | /** 66 | * Initialize a log object with the provided log arguments 67 | * @param {string} level - the log level 68 | * @param {Array<*>} args - the log arguments 69 | * @param {string} errorKey - The name of the error property to add for an error argument 70 | * @returns {Log} The new log object 71 | */ 72 | export const initLog = ( 73 | level: string, 74 | args: Array, 75 | errorKey?: string 76 | ): Log => { 77 | const log: Log = { level } 78 | const meta = { 79 | hasError: false, 80 | hasMessage: false, 81 | hasObject: false 82 | } 83 | if (!errorKey) { 84 | errorKey = 'error' 85 | } 86 | 87 | // Load provided arguemnts into log object 88 | for (let arg of args) { 89 | if (arg instanceof Error && !meta.hasError) { 90 | // Errors are pushed to the errors array 91 | log[errorKey] = { 92 | message: arg.message, 93 | name: arg.name, 94 | stack: arg.stack 95 | } 96 | meta.hasError = true 97 | } else if (typeof arg === 'object' && !meta.hasObject) { 98 | // Object values are added to the log object 99 | Object.assign(log, arg) 100 | meta.hasObject = true 101 | } else if (!meta.hasMessage) { 102 | // Primitive values are passed into the message property 103 | log.message = arg 104 | meta.hasMessage = true 105 | } 106 | } 107 | 108 | return log 109 | } 110 | 111 | /** 112 | * Create a new log object 113 | * @param {string} level - The log level 114 | * @param {Array<*>} args - The log arguments 115 | * @param {LogProps} logProps - The log props to apply to the log 116 | * @param {string} errorKey - The name of an error object to use in the log. Defaults to "error" 117 | */ 118 | export const createLog = ( 119 | level: string, 120 | args: Array, 121 | logProps?: LogProps, 122 | computed?: LogComputedProps, 123 | propsRoot?: string, 124 | errorKey?: string 125 | ): Log => { 126 | // Initialize the log object with provided aruguments 127 | const log: Log = initLog(level, args, errorKey) 128 | 129 | // Load default props into log object 130 | applyDefaultProps(log, logProps, propsRoot) 131 | 132 | // Load computed props into log object 133 | applyComputedProps(log, computed, propsRoot) 134 | 135 | return log 136 | } 137 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/decircularize.ts: -------------------------------------------------------------------------------- 1 | import stringify from 'json-stringify-safe' 2 | 3 | export const decircularize = (obj: Object) => JSON.parse(stringify(obj)) 4 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/tests/applyDefaultProps.test.ts: -------------------------------------------------------------------------------- 1 | import { applyDefaultProps } from '../createLog' 2 | import { Log } from '../../types' 3 | 4 | let log: Log 5 | beforeEach(() => { 6 | log = { message: 'test 123', level: 'info' } 7 | }) 8 | 9 | it('does nothing if no props are provided', () => { 10 | applyDefaultProps(log) 11 | expect(log).toEqual({ 12 | message: 'test 123', 13 | level: 'info' 14 | }) 15 | }) 16 | 17 | it('applies default props to log', () => { 18 | applyDefaultProps(log, { 19 | testprop: 'abc123', 20 | data: (log: Log) => `message sent: ${log.message}` 21 | }) 22 | expect(log).toEqual({ 23 | message: 'test 123', 24 | level: 'info', 25 | testprop: 'abc123', 26 | data: 'message sent: test 123' 27 | }) 28 | }) 29 | 30 | it('does not apply a prop if it already is in the log object', () => { 31 | log.testprop = 'test prop 123' 32 | applyDefaultProps(log, { testprop: 'some other string' }) 33 | expect(log).toEqual({ 34 | message: 'test 123', 35 | level: 'info', 36 | testprop: 'test prop 123' 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/tests/createLog.test.ts: -------------------------------------------------------------------------------- 1 | import * as util from '../createLog' 2 | 3 | // setup spies/mocks 4 | const mockData = { 5 | level: 'info', 6 | args: ['test message'], 7 | logProps: { test: 'test123' }, 8 | computed: {}, 9 | errorKey: 'error', 10 | log: { message: '123' } 11 | } 12 | 13 | const spies = { 14 | initLog: jest.spyOn(util, 'initLog').mockReturnValue(mockData.log), 15 | applyDefaultProps: jest 16 | .spyOn(util, 'applyDefaultProps') 17 | .mockImplementation((log) => { 18 | log.testChange = true 19 | return log 20 | }) 21 | } 22 | 23 | // Reset spies/mock data for each test 24 | beforeEach(() => { 25 | Object.values(spies).forEach((spy) => spy.mockClear()) 26 | mockData.log = { message: '123' } 27 | }) 28 | 29 | it('calls helper functions as expected and returns log object', () => { 30 | const log = util.createLog( 31 | mockData.level, 32 | mockData.args, 33 | mockData.logProps, 34 | mockData.computed, 35 | undefined, // propsRoot 36 | mockData.errorKey 37 | ) 38 | 39 | expect(spies.initLog).toHaveBeenCalledWith( 40 | ...[mockData.level, mockData.args, mockData.errorKey] 41 | ) 42 | expect(log).toBeDefined() 43 | }) 44 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/tests/decircularize.test.ts: -------------------------------------------------------------------------------- 1 | import { decircularize } from '../decircularize' 2 | 3 | it('decircularizes an object that references itself', () => { 4 | const obj = { a: 1, b: 2, selfRef: {} } 5 | obj.selfRef = obj 6 | 7 | const result = decircularize(obj) 8 | expect(result).toEqual({ 9 | a: 1, 10 | b: 2, 11 | selfRef: '[Circular ~]' 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/tests/initLog.test.ts: -------------------------------------------------------------------------------- 1 | import { bcLogger } from '../../../bc-serverless-logging-framework' 2 | import { initLog } from '../createLog' 3 | 4 | it('handles blank input gracefully', () => { 5 | const result = initLog('', []) 6 | expect(result).toEqual({ level: '' }) 7 | }) 8 | 9 | it('initializes the log object correctly given the provided arguments', () => { 10 | const result = initLog(bcLogger.levels.info, [ 11 | 'test message 123', 12 | { testprop: 'test 123' } 13 | ]) 14 | 15 | expect(result).toEqual({ 16 | level: bcLogger.levels.info, 17 | message: 'test message 123', 18 | testprop: 'test 123' 19 | }) 20 | }) 21 | 22 | it('sets an error correctly', () => { 23 | const err = new Error('test error') 24 | const result = initLog(bcLogger.levels.error, [err]) 25 | expect(result).toEqual({ 26 | level: bcLogger.levels.error, 27 | error: { 28 | name: err.name, 29 | message: err.message, 30 | stack: err.stack 31 | } 32 | }) 33 | }) 34 | 35 | it('sets an error correctly when a custom error key is specified', () => { 36 | const err = new Error('test error') 37 | const result = initLog(bcLogger.levels.error, [err], 'errorObject') 38 | expect(result).toEqual({ 39 | level: bcLogger.levels.error, 40 | errorObject: { 41 | name: err.name, 42 | message: err.message, 43 | stack: err.stack 44 | } 45 | }) 46 | }) 47 | 48 | it('initializes a log containing only an object', () => { 49 | const result = initLog(bcLogger.levels.warn, [{ stuff: 'test123' }]) 50 | expect(result).toEqual({ 51 | level: bcLogger.levels.warn, 52 | stuff: 'test123' 53 | }) 54 | }) 55 | 56 | it('initializes a log with message in arguments array after object', () => { 57 | const result = initLog(bcLogger.levels.warn, [ 58 | { stuff: 'test123' }, 59 | 'message' 60 | ]) 61 | expect(result).toEqual({ 62 | level: bcLogger.levels.warn, 63 | message: 'message', 64 | stuff: 'test123' 65 | }) 66 | }) 67 | 68 | it('ignored duplicate messages, objects, and errors in the array', () => { 69 | const err1 = new Error('error 1') 70 | const err2 = new Error('error 2') 71 | const err3 = new Error('error 3') 72 | 73 | const result = initLog(bcLogger.levels.info, [ 74 | 'message 1', 75 | { object1: true }, 76 | 'message 2', 77 | 'message 3', 78 | { object2: true }, 79 | err1, 80 | err2, 81 | { object3: true }, 82 | err3 83 | ]) 84 | 85 | expect(result).toEqual({ 86 | level: bcLogger.levels.info, 87 | message: 'message 1', 88 | object1: true, 89 | error: { 90 | name: err1.name, 91 | message: err1.message, 92 | stack: err1.stack 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/tests/runTransports.test.ts: -------------------------------------------------------------------------------- 1 | import { runTransports } from '../transport' 2 | import { Log, Transport } from '../../types' 3 | 4 | const handlers = { 5 | handler1: (): null | Promise => null, 6 | handler2: (): null | Promise => null, 7 | handler3: (): null | Promise => null 8 | } 9 | 10 | const testLog: Log = { 11 | level: 'test', 12 | message: 'test message' 13 | } 14 | 15 | const spies = { 16 | handler1: jest.spyOn(handlers, 'handler1'), 17 | handler2: jest.spyOn(handlers, 'handler2'), 18 | handler3: jest.spyOn(handlers, 'handler3'), 19 | consoleWarning: jest.spyOn(console, 'warn').mockReturnValue() 20 | } 21 | 22 | const clearSpies = () => Object.values(spies).forEach((spy) => spy.mockClear()) 23 | 24 | let transports: Array 25 | 26 | beforeEach(() => { 27 | transports = [ 28 | { 29 | handler: handlers.handler1 30 | }, 31 | { 32 | handler: handlers.handler2 33 | }, 34 | { 35 | levelNumber: 3, 36 | handler: handlers.handler3 37 | } 38 | ] 39 | clearSpies() 40 | }) 41 | 42 | it('Runs transport at specified levels only', () => { 43 | runTransports(testLog, 1, transports) 44 | expect(spies.handler1).toHaveBeenCalled() 45 | expect(spies.handler2).toHaveBeenCalled() 46 | expect(spies.handler3).not.toHaveBeenCalled() 47 | clearSpies() 48 | 49 | runTransports(testLog, 4, transports) 50 | expect(spies.handler1).toHaveBeenCalled() 51 | expect(spies.handler2).toHaveBeenCalled() 52 | expect(spies.handler3).toHaveBeenCalled() 53 | clearSpies() 54 | }) 55 | 56 | it('Runs transport if levelNumber not specified', () => { 57 | runTransports(testLog, undefined, transports) 58 | expect(spies.handler1).toHaveBeenCalled() 59 | expect(spies.handler2).toHaveBeenCalled() 60 | expect(spies.handler3).toHaveBeenCalled() 61 | }) 62 | 63 | it('Returns promises for any transports that result in promises', async () => { 64 | spies.handler1.mockReturnValueOnce(Promise.resolve('handler1_result')) 65 | spies.handler2.mockReturnValueOnce(Promise.resolve('handler2_result')) 66 | 67 | const results: Array> = runTransports(testLog, 0, transports) 68 | expect(results.length).toBe(2) 69 | 70 | const promiseResults = await Promise.all(results) 71 | expect(promiseResults).toEqual(['handler1_result', 'handler2_result']) 72 | }) 73 | 74 | it('logs a warning if a transport does not have a destination or handler', () => { 75 | // Add a transport without a destination or handler 76 | transports.push({}) 77 | 78 | // Run transports 79 | runTransports(testLog, 0, transports) 80 | 81 | // Check that warning was logged for bad transport 82 | expect(spies.consoleWarning).toHaveBeenCalledWith( 83 | `Invalid Lager transport: {}. Skipping log` 84 | ) 85 | }) 86 | 87 | it('runs a destination send() function as expected', () => { 88 | const testDestination = { 89 | send: () => undefined 90 | } 91 | const destinationSpy = jest.spyOn(testDestination, 'send') 92 | 93 | transports.push({ destination: testDestination }) 94 | 95 | runTransports(testLog, 0, transports) 96 | 97 | expect(destinationSpy).toHaveBeenCalledWith(testLog) 98 | }) 99 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/tests/setupTransport.test.ts: -------------------------------------------------------------------------------- 1 | import { setupTransport } from '../transport' 2 | import { TRANSPORT_LEVEL_ALL } from '../../constants' 3 | 4 | const testLevels = ['test_level_1', 'test_level_2', 'test_level_3'] 5 | 6 | it('sets the level number correctly', () => { 7 | testLevels.forEach((level, i) => { 8 | const transport = { 9 | level 10 | } 11 | expect(setupTransport(transport, testLevels)).toEqual({ 12 | level, 13 | levelNumber: i 14 | }) 15 | }) 16 | }) 17 | 18 | it('sets levelNumber to TRANSPORT_LEVEL_ALL if the transport does not specify a level', () => { }) 19 | 20 | it('sets levelNumber to TRANSPORT_LEVEL_ALL for appropriate scenarios', () => { 21 | // transport does not specify a level 22 | expect(setupTransport({}, testLevels)).toEqual({ 23 | levelNumber: TRANSPORT_LEVEL_ALL 24 | }) 25 | 26 | // no levels provided 27 | expect(setupTransport({ level: 'test_level_2' }, undefined)).toEqual({ 28 | level: 'test_level_2', 29 | levelNumber: TRANSPORT_LEVEL_ALL 30 | }) 31 | expect(setupTransport({ level: 'test_level_2' }, [])).toEqual({ 32 | level: 'test_level_2', 33 | levelNumber: TRANSPORT_LEVEL_ALL 34 | }) 35 | 36 | // Transport specifies level not in levels array 37 | expect(setupTransport({ level: 'test_level_4' }, testLevels)).toEqual({ 38 | level: 'test_level_4', 39 | levelNumber: TRANSPORT_LEVEL_ALL 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/bc-serverless-logging-framework/util/transport.ts: -------------------------------------------------------------------------------- 1 | import { Log, Transport } from '../types' 2 | import { TRANSPORT_LEVEL_ALL } from '../constants' 3 | 4 | /** 5 | * Setup a transport 6 | * @param {Transport} transport - The transport to set up 7 | * @param {Array} levels - The available levels for the transport to run against 8 | */ 9 | export const setupTransport = ( 10 | transport: Transport, 11 | levels: Array | undefined 12 | ) => { 13 | // Set the levelNumber based on the transport 14 | // config and levels available to log to 15 | if (transport?.level && levels?.length) { 16 | const level = transport.level 17 | transport.levelNumber = levels.indexOf(level) 18 | if (transport.levelNumber === -1) { 19 | console.warn( 20 | `Invalid level detected in transport: ${level}. This transport will run for all log levels. Valid levels: ${Object.values( 21 | levels 22 | )}` 23 | ) 24 | } 25 | } else { 26 | transport.levelNumber = TRANSPORT_LEVEL_ALL 27 | } 28 | 29 | return transport 30 | } 31 | 32 | /** 33 | * Run a set of transports for a given log at a given level number, returning an array of promises for any transports that return promises 34 | * @param {Log} log - The log to run through transports 35 | * @param {number} levelNumber - The numeric notation of the level to run the transport for 36 | * @param {Array} transports - Array of transports to run 37 | * @returns {Arary} - Array of promises initiated by the runTransports function 38 | */ 39 | export const runTransports = ( 40 | log: Log, 41 | levelNumber: number | undefined, 42 | transports: Array | undefined 43 | ) => { 44 | const transportPromises: Array> = [] 45 | 46 | // Filter transports based on level number passed in 47 | if (levelNumber !== undefined && transports?.length) { 48 | transports = transports.filter( 49 | (transport) => 50 | !transport?.levelNumber || transport.levelNumber <= levelNumber 51 | ) 52 | } 53 | 54 | // Run filtered transports 55 | transports?.forEach((transport) => { 56 | let result 57 | 58 | // Run transport or log a warning if transport is misconfigured 59 | if (transport.destination) { 60 | result = transport.destination.send(log) 61 | } else if (transport.handler) { 62 | result = transport.handler(log) 63 | } else { 64 | console.warn( 65 | `Invalid Lager transport: ${JSON.stringify(transport)}. Skipping log` 66 | ) 67 | } 68 | 69 | // Push result to promises array if result is a promise 70 | if (result instanceof Promise) { 71 | transportPromises.push(result) 72 | } 73 | }) 74 | 75 | return transportPromises 76 | } 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "lib": ["es2020"], 10 | "types": ["node", "jest", "json-stringify-safe"], 11 | "baseUrl": ".", 12 | "paths": { 13 | "~/*": ["src/*"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules", "**/__tests__/*"] 18 | } 19 | --------------------------------------------------------------------------------