Save 37% off PRO during our Black Friday Sale! »

Lumberjack's extensible architecture and cross-version Angular compatibility

Lumberjack's extensible architecture and cross-version Angular compatibility

Lumberjack is a versatile and extensible logging library for Angular.

Its plugin-based architecture and cross-version compatibility with Angular and TypeScript enables features to be released across many versions of Angular and many log driver plugins from a single codebase.

We use the builder pattern to compose log objects and provide low level building blocks as well as conventional APIs for structuring Angular application logs.

Presented at:
- Angular Vienna #2, February 2021
- BeJS #11, February 2021 https://youtu.be/lcUTCTPMhEs

4f349dbe1d48627445735f7e2c818c97?s=128

Lars Gyrup Brink Nielsen

February 23, 2021
Tweet

Transcript

  1. Lumberjack's extensible architecture and cross- version Angular compatibility By Lars

    Gyrup Brink Nielsen
  2. Overview of Lumberjack

  3. Lumberjack features Lumberjack is a versatile and extensible logging library

    for Angular. • Configurable logging with 6 severity levels • Plugin-based architecture • Robust error handling • Built-in log drivers • Declarative logger base classes • An imperative Lumberjack service • Log builders • Schematics • Verified Cross-version Angular compatibility
  4. LumberjackService @Injectable({ providedIn: LumberjackRootModule }) export class LumberjackService<TPayload extends LumberjackLogPayload

    | void = void> { log(lumberjackLog: LumberjackLog<TPayload>): void; }
  5. LumberjackService usage import { LumberjackService, LumberjackTimeService } from '@ngworker/lumberjack'; @Injectable({

    providedIn: 'root' }) export class HeroService { constructor( private http: HttpClient, private lumberjack: LumberjackService, // 👈 private time: LumberjackTimeService ) {} saveHero(hero: Hero): Observable<void> { return this.http.post(`/hero/${hero.id}`, hero).pipe( tap({ error: () => this.lumberjack.log({ // 👈 createdAt: this.time.getUnixEpochTicks(), level: LumberjackLevel.Error, message: 'Failed to save hero', scope: 'Tour of Heroes App: Heroes feature', }), next: () => this.lumberjack.log({ // 👈 createdAt: this.time.getUnixEpochTicks(), level: LumberjackLevel.Info, message: 'Successfully saved hero', scope: 'Tour of Heroes App: Heroes feature', }), }), mapTo(undefined) ); } }
  6. LumberjackService usage import { LumberjackService, LumberjackTimeService } from '@ngworker/lumberjack'; @Injectable({

    providedIn: 'root' }) export class HeroService { constructor( private http: HttpClient, private lumberjack: LumberjackService, private time: LumberjackTimeService // 👈 ) {} saveHero(hero: Hero): Observable<void> { return this.http.post(`/hero/${hero.id}`, hero).pipe( tap({ error: () => this.lumberjack.log({ createdAt: this.time.getUnixEpochTicks(), // 👈 level: LumberjackLevel.Error, message: `Failed to save hero`, scope: 'Tour of Heroes App: Heroes feature', }), next: () => this.lumberjack.log({ createdAt: this.time.getUnixEpochTicks(), // 👈 level: LumberjackLevel.Info, message: 'Successfully saved hero', scope: 'Tour of Heroes App: Heroes feature', }), }), mapTo(undefined) ); } }
  7. LumberjackLog interface LumberjackLog<TPayload extends LumberjackLogPayload | void = void> {

    /** Unix epoch ticks in milliseconds representing when the log was created. */ readonly createdAt: number; /** Level of severity. */ readonly level: LumberjackLogLevel; /** Log message, for example describing an event that happened. */ readonly message: string; /** Optional payload with custom properties. * NOTE! Make sure that these properties are supported by your log drivers. */ readonly payload?: TPayload; /** Scope, for example domain, application, component, or service. */ readonly scope?: string; }
  8. LumberjackLogLevel type LumberjackLogLevel = Exclude<LumberjackLevel, LumberjackLevel.Verbose>; enum LumberjackLevel { Critical

    = 'critical', Debug = 'debug', Error = 'error', Info = 'info', Trace = 'trace', Verbose = 'verbose', Warning = 'warn', }
  9. Plugin-based architecture Lumberjack

  10. Control flow HTTP log driver Console log driver LumberjackService HeroService

  11. Flow of dependencies lumberjackLogDriver Token HTTP log driver Console log

    driver LumberjackService HeroService <I> LumberjackLog Driver
  12. Control flow with 3rd party log driver HTTP log driver

    Console log driver LumberjackService HeroService Azure Application Insights log driver
  13. Flow of dependencies with 3rd party log driver lumberjackLogDriver Token

    HTTP log driver Console log driver LumberjackService HeroService <I> LumberjackLog Driver Azure Application Insights log driver
  14. Lumberjack Application Log drivers Flow of dependencies Log drivers Log

    drivers
  15. Lumberjack registration and configuration import { LumberjackModule } from '@ngworker/lumberjack';

    // 👈 import { LumberjackConsoleDriverModule } from '@ngworker/lumberjack/console-driver'; import { LumberjackHttpDriverModule } from '@ngworker/lumberjack/http-driver'; @NgModule({ bootstrap: [AppComponent], declarations: [AppComponent], imports: [ BrowserModule, LumberjackModule.forRoot(), // 👈 LumberjackConsoleDriverModule.forRoot(), LumberjackHttpDriverModule.withOptions({ origin: 'Tour of Heroes App', retryOptions: { maxRetries: 5, delayMs: 250 }, storeUrl: '/api/logs', }), ], }) export class AppModule {}
  16. Lumberjack registration and configuration import { LumberjackModule } from '@ngworker/lumberjack';

    import { LumberjackConsoleDriverModule } from '@ngworker/lumberjack/console-driver'; // 👆 👆 import { LumberjackHttpDriverModule } from '@ngworker/lumberjack/http-driver'; @NgModule({ bootstrap: [AppComponent], declarations: [AppComponent], imports: [ BrowserModule, LumberjackModule.forRoot(), LumberjackConsoleDriverModule.forRoot(), // 👈 LumberjackHttpDriverModule.withOptions({ origin: 'Tour of Heroes App', retryOptions: { maxRetries: 5, delayMs: 250 }, storeUrl: '/api/logs', }), ], }) export class AppModule {}
  17. Lumberjack registration and configuration import { LumberjackModule } from '@ngworker/lumberjack';

    import { LumberjackConsoleDriverModule } from '@ngworker/lumberjack/console-driver'; import { LumberjackHttpDriverModule } from '@ngworker/lumberjack/http-driver'; // 👆 👆 @NgModule({ bootstrap: [AppComponent], declarations: [AppComponent], imports: [ BrowserModule, LumberjackModule.forRoot(), LumberjackConsoleDriverModule.forRoot(), LumberjackHttpDriverModule.withOptions({ // 👈 origin: 'Tour of Heroes App', retryOptions: { maxRetries: 5, delayMs: 250 }, storeUrl: '/api/logs', }), ], }) export class AppModule {}
  18. Lumberjack log drivers • Built-in: Console log driver and HTTP

    log driver • Third party log drivers are community-driven • Custom log drivers are specific to your organization or project
  19. LumberjackLogDriver interface LumberjackLogDriver<TPayload extends LumberjackLogPayload | void = void> {

    readonly config: LumberjackLogDriverConfig; logCritical(driverLog: LumberjackLogDriverLog<TPayload>): void; logDebug(driverLog: LumberjackLogDriverLog<TPayload>): void; logError(driverLog: LumberjackLogDriverLog<TPayload>): void; logInfo(driverLog: LumberjackLogDriverLog<TPayload>): void; logTrace(driverLog: LumberjackLogDriverLog<TPayload>): void; logWarning(driverLog: LumberjackLogDriverLog<TPayload>): void; }
  20. LumberjackLogDriverLog interface LumberjackLogDriverLog<TPayload extends LumberjackLogPayload | void = void> {

    /** The text representation of the log. */ readonly formattedLog: string; /** The log. Optionally supports a log payload. */ readonly log: LumberjackLog<TPayload>; }
  21. Formatted log example critical 2021-02-20T22:30:21.338Z [Forest App] The forest is

    on fire!
  22. Log levels • Default log levels • Development: All log

    levels are enabled • Production: All log levels except Debug and Trace are enabled • Default log levels can be overridden using LumberjackModule.forRoot • Log levels are configurable on a per-log driver basis
  23. Log level configuration import { LumberjackLevel, LumberjackModule } from '@ngworker/lumberjack';

    import { LumberjackConsoleDriverModule } from '@ngworker/lumberjack/console-driver'; import { LumberjackHttpDriverModule } from '@ngworker/lumberjack/http-driver'; @NgModule({ bootstrap: [AppComponent], declarations: [AppComponent], imports: [ BrowserModule, LumberjackModule.forRoot(), LumberjackConsoleDriverModule.forRoot({ levels: [LumberjackLevel.Verbose], // 👈 }), LumberjackHttpDriverModule.forRoot({ levels: [LumberjackLevel.Critical, LumberjackLevel.Error], origin: 'Tour of Heroes App', retryOptions: { maxRetries: 5, delayMs: 250 }, storeUrl: '/api/logs', }), ], }) export class AppModule {}
  24. Log level configuration import { LumberjackLevel, LumberjackModule } from '@ngworker/lumberjack';

    import { LumberjackConsoleDriverModule } from '@ngworker/lumberjack/console-driver'; import { LumberjackHttpDriverModule } from '@ngworker/lumberjack/http-driver'; @NgModule({ bootstrap: [AppComponent], declarations: [AppComponent], imports: [ BrowserModule, LumberjackModule.forRoot(), LumberjackConsoleDriverModule.forRoot({ levels: [LumberjackLevel.Verbose], }), LumberjackHttpDriverModule.forRoot({ levels: [LumberjackLevel.Critical, LumberjackLevel.Error], // 👈 origin: 'Tour of Heroes App', retryOptions: { maxRetries: 5, delayMs: 250 }, storeUrl: '/api/logs', }), ], }) export class AppModule {}
  25. Cross-version Angular and TypeScript compatibility GitHub Actions workflow

  26. The problem • 8 Angular versions • 6 TypeScript versions

    • 2 Node.js versions • 30 possible combinations of dependencies Angular CLI version Angular version Node.js version TypeScript version 9.0.7 9.0.x 10.13.x/12.11.x or later minor version 3.6.x/3.7.x 9.1.x 9.1.x 10.13.x/12.11.x or later minor version 3.6.x/3.7.x/3.8.x 10.0.8 10.0.x 10.13.x/12.11.x or later minor version 3.9.x 10.1.7 10.1.x 10.13.x/12.11.x or later minor version 3.9.x/4.0.x 10.2.x 10.2.x 10.13.x/12.11.x or later minor version 3.9.x/4.0.x 11.0.7 11.0.x 10.13.x/12.11.x or later minor version 4.0.x 11.1.x 11.1.x 10.13.x/12.11.x or later minor version 4.0.x/4.1.x 11.2.x 11.2.x 10.13.x/12.11.x or later minor version 4.0.x/4.1.x
  27. The solution • Angular CLI workspace • Single-codebase solution •

    1 demo application • 1 end-to-end application test suite • 1 schematics target application • 1 schematics end-to-end test suite • A GitHub Actions job run per combination of dependencies • Node.js scripts
  28. 50 jobs per CI run • 49 GitHub Actions job

    runs • 10-30 job runs in parallel • ~6 minutes in total
  29. 50 jobs per CI run The build, lint, and sonar

    jobs • The build job: Production library build, latest versions • The lint job: Check formatting and linting rules, latest versions • The sonar job: Generate test coverage and lint reports, then upload them to SonarCloud
  30. 50 jobs per CI run The lib job • Node.js:

    12.x • Angular versions: • 9.0.x, 9.1.x • 10.0.x, 10.1.x, 10.2.x • 11.0.x, 11.1.x, 11.2.x • TypeScript versions: • 3.7.x, 3.8.x, 3.9.x • 4.0.x, 4.1.x
  31. The lib job • 8 matrix legs: • Install Angular

    matrix leg version • Install associated TypeScript version • Run unit and integration tests for: • Development library projects • Publishable library projects • Schematics
  32. 50 jobs per CI run The app job • Node.js

    versions: • 10.x • 12.x • Angular versions: • 9.0.x, 9.1.x • 10.0.x, 10.1.x, 10.2.x • 11.0.x, 11.1.x, 11.2.x • TypeScript versions: • 3.7.x, 3.8.x, 3.9.x • 4.0.x, 4.1.x
  33. The app job • 16 matrix legs: • Install Node.js

    matrix leg version • Install Angular matrix leg version • Install associated TypeScript version • Delete local TypeScript path mappings • Download build artifact from ”build” job • Move Lumberjack package into node_modules • Run Angular Compatibility Compiler (NGCC) • Run demo application unit and integration tests • Run demo application production build
  34. 50 jobs per CI run The e2e job • Node.js:

    12.x • Angular versions: • 9.0.x, 9.1.x • 10.0.x, 10.1.x, 10.2.x • 11.0.x, 11.1.x, 11.2.x • TypeScript versions: • 3.7.x, 3.8.x, 3.9.x • 4.0.x, 4.1.x
  35. The e2e job • 8 matrix legs: • Install Angular

    matrix leg version • Install associated TypeScript version • Delete local TypeScript path mappings • Download build artifact from ”build” job • Move Lumberjack package into node_modules • Run Angular Compatibility Compiler (NGCC) • Install latest Google Chrome • Run end-to-end tests for demo application
  36. 50 jobs per CI run The schematics-e2e job • Node.js

    versions: • 10.x • 12.x • Angular versions: • 9.1.x • 10.0.x, 10.1.x, 10.2.x • 11.0.x, 11.1.x, 11.2.x • TypeScript versions: • 3.7.x, 3.8.x, 3.9.x • 4.0.x, 4.1.x
  37. The schematics-e2e job • 14 matrix legs: • Install Node.js

    matrix leg version • Install Angular matrix leg version • Install associated TypeScript version • Delete local TypeScript path mappings • Download build artifact from ”build” job • Move Lumberjack package into node_modules • Run Angular Compatibility Compiler (NGCC) • Run end-to-end tests for schematics
  38. The 50th job SonarCloud • Quality gate for new code

    • Quality gate for existing code • Quality profile • Additional SonarSource lint rules • Cyclomatic complexity analysis • Cognitive complexity analysis
  39. The 50th job SonarCloud • Reliability score • Security score

    • Maintainability score • Test coverage • Duplication detection
  40. How our GitHub workflow matrix is configured app: runs-on: ubuntu-latest

    needs: build strategy: matrix: node-version: [10.x, 12.x] # 👈 angular-version: [9.0.x, 9.1.x, 10.0.x, 10.1.x, 10.2.x, 11.0.x, 11.1.x, 11.2.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} # 👈 - name: Use Angular version ${{ matrix.angular-version }} uses: ngworker/angular-versions-action@v3 with: angular-version: ${{ matrix.angular-version }} # Yarn caching left out for brevity - run: yarn install # Intermediary steps left out for brevity - run: yarn test:ci - run: yarn build
  41. How our GitHub workflow matrix is configured app: runs-on: ubuntu-latest

    needs: build strategy: matrix: node-version: [10.x, 12.x] angular-version: [9.0.x, 9.1.x, 10.0.x, 10.1.x, 10.2.x, 11.0.x, 11.1.x, 11.2.x] # 👆 steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Use Angular version ${{ matrix.angular-version }} uses: ngworker/angular-versions-action@v3 with: angular-version: ${{ matrix.angular-version }} # 👈 # Yarn caching left out for brevity - run: yarn install # Intermediary steps left out for brevity - run: yarn test:ci - run: yarn build
  42. How our GitHub workflow matrix is configured app: runs-on: ubuntu-latest

    needs: build strategy: matrix: node-version: [10.x, 12.x] angular-version: [9.0.x, 9.1.x, 10.0.x, 10.1.x, 10.2.x, 11.0.x, 11.1.x, 11.2.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Use Angular version ${{ matrix.angular-version }} uses: ngworker/angular-versions-action@v3 # 👈 with: angular-version: ${{ matrix.angular-version }} # Yarn caching left out for brevity - run: yarn install # Intermediary steps left out for brevity - run: yarn test:ci - run: yarn build
  43. ngworker/angular-versions-action • Input parameter: angular-version • Example value: [11.0.x, 11.1.x,

    11.2.x] • Replaces Angular and related dependencies such as TypeScript in package.json • Combinations of dependencies verified to still be in working state
  44. Log builders and conventions

  45. LumberjackService usage with log object literal import { LumberjackService, LumberjackTimeService

    } from '@ngworker/lumberjack'; @Injectable({ providedIn: 'root' }) export class HeroService { constructor( private http: HttpClient, private lumberjack: LumberjackService, private time: LumberjackTimeService // 👈 ) {} saveHero(hero: Hero): Observable<void> { return this.http.post(`/hero/${hero.id}`, hero).pipe( tap({ error: () => this.lumberjack.log({ createdAt: this.time.getUnixEpochTicks(), // 👈 level: LumberjackLevel.Error, message: 'Failed to save hero', scope: 'Tour of Heroes App: Heroes feature', }), next: () => this.lumberjack.log({ createdAt: this.time.getUnixEpochTicks(), // 👈 level: LumberjackLevel.Info, message: 'Successfully saved hero', scope: 'Tour of Heroes App: Heroes feature', }), }), mapTo(undefined) ); } }
  46. LumberjackService usage with LumberjackLogFactory import { LumberjackLogFactory, LumberjackService } from

    '@ngworker/lumberjack'; @Injectable({ providedIn: 'root' }) export class HeroService { constructor( private http: HttpClient, private lumberjack: LumberjackService, private logFactory: LumberjackLogFactory // 👈 ) {} saveHero(hero: Hero): Observable<void> { return this.http.post(`/hero/${hero.id}`, hero).pipe( tap({ error: () => this.lumberjack.log( this.logFactory // 👈 .createErrorLog('Failed to save hero') .withScope('Tour of Heroes App: Heroes feature') .build()), next: () => this.lumberjack.log( this.logFactory // 👈 .createInfoLog('Successfully saved hero') .withScope('Tour of Heroes App: Heroes feature') .build()), }), mapTo(undefined) ); } }
  47. HeroesLogger import { Injectable } from '@angular/core'; import { ScopedLumberjackLogger

    } from '@ngworker/lumberjack'; // 👈 @Injectable({ providedIn: 'root' }) export class HeroesLogger extends ScopedLumberjackLogger { // 👈 readonly scope = 'Tour of Heroes App: Heroes feature'; heroSaved = this.createInfoLogger('Successfully saved hero').build(); heroSaveFailed = this.createErrorLogger('Failed to save hero').build(); }
  48. HeroesLogger import { Injectable } from '@angular/core'; import { ScopedLumberjackLogger

    } from '@ngworker/lumberjack'; @Injectable({ providedIn: 'root' }) export class HeroesLogger extends ScopedLumberjackLogger { readonly scope = 'Tour of Heroes App: Heroes feature'; // 👈 heroSaved = this.createInfoLogger('Successfully saved hero').build(); heroSaveFailed = this.createErrorLogger('Failed to save hero').build(); }
  49. HeroesLogger import { Injectable } from '@angular/core'; import { ScopedLumberjackLogger

    } from '@ngworker/lumberjack'; @Injectable({ providedIn: 'root' }) export class HeroesLogger extends ScopedLumberjackLogger { readonly scope = 'Tour of Heroes App: Heroes feature'; heroSaved = this.createInfoLogger('Successfully saved hero').build(); // 👈 heroSaveFailed = this.createErrorLogger('Failed to save hero').build(); // 👈 }
  50. HeroesLogger usage import { HeroesLogger } from './heroes-logger.service'; // 👈

    @Injectable({ providedIn: 'root' }) class HeroService { constructor(private http: HttpClient, private logger: HeroesLogger/*👈*/) {} saveHero(hero: Hero): Observable<void> { return this.http.post(`/hero/${hero.id}`, hero).pipe( tap({ error: () => this.logger.heroSaveFailed(), next: () => this.logger.heroSaved(), }), mapTo(undefined) ); } }
  51. HeroesLogger usage import { HeroesLogger } from './heroes-logger.service'; @Injectable({ providedIn:

    'root' }) class HeroService { constructor(private http: HttpClient, private logger: HeroesLogger) {} saveHero(hero: Hero): Observable<void> { return this.http.post(`/hero/${hero.id}`, hero).pipe( tap({ error: () => this.logger.heroSaveFailed(), // 👈 next: () => this.logger.heroSaved(), // 👈 }), mapTo(undefined) ); } }
  52. Conclusion

  53. Plugin-based architecture • Application code is independent of logging configuration

    • Application code is independent of logging providers • Bundled with console log driver • Bundled with HTTP log driver • Support for 3rd party log drivers • Support for custom log drivers • Built-in error handling • Fast log processing Lumberjack Application Log drivers Log drivers Log drivers
  54. Cross-version compatibility • Simple single-codebase solution • Purpose-built GitHub Action

    for Angular dependency management in CI workflows • Fast, parallellized GitHub Actions workflow • Each combination of dependencies is run in isolation for: • Unit and integration tests • End-to-end tests • Schematics end-to-end tests
  55. Cross-version compatibility • We release features and patches across 8

    Angular versions, 6 TypeScript versions, and 2 Node.js versions from a single codebase • Backward-incompatible API and syntax usage is immediately detected • Verified support for new Angular, TypeScript, and Node.js versions is usually as simple as adding a value to a list parameter in our CI workflow
  56. Building blocks for creating and structuring logs • LumberjackLogBuilder class

    • LumberjackLogFactory service • LumberjackLogger base service class • LumberScopedLogger base service class
  57. Lumberjack logger services • Conventional structuring of logged system events

    • Encapsulates implementation details • Abstraction layer between application code and Lumberjack logging • Logging level, log scope, log message, and static log payload can be changed without touching application code
  58. Thank you 👋 🐦 @LayZeeDK ng add @ngworker/lumberjack

  59. None