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's extensible
    architecture and cross-
    version Angular compatibility
    By Lars Gyrup Brink Nielsen

    View Slide

  2. Overview of Lumberjack

    View Slide

  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

    View Slide

  4. LumberjackService
    @Injectable({ providedIn: LumberjackRootModule })
    export class LumberjackService {
    log(lumberjackLog: LumberjackLog): void;
    }

    View Slide

  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 {
    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)
    );
    }
    }

    View Slide

  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 {
    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)
    );
    }
    }

    View Slide

  7. LumberjackLog
    interface LumberjackLog {
    /** 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;
    }

    View Slide

  8. LumberjackLogLevel
    type LumberjackLogLevel = Exclude;
    enum LumberjackLevel {
    Critical = 'critical',
    Debug = 'debug',
    Error = 'error',
    Info = 'info',
    Trace = 'trace',
    Verbose = 'verbose',
    Warning = 'warn',
    }

    View Slide

  9. Plugin-based architecture
    Lumberjack

    View Slide

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

    View Slide

  11. Flow of dependencies
    lumberjackLogDriver
    Token
    HTTP log driver
    Console log driver
    LumberjackService
    HeroService
    LumberjackLog
    Driver

    View Slide

  12. Control flow with 3rd party log driver
    HTTP log driver
    Console log driver
    LumberjackService
    HeroService
    Azure Application
    Insights log driver

    View Slide

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

    View Slide

  14. Lumberjack
    Application
    Log drivers
    Flow of dependencies
    Log drivers
    Log drivers

    View Slide

  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 {}

    View Slide

  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 {}

    View Slide

  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 {}

    View Slide

  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

    View Slide

  19. LumberjackLogDriver
    interface LumberjackLogDriver {
    readonly config: LumberjackLogDriverConfig;
    logCritical(driverLog: LumberjackLogDriverLog): void;
    logDebug(driverLog: LumberjackLogDriverLog): void;
    logError(driverLog: LumberjackLogDriverLog): void;
    logInfo(driverLog: LumberjackLogDriverLog): void;
    logTrace(driverLog: LumberjackLogDriverLog): void;
    logWarning(driverLog: LumberjackLogDriverLog): void;
    }

    View Slide

  20. LumberjackLogDriverLog
    interface LumberjackLogDriverLog {
    /** The text representation of the log. */
    readonly formattedLog: string;
    /** The log. Optionally supports a log payload. */
    readonly log: LumberjackLog;
    }

    View Slide

  21. Formatted log example
    critical 2021-02-20T22:30:21.338Z [Forest App] The forest is on fire!

    View Slide

  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

    View Slide

  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 {}

    View Slide

  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 {}

    View Slide

  25. Cross-version Angular and
    TypeScript compatibility
    GitHub Actions workflow

    View Slide

  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

    View Slide

  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

    View Slide

  28. 50 jobs per CI run
    • 49 GitHub Actions job runs
    • 10-30 job runs in parallel
    • ~6 minutes in total

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  39. The 50th job
    SonarCloud
    • Reliability score
    • Security score
    • Maintainability score
    • Test coverage
    • Duplication detection

    View Slide

  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/[email protected]
    - name: Use Node.js ${{ matrix.node-version }}
    uses: actions/[email protected]
    with:
    node-version: ${{ matrix.node-version }} # 👈
    - name: Use Angular version ${{ matrix.angular-version }}
    uses: ngworker/[email protected]
    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

    View Slide

  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/[email protected]
    - name: Use Node.js ${{ matrix.node-version }}
    uses: actions/[email protected]
    with:
    node-version: ${{ matrix.node-version }}
    - name: Use Angular version ${{ matrix.angular-version }}
    uses: ngworker/[email protected]
    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

    View Slide

  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/[email protected]
    - name: Use Node.js ${{ matrix.node-version }}
    uses: actions/[email protected]
    with:
    node-version: ${{ matrix.node-version }}
    - name: Use Angular version ${{ matrix.angular-version }}
    uses: ngworker/[email protected] # 👈
    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

    View Slide

  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

    View Slide

  44. Log builders and conventions

    View Slide

  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 {
    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)
    );
    }
    }

    View Slide

  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 {
    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)
    );
    }
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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(); // 👈
    }

    View Slide

  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 {
    return this.http.post(`/hero/${hero.id}`, hero).pipe(
    tap({
    error: () => this.logger.heroSaveFailed(),
    next: () => this.logger.heroSaved(),
    }),
    mapTo(undefined)
    );
    }
    }

    View Slide

  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 {
    return this.http.post(`/hero/${hero.id}`, hero).pipe(
    tap({
    error: () => this.logger.heroSaveFailed(), // 👈
    next: () => this.logger.heroSaved(), // 👈
    }),
    mapTo(undefined)
    );
    }
    }

    View Slide

  52. Conclusion

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  56. Building blocks for creating and
    structuring logs
    • LumberjackLogBuilder class
    • LumberjackLogFactory service
    • LumberjackLogger base service class
    • LumberScopedLogger base service class

    View Slide

  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

    View Slide

  58. Thank you 👋
    🐦 @LayZeeDK
    ng add @ngworker/lumberjack

    View Slide

  59. View Slide