Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Lars Gyrup Brink Nielsen

February 23, 2021
Tweet

More Decks by Lars Gyrup Brink Nielsen

Other Decks in Programming

Transcript

  1. 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
  2. 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) ); } }
  3. 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) ); } }
  4. 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; }
  5. LumberjackLogLevel type LumberjackLogLevel = Exclude<LumberjackLevel, LumberjackLevel.Verbose>; enum LumberjackLevel { Critical

    = 'critical', Debug = 'debug', Error = 'error', Info = 'info', Trace = 'trace', Verbose = 'verbose', Warning = 'warn', }
  6. Flow of dependencies lumberjackLogDriver Token HTTP log driver Console log

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

    Console log driver LumberjackService HeroService Azure Application Insights log driver
  8. 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
  9. 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 {}
  10. 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 {}
  11. 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 {}
  12. 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
  13. 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; }
  14. 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>; }
  15. 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
  16. 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 {}
  17. 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 {}
  18. 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
  19. 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
  20. 50 jobs per CI run • 49 GitHub Actions job

    runs • 10-30 job runs in parallel • ~6 minutes in total
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. The 50th job SonarCloud • Reliability score • Security score

    • Maintainability score • Test coverage • Duplication detection
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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) ); } }
  37. 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) ); } }
  38. 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(); }
  39. 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(); }
  40. 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(); // 👈 }
  41. 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) ); } }
  42. 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) ); } }
  43. 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
  44. 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
  45. 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
  46. Building blocks for creating and structuring logs • LumberjackLogBuilder class

    • LumberjackLogFactory service • LumberjackLogger base service class • LumberScopedLogger base service class
  47. 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