Skip to content

Observability

The adapter ships with a two-tier observability system built around a clear separation of concerns:

  • Observers — read-only hooks for metrics, structured logging, and distributed tracing
  • Interceptors — read-write hooks for mutating requests/responses

Use observers when you need to observe. Use interceptors when you need to mutate.

HttpAdapterObserver

Attach a single observer to the adapter via .withObserver() on the builder.

HookWhen it fires
onRequestStart(request)After all request interceptors, immediately before the HTTP call
onRequestSuccess(response, durationMs)After a successful response (includes total time across retries)
onRequestFailure(error, durationMs)When the final error is propagated to the caller
onRetry(attempt, error, delayMs)Each time a retry is scheduled, before the backoff delay

Example: Metrics + Structured Logging

typescript
import {
  HttpAdapterObserver,
  HttpAdapter,
  RetryPolicies,
  Response,
  BaseAdapterException,
} from '@yildizpay/http-adapter';

class MetricsObserver implements HttpAdapterObserver {
  onRequestStart(request: Request): void {
    logger.debug({
      event: 'http_request_start',
      method: request.method,
      url: request.url,
      correlationId: request.correlationId,
    });
  }

  onRequestSuccess(response: Response, durationMs: number): void {
    metrics.histogram('http.request.duration_ms', durationMs, {
      status: String(response.status),
    });
  }

  onRequestFailure(error: BaseAdapterException, durationMs: number): void {
    metrics.increment('http.request.error', {
      type: error.name,
      retryable: String(error.isRetryable()),
    });
    logger.error({ event: 'http_request_failure', ...error.toJSON(), durationMs });
  }

  onRetry(attempt: number, error: BaseAdapterException, delayMs: number): void {
    logger.warn({
      event: 'http_retry',
      attempt,
      delayMs,
      reason: error.name,
    });
  }
}

const adapter = HttpAdapter.builder()
  .withRetryPolicy(RetryPolicies.exponential(3))
  .withObserver(new MetricsObserver())
  .build();

CircuitBreakerObserver

Attach an observer to a CircuitBreaker instance via the fluent .observe() method.

HookWhen it fires
onStateChange(from, to)On every state transition (CLOSED↔OPEN↔HALF_OPEN)
onSuccess()After every successful execution through the breaker
onFailure(error)When a failure is counted (i.e. the isFailure predicate returned true)
onProbeRejected()When a concurrent caller is turned away in HALF_OPEN

Example: Circuit Breaker Metrics

typescript
import {
  CircuitBreaker,
  CircuitBreakerObserver,
  CircuitState,
  BaseAdapterException,
} from '@yildizpay/http-adapter';

class CircuitMetricsObserver implements CircuitBreakerObserver {
  onStateChange(from: CircuitState, to: CircuitState): void {
    logger.warn({
      event: 'circuit_breaker_state_change',
      from,
      to,
    });
    metrics.increment('circuit_breaker.state_change', { from, to });
  }

  onProbeRejected(): void {
    metrics.increment('circuit_breaker.probe_rejected');
  }

  onFailure(error: BaseAdapterException): void {
    metrics.increment('circuit_breaker.failure', { type: error.name });
  }
}

const adapter = HttpAdapter.builder()
  .withCircuitBreaker(
    new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 })
      .observe(new CircuitMetricsObserver()),
  )
  .withObserver(new MetricsObserver())
  .build();

Integration with Pino / Winston

Because all BaseAdapterException instances implement toJSON(), they integrate naturally with structured loggers:

typescript
// Pino
logger.error(error.toJSON());

// Winston
logger.error('request failed', error.toJSON());

// Any logger that accepts a plain object
logger.log('error', { message: error.message, ...error.toJSON() });

The toJSON() output includes name, message, code, stack, and — for HTTP exceptions — the full response object with status code, response body, and RequestContext.

Released under the MIT License.