| |

Angular 38 🅰️ Logging and Observability

Logging is the practice of recording what an application did. Observability is the broader discipline of understanding what an application is doing — through logs, metrics, traces, and the context that ties them together. In a front-end application, observability is often treated as an afterthought, because the developer’s console is right there and errors are visible. But in production, the console is not visible. The user sees a broken screen, and the developer sees nothing unless the application has been instrumented to report. This chapter covers logging and observability in Angular: the levels and categories of log messages, the design of a logging service that is injectable, configurable, and testable, the integration with error handling from Angular 37, the role of interceptors in request tracing, and the practices that make production issues diagnosable. It treats logging as infrastructure — something that is designed once and used everywhere.

Key point: A logging service is an injectable that exposes methods for each log level, accepts structured data alongside the message, and forwards entries to one or more sinks — the console in development, a remote endpoint in production. Log levels are configuration: development enables debug and above, production enables warn and above. Structured logs carry context — a correlation ID, the current user, the route, the feature — that turns a message into a diagnosable event. Error handling (Angular 37) produces log entries; the logging service consumes them. The two are separate concerns that connect at the ErrorHandler.


Why front-end observability matters

Backend observability is well-established. Servers log requests, metrics dashboards show latency, distributed traces follow a request across services. Front-end observability has historically lagged, for understandable reasons — the browser is a hostile environment, the user’s device is not controlled, and the network is unreliable. But the front end is where the user experiences failures, and without instrumentation, those failures are invisible.

The three pillars. Observability rests on logs, metrics, and traces. Logs are discrete events with context. Metrics are numeric measurements over time — request counts, error rates, timing distributions. Traces follow a single operation through multiple systems, connecting the front-end request to the backend call to the database query. Angular applications participate in all three, and the logging service is the foundation.

Why logs alone are not enough. A log entry says “an error occurred at 14:32.” A metric says “error rate increased 40% in the last hour.” A trace says “this specific user’s request failed at the database call.” Each answers a different question. The logging service produces the raw data; the metrics and traces are derived from it or produced alongside it.

Why the front-end is uniquely difficult. The front-end runs on hardware the developer does not control, in a browser version that may be old, on a network that may be flaky. Errors that occur on one user’s machine do not occur on another’s. Session replay, error reporting services, and structured logging are the tools that make these cases visible. Without them, the developer is debugging blind.

Why the logging service is the entry point. Every observability practice starts with recording what happened. The logging service is the single place where application code sends messages, and it is the layer that decides what to do with them — write to console, buffer for batching, send to a remote endpoint, attach context. Building it well is the prerequisite for everything else.

Why a logging service is not console.log. console.log is a debugging tool, not an observability tool. It has no levels, no structure, no remote sink, and no way to be disabled in production. A logging service adds those. The cost is a small amount of infrastructure; the benefit is that production issues are diagnosable.


Log levels and their meaning

A log level indicates the severity and purpose of a message. The conventional levels, from least to most severe, are debug, info, warn, and error. Some systems add trace and fatal.

LevelPurposeProduction
traceFine-grained debuggingOff
debugDevelopment diagnosticsOff
infoNormal operations worth recordingOn (sparingly)
warnSomething unexpected but recoverableOn
errorA failureOn
fatalThe application cannot continueOn

Why the level matters. The level determines whether the message is emitted. In development, debug and above are enabled, so the developer sees everything. In production, info and above are enabled, so the volume is lower and the signal is higher. The level is a filter, and the filter is configuration.

Why info should be used sparingly in production. Every info message that fires on every request is noise. A log that records “user opened the page” for every user is not useful — it is a metric, and metrics are the right tool. info is for events that are rare and meaningful: application startup, configuration loaded, a significant state transition. The distinction between info and debug is not severity but volume.

Why warn and error are the production default. These are the messages that indicate something went wrong or is about to. They are the ones worth alerting on and worth reviewing. A production log at warn and above should be small enough to read and rich enough to diagnose.

Why the level should be configurable at runtime. A hardcoded level requires a rebuild to change. A level read from environment configuration can be adjusted per deployment. For a debugging session in production, the ability to raise the level temporarily — via a query parameter, a feature flag, or a remote configuration — is invaluable.


Designing a logging service

The logging service is an injectable with a method per level. Each method accepts a message and optional structured data, and the service decides what to do with the entry.

export type LogLevel = 'debug' | 'info' | 'warn' | 'error';

export interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: number;
  context?: Record<string, unknown>;
  error?: unknown;
}

@Injectable({ providedIn: 'root' })
export class LoggingService {
  private readonly minLevel = signal<LogLevel>(
    environment.production ? 'warn' : 'debug',
  );

  debug(message: string, context?: Record<string, unknown>): void {
    this.log('debug', message, context);
  }

  info(message: string, context?: Record<string, unknown>): void {
    this.log('info', message, context);
  }

  warn(message: string, context?: Record<string, unknown>): void {
    this.log('warn', message, context);
  }

  error(message: string, error?: unknown, context?: Record<string, unknown>): void {
    this.log('error', message, { ...context, error: this.serialize(error) });
  }

  private log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
    if (!this.isEnabled(level)) return;
    const entry: LogEntry = {
      level,
      message,
      timestamp: Date.now(),
      context,
    };
    this.write(entry);
  }

  private isEnabled(level: LogLevel): boolean {
    const order: LogLevel[] = ['debug', 'info', 'warn', 'error'];
    return order.indexOf(level) >= order.indexOf(this.minLevel());
  }

  private write(entry: LogEntry): void {
    // console in development, remote sink in production
    if (!environment.production) {
      console[entry.level === 'debug' ? 'log' : entry.level](
        `[${entry.level.toUpperCase()}] ${entry.message}`,
        entry.context ?? '',
      );
    } else {
      this.enqueue(entry);
    }
  }

  private enqueue(_entry: LogEntry): void {
    // buffer and send to remote endpoint
  }

  private serialize(error: unknown): unknown {
    if (error instanceof Error) {
      return { name: error.name, message: error.message, stack: error.stack };
    }
    return error;
  }
}

The service has a method per level, a private log that checks the level and constructs the entry, and a private write that decides where the entry goes. In development, it writes to the console. In production, it enqueues for remote delivery.

Why the level check is centralized. The isEnabled method compares the requested level against the configured minimum. Putting the check in one place means adding a new level or changing the ordering is a single change. It also means the level is checked once, at the start of the log call, rather than in each method.

Why the entry is structured. The LogEntry has fields for level, message, timestamp, and context. A structured entry can be serialized to JSON and sent to a remote endpoint, where it can be indexed and queried. A free-text log line cannot. The structure is what makes the log machine-readable.

Why error serialization is explicit. An Error object does not serialize well — JSON.stringify(new Error('x')) produces {} because message and stack are non-enumerable. The serialize method extracts the useful fields into a plain object that serializes correctly. This is a detail that matters when the log is sent remotely.

Why the service uses inject and is providedIn: 'root'. The service is a singleton, injected wherever needed. Components and services depend on it, and there is one instance per application. This is the standard Angular service pattern, and it makes the logging service a natural dependency for other services.

Why the logging service should not depend on HTTP. If the logging service used HttpClient to send entries, and an interceptor logged each request, the logging would produce requests that would themselves be logged — an infinite loop. The logging service should send via a channel that bypasses the HTTP client’s interception, such as the native fetch or a sendBeacon call. This is a subtle but critical detail.


Context in log entries

A message without context is a fact without meaning. “Request failed” is less useful than “Request failed for user alice, route /orders, correlation ID abc123.” The logging service should attach context automatically where possible, and accept context explicitly where it is known.

Automatic context. The service can attach a correlation ID, the current route, the application version, and the user’s session identifier. These are the same for many entries and are worth attaching without the caller’s involvement.

private baseContext(): Record<string, unknown> {
  return {
    correlationId: this.correlationId,
    route: this.router.url,
    version: environment.version,
  };
}

Correlation IDs. A correlation ID is a value that is generated when an operation begins and attached to every log entry and every HTTP request that the operation produces. On the backend, the same ID appears in the server’s logs, so a front-end error can be matched to the backend request that caused it.

@Injectable({ providedIn: 'root' })
export class CorrelationService {
  private readonly id = crypto.randomUUID();
  readonly value = this.id;
}

The ID is generated once per session or per operation, and the interceptor from Angular 36 adds it to every request as a header. The backend echoes it, and the front-end log entries carry it. The result is a trail that spans the boundary.

User and session context. The current user’s ID (not their name or email, to avoid logging personal data) and the session ID are useful for grouping entries. They should be attached by the service, not passed by each caller.

Feature context. When a log entry is produced by a specific feature, the feature name or module is useful context. This can be passed explicitly by the caller or provided through an injection token.

Why context should be a flat object. Nested objects are harder to index and query in a log aggregation system. A flat object with string keys and primitive values is the standard. If nested data is needed, it can be serialized to a string, but the flat form is preferred.

Why personal data should not be logged. Logs are often less protected than databases. Logging an email address, a token, or a credit card number creates a compliance risk. The rule is to log identifiers, not personal data, and to redact anything sensitive before the entry leaves the service.


Connecting logging to error handling

The ErrorHandler from Angular 37 is the natural place to connect error handling to the logging service. The handler catches errors that Angular sees, and the logging service records them.

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
  private readonly logger = inject(LoggingService);

  handleError(error: unknown): void {
    const message = error instanceof Error ? error.message : String(error);
    this.logger.error(message, error, { source: 'ErrorHandler' });
  }
}

The handler extracts a message, passes the original error for serialization, and tags the entry with its source. The entry lands in the logging service, which writes it to the console in development and sends it remotely in production.

Why the source tag is useful. Errors can come from the ErrorHandler, from an HTTP interceptor, from a service’s own catchError, or from a global listener. Tagging the source allows filtering and analysis — “how many errors come from the global listener?” is a question worth answering.

Why the handler should not throw. If the handler throws, the error handling breaks. The handler must log and return. If logging itself fails — the remote endpoint is down, the buffer is full — the failure should be silent. An error in the error path is worse than the original error.

Why the handler should not show user feedback. The ErrorHandler has no context for a user-facing message. It logs. The component that initiated the operation is the one that shows feedback, because it knows what the user was doing.

Why the logging service and the ErrorHandler are separate. The logging service is a general-purpose tool that any code can call. The ErrorHandler is a specific integration point for Angular’s error mechanism. Keeping them separate means the logging service can be used in services, components, and interceptors without involving the error handler, and the error handler can evolve independently.


Request tracing with interceptors

The HTTP interceptor from Angular 36 is the natural place to log requests and responses, attach correlation IDs, and measure timing.

export const tracingInterceptor: HttpInterceptorFn = (req, next) => {
  const logger = inject(LoggingService);
  const correlation = inject(CorrelationService);
  const start = performance.now();

  const traced = req.clone({
    setHeaders: { 'X-Correlation-Id': correlation.value },
  });

  return next(traced).pipe(
    tap({
      next: (event) => {
        if (event.type === HttpEventType.Response) {
          logger.debug(`${req.method} ${req.url} completed`, {
            durationMs: Math.round(performance.now() - start),
            status: event.status,
          });
        }
      },
      error: (error: HttpErrorResponse) => {
        logger.error(`${req.method} ${req.url} failed`, error, {
          durationMs: Math.round(performance.now() - start),
          status: error.status,
        });
      },
    }),
  );
};

The interceptor clones the request with a correlation ID header, times the request, and logs on both success and failure. The timing is in milliseconds, the status is included, and the correlation ID is attached by the logging service’s base context.

Why timing belongs in the interceptor. A request’s duration is measured once, at the interception point, and is relevant to every request. Measuring it in each service method would duplicate the logic. The interceptor is the natural place.

Why the correlation ID is added here. The interceptor runs for every request, so attaching the header here means every request carries it. The backend can log it, and the front-end log entries can be matched to the backend’s.

Why the log level is debug for success and error for failure. Successful requests are not interesting in production — they are the norm. Failed requests are. Logging every successful request at info would flood the log. debug keeps them available in development and silent in production.

Why the interceptor should not log the body. Request and response bodies may contain sensitive data — passwords, tokens, personal information. Logging the URL and status is safe; logging the body is not. If the body must be logged for debugging, it should be redacted first.


Sinks: where log entries go

A sink is a destination for log entries. The logging service writes to one or more sinks. The choice of sink depends on the environment and the purpose.

SinkDevelopmentProduction
Console⚠️ (limited)
Remote endpoint
Local buffer✅ (before send)
Browser storage⚠️ (limited)

Console. The browser console is the developer’s immediate feedback. It is useful in development and limited in production, because the user does not open the console and the entries are not persisted. In production, the console can still receive entries for the developer’s own debugging, but it is not the primary sink.

Remote endpoint. A remote endpoint receives entries over the network and stores them in a log aggregation system — Sentry, Datadog, LogRocket, a custom service. This is the production sink. It must be reliable, but it must also fail silently — if the endpoint is down, the application must continue.

Batching. Sending each entry individually is expensive. Batching collects entries over a short interval and sends them in one request. The tradeoff is latency — an entry is not sent immediately — versus efficiency. Batching is standard for remote sinks.

Buffering. Entries produced when the network is unavailable are buffered in memory and sent when the network returns. The buffer has a maximum size, and entries beyond it are dropped. Dropping is acceptable; blocking is not.

Why multiple sinks are common. In development, the console is the primary sink. In production, the console is still useful for the developer’s own debugging, and the remote endpoint is the primary sink. The service writes to both, and the configuration determines which is active.

Why the sink must not throw. A logging call that throws because the sink failed is worse than no logging. The write path must catch its own errors and continue. The application should never fail because logging failed.

Why sendBeacon is useful for the final flush. When the page is unloading, a normal fetch may be canceled. navigator.sendBeacon sends a request that the browser guarantees to complete. It is the right tool for flushing the buffer when the user navigates away. The limitation is that it is fire-and-forget — no response is received.


Complete Example Session

import { Injectable, ErrorHandler, inject, signal, ApplicationConfig } from '@angular/core';
import { HttpInterceptorFn, HttpErrorResponse, HttpEventType } from '@angular/common/http';
import { tap } from 'rxjs';
import { environment } from './environments/environment';

// ============================================
// PART 1: LOG TYPES
// ============================================

export type LogLevel = 'debug' | 'info' | 'warn' | 'error';

export interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: number;
  context?: Record<string, unknown>;
}

// ============================================
// PART 2: CORRELATION SERVICE
// ============================================

@Injectable({ providedIn: 'root' })
export class CorrelationService {
  readonly value = crypto.randomUUID();
}

// ============================================
// PART 3: LOGGING SERVICE
// ============================================

@Injectable({ providedIn: 'root' })
export class LoggingService {
  private readonly correlation = inject(CorrelationService);
  private readonly minLevel = signal<LogLevel>(
    environment.production ? 'warn' : 'debug',
  );

  debug(message: string, context?: Record<string, unknown>): void {
    this.log('debug', message, context);
  }

  info(message: string, context?: Record<string, unknown>): void {
    this.log('info', message, context);
  }

  warn(message: string, context?: Record<string, unknown>): void {
    this.log('warn', message, context);
  }

  error(message: string, error?: unknown, context?: Record<string, unknown>): void {
    this.log('error', message, { ...context, error: this.serialize(error) });
  }

  private log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
    if (!this.isEnabled(level)) return;
    const entry: LogEntry = {
      level,
      message,
      timestamp: Date.now(),
      context: { ...this.baseContext(), ...context },
    };
    this.write(entry);
  }

  private isEnabled(level: LogLevel): boolean {
    const order: LogLevel[] = ['debug', 'info', 'warn', 'error'];
    return order.indexOf(level) >= order.indexOf(this.minLevel());
  }

  private baseContext(): Record<string, unknown> {
    return { correlationId: this.correlation.value };
  }

  private write(entry: LogEntry): void {
    if (!environment.production) {
      const method = entry.level === 'debug' ? 'log' : entry.level;
      console[method](`[${entry.level.toUpperCase()}] ${entry.message}`, entry.context ?? '');
    } else if (entry.level === 'error' || entry.level === 'warn') {
      this.send(entry);
    }
  }

  private send(entry: LogEntry): void {
    void fetch('/api/logs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry),
      keepalive: true,
    }).catch(() => {
      // silent — logging must never break the app
    });
  }

  private serialize(error: unknown): unknown {
    if (error instanceof Error) {
      return { name: error.name, message: error.message, stack: error.stack };
    }
    return error;
  }
}

// ============================================
// PART 4: GLOBAL ERROR HANDLER
// ============================================

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
  private readonly logger = inject(LoggingService);

  handleError(error: unknown): void {
    const message = error instanceof Error ? error.message : String(error);
    this.logger.error(message, error, { source: 'ErrorHandler' });
  }
}

// ============================================
// PART 5: TRACING INTERCEPTOR
// ============================================

export const tracingInterceptor: HttpInterceptorFn = (req, next) => {
  const logger = inject(LoggingService);
  const correlation = inject(CorrelationService);
  const start = performance.now();

  const traced = req.clone({
    setHeaders: { 'X-Correlation-Id': correlation.value },
  });

  return next(traced).pipe(
    tap({
      next: (event) => {
        if (event.type === HttpEventType.Response) {
          logger.debug(`${req.method} ${req.url}`, {
            durationMs: Math.round(performance.now() - start),
            status: event.status,
          });
        }
      },
      error: (error: HttpErrorResponse) => {
        logger.error(`${req.method} ${req.url} failed`, error, {
          durationMs: Math.round(performance.now() - start),
          status: error.status,
        });
      },
    }),
  );
};

// ============================================
// PART 6: APPLICATION CONFIGURATION
// ============================================

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: ErrorHandler, useClass: GlobalErrorHandler },
    provideHttpClient(withInterceptors([tracingInterceptor])),
  ],
};

// ============================================
// PART 7: USAGE IN A SERVICE
// ============================================

@Injectable({ providedIn: 'root' })
export class OrderService {
  private readonly http = inject(HttpClient);
  private readonly logger = inject(LoggingService);

  placeOrder(order: Order): Observable<Receipt> {
    this.logger.info('Placing order', { orderId: order.id });
    return this.http.post<Receipt>('/api/orders', order).pipe(
      catchError((error: HttpErrorResponse) => {
        this.logger.error('Order failed', error, { orderId: order.id });
        return throwError(() => error);
      }),
    );
  }
}

The example shows the correlation service, the logging service with levels and sinks, the global error handler integration, the tracing interceptor, the application configuration, and usage in a service.


Quick Reference

Log Levels

LevelVolumeProduction
debugHighOff
infoMediumSparingly
warnLowOn
errorLowOn

Logging Service API

MethodPurpose
debug(msg, ctx?)Development diagnostics
info(msg, ctx?)Normal operations
warn(msg, ctx?)Unexpected but recoverable
error(msg, err?, ctx?)Failures

Context Fields

FieldSource
correlationIdCorrelationService
routeRouter
versionEnvironment
userIdAuthService
errorSerialized error

Sinks

SinkUse
ConsoleDevelopment
Remote endpointProduction
BufferBefore send
sendBeaconOn unload

Integration Points

PointPurpose
ErrorHandlerAngular-caught errors
InterceptorRequest tracing
Service catchErrorOperation-specific
Global listenersEscaped errors

Best Practices

Do This:

// One logging service, injected everywhere
private readonly logger = inject(LoggingService);              // ✅

// Structured entries with context
this.logger.error('Order failed', error, { orderId: order.id }); // ✅

// Serialize errors explicitly
private serialize(error: unknown): unknown { ... }             // ✅

// Attach correlation IDs automatically
context: { ...this.baseContext(), ...context }                 // ✅

// Log level as configuration
environment.production ? 'warn' : 'debug'                      // ✅

// Silent failure in the send path
fetch(...).catch(() => {})                                     // ✅

// Redact sensitive data before logging
{ userId: user.id }  // not the user object                    // ✅

Don’t Do This:

// Don't use console.log in production code
console.log('user clicked', user);                              // ⚠️

// Don't log every successful request at info
logger.info('GET /api/users completed');  // floods              // ⚠️

// Don't log personal data
logger.info('Login', { email, password });                      // ⚠️

// Don't let logging throw
logger.error('x');  // if the sink fails, the app breaks        // ⚠️

// Don't log via HttpClient
// The logging request would be logged — infinite loop           // ⚠️

// Don't skip correlation IDs
// Without them, front-end and back-end logs cannot be matched   // ⚠️

// Don't log the request body
{ body: req.body }  // may contain tokens or passwords          // ⚠️

Common Pitfalls

PitfallProblemSolution
console.log everywhereNo levels, no sinkUse the service
Logging too muchNoise hides signalRaise the level
Personal data loggedCompliance riskLog IDs, not data
Error not serialized{} in the logExtract fields
No correlation IDCannot match FE/BEAttach automatically
Logging via HttpClientInfinite loopUse fetch
Sink throwsApp breaksCatch and continue
No level filterEverything loggedCheck the level
Buffer unboundedMemory growthCap and drop
No flush on unloadLost entriesUse sendBeacon

Real-World Examples

1. Correlation ID in interceptor

const traced = req.clone({ setHeaders: { 'X-Correlation-Id': correlation.value } });

2. Error serialization

{ name: error.name, message: error.message, stack: error.stack }

3. Error handler integration

handleError(error) { this.logger.error('Unhandled', error, { source: 'ErrorHandler' }); }

4. Request timing

durationMs: Math.round(performance.now() - start)

5. Level filtering

isEnabled(level) { return order.indexOf(level) >= order.indexOf(minLevel()); }

6. Remote send with keepalive

fetch('/api/logs', { method: 'POST', body, keepalive: true });

7. Batching

// collect entries, send every 5s or every 20 entries

8. Feature context

this.logger.info('Checkout started', { feature: 'checkout', step: 1 });

9. Route context

{ route: this.router.url }

10. Version context

{ version: environment.version }

Visual: Logging Architecture

┌──────────────────────────────────────────────────────────┐
│  COMPONENT                                               │
│    │                                                     │
│    │  logger.info('...', context)                        │
│    ▼                                                     │
│  LOGGING SERVICE                                         │
│    │                                                     │
│    ├── check level                                       │
│    ├── attach base context (correlationId, route, ...)   │
│    ├── build LogEntry                                    │
│    │                                                     │
│    ▼                                                     │
│  SINKS                                                   │
│    │                                                     │
│    ├── Console (dev)                                     │
│    │                                                     │
│    └── Remote endpoint (prod)                            │
│           │                                              │
│           ├── buffer                                     │
│           ├── batch                                      │
│           └── send (fetch / sendBeacon)                  │
│                                                          │
│  Errors from the ErrorHandler feed into the same path.   │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Log Levels

┌──────────────────────────────────────────────────────────┐
│  Volume                                                  │
│    ▲                                                     │
│    │  ┌──────────────────┐                               │
│    │  │  trace / debug   │  development only             │
│    │  └──────────────────┘                               │
│    │  ┌──────────────────┐                               │
│    │  │  info            │  rare, meaningful events      │
│    │  └──────────────────┘                               │
│    │  ┌──────────────────┐                               │
│    │  │  warn            │  recoverable issues           │
│    │  └──────────────────┘                               │
│    │  ┌──────────────────┐                               │
│    │  │  error           │  failures                     │
│    │  └──────────────────┘                               │
│    └──────────────────────────────────────────► Severity│
│                                                          │
│  Production threshold is usually warn and above.         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Correlation Across Boundaries

┌──────────────────────────────────────────────────────────┐
│  BROWSER                                                 │
│    │                                                     │
│    │  correlationId = "abc-123"                          │
│    │                                                     │
│    ├── logger.error('Order failed', { correlationId })   │
│    │                                                     │
│    └── HTTP request                                      │
│          Header: X-Correlation-Id: abc-123               │
│                │                                         │
│                ▼                                         │
│  SERVER                                                  │
│    │                                                     │
│    ├── logs "abc-123" for the request                    │
│    │                                                     │
│    └── response                                          │
│                                                          │
│  Log aggregation joins the front-end and back-end        │
│  entries by correlation ID.                              │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Batching and Buffering

┌──────────────────────────────────────────────────────────┐
│  Entries produced:                                       │
│    e1  e2  e3  e4  ...  e10                              │
│                                                          │
│  Buffer:                                                 │
│    [e1, e2, e3, e4, ..., e10]                            │
│                                                          │
│  Batch trigger:                                          │
│    every 5 seconds                                       │
│    OR every 20 entries                                   │
│                                                          │
│  Send:                                                   │
│    POST /api/logs  body: [e1..e10]                       │
│                                                          │
│  On failure:                                             │
│    keep in buffer, retry on next tick                    │
│                                                          │
│  On page unload:                                         │
│    navigator.sendBeacon('/api/logs', buffer)             │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: Error Flow Into Logging

┌──────────────────────────────────────────────────────────┐
│  Template binding error                                  │
│       │                                                  │
│       ▼                                                  │
│  Angular ErrorHandler                                    │
│       │                                                  │
│       └──► LoggingService.error(...)                     │
│                                                          │
│  HTTP error                                              │
│       │                                                  │
│       ▼                                                  │
│  Interceptor catchError                                  │
│       │                                                  │
│       └──► LoggingService.error(...)                     │
│                                                          │
│  Service-level error                                     │
│       │                                                  │
│       ▼                                                  │
│  catchError in the service                               │
│       │                                                  │
│       └──► LoggingService.error(...)                     │
│                                                          │
│  All paths converge on the logging service.              │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

AspectValue
Levelsdebug, info, warn, error
Production thresholdwarn and above
ServiceInjectable, one instance
Entry shapeLevel, message, timestamp, context
ContextCorrelation ID, route, version, user
SinksConsole (dev), remote (prod)
IntegrationErrorHandler, interceptors, services
Transportfetch with keepalive, sendBeacon
Failure modeSilent — never break the app
RedactionIDs, not personal data

Key takeaways:

  • A logging service is infrastructure — designed once, injected everywhere, and configured by environment
  • Log levels are filters — development enables debug and above, production enables warn and above, and the level is configuration rather than code
  • Entries are structured — level, message, timestamp, and context, so they can be serialized, indexed, and queried
  • Context is what makes a log useful — a correlation ID, the current route, the application version, and the user’s ID turn a message into a diagnosable event
  • Error handling feeds logging — the ErrorHandler, interceptors, and service-level catchError all produce entries through the same service
  • Correlation IDs span boundaries — the front-end and back-end logs can be joined if the same ID appears in both
  • Sinks are layered — the console in development, a remote endpoint in production, with batching and buffering to make the remote path efficient and resilient
  • The send path must fail silently — logging that breaks the application is worse than no logging
  • Sensitive data should never be logged — IDs and statuses are safe, personal data and tokens are not
  • The logging service should bypass HttpClient — using the HTTP client to send logs would produce requests that are themselves logged, creating a loop

Remember: Logging is not console.log. It is a designed service with levels, structure, context, and sinks, and it is the foundation of front-end observability. The application that logs well is the application that can be diagnosed in production, where the developer’s console is not available and the user’s report is the only signal. Build the service once, inject it everywhere, and let the error handling and interceptors feed it.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!