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.
| Hook | When 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.
| Hook | When 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.
