Slide 1

Slide 1 text

Lumberjack's extensible architecture and cross- version Angular compatibility By Lars Gyrup Brink Nielsen

Slide 2

Slide 2 text

Overview of Lumberjack

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Plugin-based architecture Lumberjack

Slide 10

Slide 10 text

Control flow HTTP log driver Console log driver LumberjackService HeroService

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Lumberjack Application Log drivers Flow of dependencies Log drivers Log drivers

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Cross-version Angular and TypeScript compatibility GitHub Actions workflow

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Log builders and conventions

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Conclusion

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

No content