Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/health/controllers/payment-provider-health.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Controller, Get, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { HealthIndicatorsService, PaymentProviderHealthResult } from '../health-indicators.service';

/**
* Exposes /health/payment-provider as a dedicated sub-check so that
* monitoring systems (e.g. Prometheus, Grafana, uptime robots) can alert on
* the payment-gateway circuit state independently of the main health endpoint.
*/
@ApiTags('Health')
@Controller('health/payment-provider')
export class PaymentProviderHealthController {
constructor(private readonly healthIndicators: HealthIndicatorsService) {}

@Get()
@ApiOperation({
summary: 'Payment provider circuit-breaker health',
description:
'Returns the current circuit-breaker state for the payment provider. ' +
'HTTP 200 when circuit is CLOSED (up), HTTP 503 when OPEN (down), ' +
'HTTP 207 when HALF_OPEN (degraded).',
})
@ApiResponse({ status: 200, description: 'Payment provider is healthy (circuit CLOSED)' })
@ApiResponse({ status: 207, description: 'Payment provider is degraded (circuit HALF_OPEN)' })
@ApiResponse({ status: 503, description: 'Payment provider is unavailable (circuit OPEN)' })
getPaymentProviderHealth(): PaymentProviderHealthResult {
const result = this.healthIndicators.checkPaymentProvider();

// Response status reflects circuit state so load-balancers / uptime monitors
// can act on it without parsing the body.
if (result.status === 'down') {
// NestJS does not allow dynamic status codes via decorators, so we throw
// instead — but we still want to return the body. Use HttpException directly.
const { HttpException } = require('@nestjs/common');
throw new HttpException(result, HttpStatus.SERVICE_UNAVAILABLE);
}

if (result.status === 'degraded') {
const { HttpException } = require('@nestjs/common');
throw new HttpException(result, 207);
}

return result;
}

@Get('status')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Payment provider circuit-breaker status (always 200)',
description:
'Always returns HTTP 200 with circuit state in the body — useful for dashboards ' +
'that poll metrics without relying on HTTP status semantics.',
})
@ApiResponse({ status: 200, description: 'Circuit-breaker status payload' })
getPaymentProviderStatus(): PaymentProviderHealthResult {
return this.healthIndicators.checkPaymentProvider();
}
}
41 changes: 41 additions & 0 deletions src/health/health-indicators.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { Injectable } from '@nestjs/common';
import { PaymentProviderCircuitBreakerService } from '../payments/services/payment-provider-circuit-breaker.service';

export interface PaymentProviderHealthResult {
status: 'up' | 'degraded' | 'down';
circuitState: string;
errorRate: number;
failures: number;
successes: number;
rejects: number;
}

@Injectable()
export class HealthIndicatorsService {
constructor(
private readonly paymentCircuitBreaker: PaymentProviderCircuitBreakerService,
) {}

async checkPostgres(): Promise<boolean> {
return true;
}
Expand All @@ -18,12 +32,39 @@ export class HealthIndicatorsService {
return true;
}

checkPaymentProvider(): PaymentProviderHealthResult {
const stats = this.paymentCircuitBreaker.getStats();

let status: 'up' | 'degraded' | 'down';
switch (stats.state) {
case 'OPEN':
status = 'down';
break;
case 'HALF_OPEN':
status = 'degraded';
break;
default:
status = 'up';
}

return {
status,
circuitState: stats.state,
errorRate: stats.errorRate,
failures: stats.failures,
successes: stats.successes,
rejects: stats.rejects,
};
}

async readiness(): Promise<Record<string, string>> {
const paymentHealth = this.checkPaymentProvider();
const results = {
postgres: (await this.checkPostgres()) ? 'up' : 'down',
redis: (await this.checkRedis()) ? 'up' : 'down',
elasticsearch: (await this.checkElasticsearch()) ? 'up' : 'down',
queue: (await this.checkQueueDepth()) ? 'up' : 'down',
paymentProvider: paymentHealth.status,
};
return results;
}
Expand Down
9 changes: 8 additions & 1 deletion src/health/health.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Module } from '@nestjs/common';
import { ShutdownHealthController } from './controllers/shutdown-health.controller';
import { PaymentProviderHealthController } from './controllers/payment-provider-health.controller';
import { GracefulShutdownService } from '../common/services/graceful-shutdown.service';
import { RequestTrackerService } from '../common/services/request-tracker.service';
import { DatabaseShutdownService } from '../database/services/database-shutdown.service';
import { WorkerShutdownService } from '../workers/services/worker-shutdown.service';
import { ShutdownStateService } from '../common/services/shutdown-state.service';
import { PoolMonitorService } from '../database/pool/pool-monitor.service';
import { WorkerOrchestrationService } from '../workers/orchestration/worker-orchestration.service';
import { HealthIndicatorsService } from './health-indicators.service';
import { PaymentProviderCircuitBreakerService } from '../payments/services/payment-provider-circuit-breaker.service';

@Module({
controllers: [ShutdownHealthController],
controllers: [ShutdownHealthController, PaymentProviderHealthController],
providers: [
GracefulShutdownService,
RequestTrackerService,
Expand All @@ -18,12 +21,16 @@ import { WorkerOrchestrationService } from '../workers/orchestration/worker-orch
ShutdownStateService,
PoolMonitorService,
WorkerOrchestrationService,
HealthIndicatorsService,
PaymentProviderCircuitBreakerService,
],
exports: [
GracefulShutdownService,
RequestTrackerService,
DatabaseShutdownService,
WorkerShutdownService,
HealthIndicatorsService,
PaymentProviderCircuitBreakerService,
],
})
export class HealthModule {}
Loading