Skip to content

Commit

Permalink
feat: adds health-check endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
hudgins committed Feb 27, 2024
1 parent 70e6539 commit 76dbe1e
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 24 deletions.
5 changes: 2 additions & 3 deletions src/app/app.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { OpenWeatherMapModule } from '../weather-sources/open-weather-map/open-weather-map.module';
import { AlwaysSunnyModule } from '../weather-sources/always-sunny/always-sunny.module';
import { WeatherSource } from '../core/weather-data/weather-data.interface';
import { WeatherSourcesRegistryModule } from '../weather-sources/weather-sources-registry.module';
// import { ConsoleLogger } from '@nestjs/common';

describe('AppController', () => {
let appController: AppController;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
imports: [ConfigModule.forRoot(), OpenWeatherMapModule, AlwaysSunnyModule],
imports: [ConfigModule.forRoot(), WeatherSourcesRegistryModule],
controllers: [AppController],
providers: [AppService],
}).compile();
Expand Down
8 changes: 4 additions & 4 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { ConfigModule } from '@nestjs/config'
import { LoggerModule } from 'nestjs-pino';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { OpenWeatherMapModule } from '../weather-sources/open-weather-map/open-weather-map.module';
import { MetricsModule } from '../metrics/metrics.module';
import { CoreModule } from '../core/core.module';
import { AlwaysSunnyModule } from '../weather-sources/always-sunny/always-sunny.module';
import { WeatherSourcesRegistryModule } from 'src/weather-sources/weather-sources-registry.module';
import { HealthCheckModule } from 'src/health-check/health-check.module';

@Module({
imports: [
Expand All @@ -27,8 +27,8 @@ import { AlwaysSunnyModule } from '../weather-sources/always-sunny/always-sunny.
// }),
MetricsModule,
CoreModule,
OpenWeatherMapModule,
AlwaysSunnyModule
WeatherSourcesRegistryModule,
HealthCheckModule
],
controllers: [AppController],
providers: [AppService],
Expand Down
20 changes: 5 additions & 15 deletions src/app/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
import { Injectable } from '@nestjs/common';
import { WeatherData, WeatherSource, WeatherService, WeatherUnits } from '../core/weather-data/weather-data.interface';
import { OpenWeatherMapService } from '../weather-sources/open-weather-map/open-weather-map.service';
import { AlwaysSunnyService } from '../weather-sources/always-sunny/always-sunny.service';
import { WeatherSourcesRegistryService } from '../weather-sources/weather-sources-registry.service';

@Injectable()
export class AppService {
private readonly weatherServices: Map<WeatherSource, WeatherService> = new Map<WeatherSource, WeatherService>();

constructor(openWeatherMapService: OpenWeatherMapService, alwaysSunnyService: AlwaysSunnyService) {
this.weatherServices.set(WeatherSource.OpenWeatherMap, openWeatherMapService)
this.weatherServices.set(WeatherSource.AlwaysSunny, alwaysSunnyService)
constructor(private readonly weatherSourcesRegistryService: WeatherSourcesRegistryService) {
}

async getWeatherForCity(city: string, units: WeatherUnits = WeatherUnits.Metric, source: WeatherSource = WeatherSource.OpenWeatherMap): Promise<WeatherData> {
return this.getWeatherService(source).fetchWeatherForCity(city, units)
return this.weatherSourcesRegistryService.getWeatherService(source).fetchWeatherForCity(city, units)
}

async getWeatherForZipCode(zip: string, units: WeatherUnits = WeatherUnits.Metric, source: WeatherSource = WeatherSource.OpenWeatherMap): Promise<WeatherData> {
return this.getWeatherService(source).fetchWeatherForZipCode(zip, units)
return this.weatherSourcesRegistryService.getWeatherService(source).fetchWeatherForZipCode(zip, units)
}

async getWeatherForLatLong(latitude: string, longitude: string, units: WeatherUnits = WeatherUnits.Metric, source: WeatherSource = WeatherSource.OpenWeatherMap): Promise<WeatherData> {
return this.getWeatherService(source).fetchWeatherForLatLong(latitude, longitude, units)
}

private getWeatherService(source: WeatherSource): WeatherService {
if (!this.weatherServices.has(source)) throw new Error('unrecognized weather source')
return this.weatherServices.get(source)
return this.weatherSourcesRegistryService.getWeatherService(source).fetchWeatherForLatLong(latitude, longitude, units)
}
}
10 changes: 10 additions & 0 deletions src/core/service/service.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum ServiceHealth {
Normal = 'normal',
Degraded = 'degraded'
}

export interface Service {
getName(): string
getHealth(): Promise<ServiceHealth>
}

5 changes: 4 additions & 1 deletion src/core/weather-data/weather-data.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { WeatherSourcesRegistryService } from "src/weather-sources/weather-sources-registry.service"
import { Service } from "../service/service.interface"

export interface WeatherData {
locationCoords: { lat: number, long: number }
locationName: string
Expand All @@ -22,7 +25,7 @@ export enum WeatherUnits {
Imperial = 'imperial'
}

export interface WeatherService {
export interface WeatherService extends Service {
fetchWeatherForCity(city: string, units: WeatherUnits): Promise<WeatherData>
fetchWeatherForZipCode(zipCode: string, units: WeatherUnits): Promise<WeatherData>
fetchWeatherForLatLong(latitude: string, longitude: string, units: WeatherUnits): Promise<WeatherData>
Expand Down
35 changes: 35 additions & 0 deletions src/health-check/health-check.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthCheckController } from './health-check.controller';
import { WeatherSourcesRegistryModule } from '../weather-sources/weather-sources-registry.module';
import { ConfigModule } from '@nestjs/config';

describe('HealthCheckController', () => {
let controller: HealthCheckController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ConfigModule.forRoot(), WeatherSourcesRegistryModule],
controllers: [HealthCheckController],
}).compile();

controller = module.get<HealthCheckController>(HealthCheckController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

it('should return "up" from basic health check route', () => {
expect(controller.getBasicHealth()).toEqual('up')
})

it('should return status for each weather source', async () => {
const statuses = await controller.getDetailedHealth()
expect(statuses).toHaveProperty('status')
expect(statuses).toHaveProperty('sources')
expect(statuses.sources).toHaveProperty('alwayssunny')
expect(statuses.sources).toHaveProperty('openweathermap')
expect(statuses.sources.alwayssunny.status).toEqual('normal')
expect(statuses.sources.openweathermap.status).toEqual('normal')
})
});
36 changes: 36 additions & 0 deletions src/health-check/health-check.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Controller, Get } from '@nestjs/common';
import { WeatherSourcesRegistryService } from '../weather-sources/weather-sources-registry.service';
import { ServiceHealth } from '../core/service/service.interface';

type SourceHealth = {
[source: string]: { status: ServiceHealth }
}

type DetailedHealthResponse = {
status: ServiceHealth
sources: SourceHealth
}

@Controller('v1/health-check')
export class HealthCheckController {
constructor(private readonly weatherSourcesRegistryService: WeatherSourcesRegistryService) {}

@Get()
getBasicHealth() {
return 'up'
}

@Get('detailed')
async getDetailedHealth(): Promise<DetailedHealthResponse> {
const services = this.weatherSourcesRegistryService.getWeatherServices()
let health = { status: ServiceHealth.Normal, sources: {} }
for (let i = 0; i < services.length; i++) {
const service = services[i]
const status = await service.getHealth()
health.sources[service.getName()] = { status }
if (status !== ServiceHealth.Normal.toString()) health.status = status as ServiceHealth
}
return health
}

}
9 changes: 9 additions & 0 deletions src/health-check/health-check.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { HealthCheckController } from './health-check.controller';
import { WeatherSourcesRegistryModule } from 'src/weather-sources/weather-sources-registry.module';

@Module({
imports: [WeatherSourcesRegistryModule],
controllers: [HealthCheckController]
})
export class HealthCheckModule {}
9 changes: 9 additions & 0 deletions src/weather-sources/always-sunny/always-sunny.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Logger } from '@nestjs/common';

import { MetricsService } from '../../metrics/metrics.service';
import { WeatherData, WeatherService, WeatherSource, WeatherUnits } from '../../core/weather-data/weather-data.interface';
import { ServiceHealth } from '../../core/service/service.interface';

@Injectable()
export class AlwaysSunnyService implements WeatherService {
Expand All @@ -11,6 +12,14 @@ export class AlwaysSunnyService implements WeatherService {
constructor(private readonly metricsService: MetricsService) {
}

getName(): WeatherSource.AlwaysSunny {
return WeatherSource.AlwaysSunny
}

getHealth(): Promise<ServiceHealth> {
return Promise.resolve(ServiceHealth.Normal)
}

async fetchWeatherForCity(city: string, units: WeatherUnits = WeatherUnits.Metric): Promise<WeatherData> {
return this.fetchWeather({ city, units })
}
Expand Down
14 changes: 13 additions & 1 deletion src/weather-sources/open-weather-map/open-weather-map.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Logger } from '@nestjs/common';
import axios from 'axios'
import { MetricsService } from '../../metrics/metrics.service';
import { WeatherData, WeatherService, WeatherSource, WeatherUnits } from '../../core/weather-data/weather-data.interface';
import { ServiceHealth } from '../../core/service/service.interface';

const OPEN_WEATHER_MAP_API = 'https://api.openweathermap.org/data/2.5'

Expand All @@ -23,7 +24,18 @@ export class OpenWeatherMapService implements WeatherService {
private readonly logger = new Logger(OpenWeatherMapService.name)

constructor(private configService: ConfigService, private readonly metricsService: MetricsService) {
this.apiKey = configService.get<string>('API_KEY') || 'none'
this.apiKey = configService.get<string>('API_KEY')
if (!this.apiKey) throw new Error('missing required API_KEY variable in environment')
}

getName(): WeatherSource.OpenWeatherMap {
return WeatherSource.OpenWeatherMap
}

async getHealth(): Promise<ServiceHealth> {
const result = await this.fetchWeather({ q: 'Nelson, CA', units: WeatherUnits.Metric })
if (result.locationName === 'Nelson') return ServiceHealth.Normal
return ServiceHealth.Degraded
}

async fetchWeatherForCity(city: string, units: WeatherUnits = WeatherUnits.Metric): Promise<WeatherData> {
Expand Down
20 changes: 20 additions & 0 deletions src/weather-sources/weather-sources-registry.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { OpenWeatherMapModule } from './open-weather-map/open-weather-map.module';
import { AlwaysSunnyModule } from './always-sunny/always-sunny.module';
import { WeatherSourcesRegistryService } from './weather-sources-registry.service';
import { ConfigModule } from '@nestjs/config';

@Module({
imports: [
ConfigModule,
OpenWeatherMapModule,
AlwaysSunnyModule
],
providers: [
WeatherSourcesRegistryService
],
exports: [
WeatherSourcesRegistryService
]
})
export class WeatherSourcesRegistryModule {}
32 changes: 32 additions & 0 deletions src/weather-sources/weather-sources-registry.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WeatherSourcesRegistryService } from './weather-sources-registry.service';
import { AlwaysSunnyModule } from './always-sunny/always-sunny.module';
import { OpenWeatherMapModule } from './open-weather-map/open-weather-map.module';
import { ConfigModule } from '@nestjs/config';
import { WeatherSource } from 'src/core/weather-data/weather-data.interface';

describe('WeatherSourcesRegistryService', () => {
let registry: WeatherSourcesRegistryService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ConfigModule.forRoot(), AlwaysSunnyModule, OpenWeatherMapModule],
providers: [WeatherSourcesRegistryService],
}).compile();

registry = module.get<WeatherSourcesRegistryService>(WeatherSourcesRegistryService);
});

it('should be defined', () => {
expect(registry).toBeDefined();
});

it('should return the list of services', () => {
const services = registry.getWeatherServices()
expect(services.length).toBeGreaterThan(1)
services.forEach((service) => {
expect(service.getName()).toBeDefined()
expect(registry.getWeatherService(service.getName() as WeatherSource).getName()).toEqual(service.getName())
})
});
});
25 changes: 25 additions & 0 deletions src/weather-sources/weather-sources-registry.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';

import { WeatherSource, WeatherService } from '../core/weather-data/weather-data.interface';
import { OpenWeatherMapService } from './open-weather-map/open-weather-map.service';
import { AlwaysSunnyService } from './always-sunny/always-sunny.service';

@Injectable()
export class WeatherSourcesRegistryService {
private readonly weatherServices: Map<WeatherSource, WeatherService> = new Map<WeatherSource, WeatherService>();

constructor(openWeatherMapService: OpenWeatherMapService, alwaysSunnyService: AlwaysSunnyService) {
this.weatherServices.set(WeatherSource.OpenWeatherMap, openWeatherMapService)
this.weatherServices.set(WeatherSource.AlwaysSunny, alwaysSunnyService)
}

getWeatherServices(): readonly WeatherService[] {
return Array.from(this.weatherServices.values())
}

getWeatherService(source: WeatherSource): WeatherService {
if (!this.weatherServices.has(source)) throw new Error('unrecognized weather source')
return this.weatherServices.get(source)
}

}

0 comments on commit 76dbe1e

Please sign in to comment.