Skip to content

Commit fd055a9

Browse files
authored
add monitoring endpoint
1 parent 0af5208 commit fd055a9

File tree

13 files changed

+210
-71
lines changed

13 files changed

+210
-71
lines changed

.github/workflows/cd.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ on:
55
workflows: ["Tiny-URL-CI-Pipeline"]
66
types:
77
- completed
8-
workflow_dispatch: {}
98

109
jobs:
1110
build-docker-images:
1211
if: >
13-
github.event.workflow_run.conclusion == 'success' &&
14-
github.event.workflow_run.head_branch == 'main'
12+
github.event.workflow_run.conclusion == 'success'
1513
runs-on: ubuntu-latest
1614

1715
steps:
@@ -57,4 +55,16 @@ jobs:
5755
--cache-from=type=local,src=/tmp/.buildx-cache \
5856
--cache-to=type=local,dest=/tmp/.buildx-cache \
5957
--load \
60-
./webapp
58+
./webapp
59+
60+
- name: Start docker-compose
61+
run: |
62+
docker-compose -f docker-compose.yml up -d --build
63+
64+
- smoke-test:
65+
image: curlimages/curl
66+
depends_on:
67+
- webapp
68+
- api
69+
entrypoint: >
70+
sh -c "sleep 5 && curl -sf http://localhost:3001 || exit 1"

api/src/infrastructure/infrastructure.module.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Module, DynamicModule } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
33
import { ShortenUrlRepository } from '../shorten-url/shorten-url.repository';
44
import { ShortenUrlRepositoryFactory } from './repository/shorten-url/shorten-url-repository.factory';
5-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
6-
import { DynamoClientProvider } from './provider/dynamodb-client.provider';
5+
import { DynamoDbClientProvider } from './provider/dynamo-db-client-provider';
76
import { RedisClientProvider } from './provider/redis-client.provider';
7+
import { HealthController } from './monitoring/health.controller';
88

99
@Module({
1010
imports: [ConfigModule],
@@ -13,12 +13,17 @@ export class InfrastructureModule {
1313
static register(): DynamicModule {
1414
return {
1515
module: InfrastructureModule,
16+
controllers: [HealthController],
1617
providers: [
1718
RedisClientProvider,
18-
DynamoClientProvider,
19+
DynamoDbClientProvider,
1920
ShortenUrlRepositoryFactory,
2021
],
21-
exports: [RedisClientProvider, DynamoDBClient, ShortenUrlRepository],
22+
exports: [
23+
RedisClientProvider,
24+
DynamoDbClientProvider,
25+
ShortenUrlRepository,
26+
],
2227
};
2328
}
2429
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Controller, Get, Inject } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import { RedisClientProvider } from '../provider/redis-client.provider';
4+
import { DynamoDbClientProvider } from '../provider/dynamo-db-client-provider';
5+
6+
@Controller('monitoring')
7+
export class HealthController {
8+
constructor(
9+
private readonly configService: ConfigService,
10+
private readonly redisClient: RedisClientProvider | null,
11+
private readonly dynamoDbClient: DynamoDbClientProvider | null,
12+
) {}
13+
14+
@Get('/readiness')
15+
async readiness(): Promise<{ status: string }> {
16+
const usePersistenceStorage = this.configService.get<string>(
17+
'USE_PERSISTENT_STORAGE',
18+
'true',
19+
);
20+
21+
if (usePersistenceStorage === 'false') {
22+
return { status: 'OK' };
23+
}
24+
25+
return (await this.dynamoDbClient.isReady()) &&
26+
(await this.redisClient.isReady())
27+
? { status: 'OK' }
28+
: { status: 'KO' };
29+
}
30+
31+
@Get('/ping')
32+
async ping(): Promise<{ status: string }> {
33+
return { status: 'ok' };
34+
}
35+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
Injectable,
3+
Logger,
4+
OnModuleDestroy,
5+
OnModuleInit,
6+
} from '@nestjs/common';
7+
import { ConfigService } from '@nestjs/config';
8+
import {
9+
DynamoDBClient,
10+
GetItemCommand,
11+
PutItemCommand,
12+
ListTablesCommand,
13+
} from '@aws-sdk/client-dynamodb';
14+
import { fromEnv } from '@aws-sdk/credential-providers';
15+
import { unmarshall } from '@aws-sdk/util-dynamodb';
16+
17+
@Injectable()
18+
export class DynamoDbClientProvider implements OnModuleInit, OnModuleDestroy {
19+
private dynamoDBClient: DynamoDBClient | null = null;
20+
21+
constructor(private readonly configService: ConfigService) {}
22+
23+
async putItem(key: string, value: string, tableName: string): Promise<void> {
24+
if (!this.dynamoDBClient)
25+
throw new Error('DynamoDB client not initialized');
26+
27+
await this.dynamoDBClient.send(
28+
new PutItemCommand({
29+
TableName: tableName,
30+
Item: {
31+
shortId: { S: key },
32+
originalUrl: { S: value },
33+
createdAt: { S: new Date().toISOString() },
34+
},
35+
}),
36+
);
37+
}
38+
39+
async getItem(
40+
key: string,
41+
tableName: string,
42+
): Promise<Record<string, any> | null> {
43+
if (!this.dynamoDBClient)
44+
throw new Error('DynamoDB client not initialized');
45+
46+
const result = await this.dynamoDBClient.send(
47+
new GetItemCommand({
48+
TableName: tableName,
49+
Key: { shortId: { S: key } },
50+
}),
51+
);
52+
53+
return unmarshall(result.Item);
54+
}
55+
56+
async isReady(): Promise<boolean> {
57+
if (!this.dynamoDBClient) return false;
58+
try {
59+
await this.dynamoDBClient.send(new ListTablesCommand({}));
60+
return true;
61+
} catch (err) {
62+
Logger.error('DynamoDB readiness check failed', err);
63+
return false;
64+
}
65+
}
66+
67+
async onModuleInit() {
68+
const usePersistent = this.configService.get<string>(
69+
'USE_PERSISTENT_STORAGE',
70+
'false',
71+
);
72+
73+
if (usePersistent === 'false') {
74+
Logger.log('🧪 Skipping dynamo connection (non-persistent mode)');
75+
return;
76+
}
77+
78+
this.dynamoDBClient = new DynamoDBClient({
79+
region: this.configService.get('AWS_REGION', 'local'),
80+
endpoint: this.configService.get(
81+
'DYNAMO_ENDPOINT',
82+
'http://localhost:8000',
83+
),
84+
credentials: fromEnv({}),
85+
});
86+
Logger.log('Connected to dynamodb');
87+
}
88+
89+
async onModuleDestroy() {
90+
if (this.dynamoDBClient) {
91+
this.dynamoDBClient.destroy();
92+
Logger.log('Dynamodb connection closed');
93+
}
94+
}
95+
}

api/src/infrastructure/provider/dynamodb-client.provider.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

api/src/infrastructure/provider/redis-client.provider.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createClient, RedisClientType } from 'redis';
99

1010
@Injectable()
1111
export class RedisClientProvider implements OnModuleInit, OnModuleDestroy {
12+
private readonly RETRY_DELAY_SECONDS = 30_000;
1213
private client: RedisClientType | null = null;
1314

1415
constructor(private readonly configService: ConfigService) {}
@@ -21,6 +22,16 @@ export class RedisClientProvider implements OnModuleInit, OnModuleDestroy {
2122
await this.client.set(key, value, { EX: ttlInSeconds });
2223
}
2324

25+
async isReady(): Promise<boolean> {
26+
try {
27+
await this.client.ping();
28+
return true;
29+
} catch (error) {
30+
Logger.error('Redis client is not ready', error);
31+
return false;
32+
}
33+
}
34+
2435
async onModuleInit() {
2536
const usePersistent = this.configService.get<string>(
2637
'USE_PERSISTENT_STORAGE',
@@ -33,7 +44,19 @@ export class RedisClientProvider implements OnModuleInit, OnModuleDestroy {
3344
}
3445

3546
const redisUrl = this.configService.get<string>('REDIS_URL');
36-
this.client = createClient({ url: redisUrl });
47+
this.client = createClient({
48+
url: redisUrl,
49+
socket: {
50+
reconnectStrategy: (retries) => {
51+
if (retries > 5) {
52+
Logger.warn('🔌 Redis reconnect limit reached');
53+
return new Error('Stop retrying Redis');
54+
}
55+
Logger.warn('Redis reconnecting...', retries);
56+
return this.RETRY_DELAY_SECONDS;
57+
},
58+
},
59+
});
3760

3861
this.client.on('error', (err) =>
3962
Logger.error('Redis client error', err, 'REDIS_CLIENT'),

api/src/infrastructure/repository/shorten-url/persistent-url-storage.repository.ts

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { ShortenUrlRepository } from '../../../shorten-url/shorten-url.repository';
2-
import { Inject, Injectable, Logger } from '@nestjs/common';
2+
import { Injectable, Logger } from '@nestjs/common';
33
import { ConfigService } from '@nestjs/config';
4-
import {
5-
DynamoDBClient,
6-
PutItemCommand,
7-
GetItemCommand,
8-
} from '@aws-sdk/client-dynamodb';
94
import { unmarshall } from '@aws-sdk/util-dynamodb';
105
import { RedisClientProvider } from '../../provider/redis-client.provider';
6+
import { DynamoDbClientProvider } from '../../provider/dynamo-db-client-provider';
117

128
@Injectable()
139
export class PersistentUrlStorageRepository implements ShortenUrlRepository {
@@ -18,8 +14,7 @@ export class PersistentUrlStorageRepository implements ShortenUrlRepository {
1814
constructor(
1915
private readonly configService: ConfigService,
2016
private readonly redisClient: RedisClientProvider | null,
21-
@Inject(DynamoDBClient)
22-
private readonly dynamoDbClient: DynamoDBClient | null,
17+
private readonly dynamoDbClient: DynamoDbClientProvider | null,
2318
) {
2419
this.redisTTL = this.configService.get<number>(
2520
'REDIS_TTL',
@@ -32,17 +27,7 @@ export class PersistentUrlStorageRepository implements ShortenUrlRepository {
3227
const now = new Date().toISOString();
3328

3429
// Write to DynamoDB
35-
await this.dynamoDbClient.send(
36-
new PutItemCommand({
37-
TableName: this.tableName,
38-
Item: {
39-
shortId: { S: shortId },
40-
originalUrl: { S: originalUrl },
41-
createdAt: { S: now },
42-
},
43-
}),
44-
);
45-
30+
await this.dynamoDbClient.putItem(shortId, originalUrl, this.tableName);
4631
// Cache in Redis
4732
await this.redisClient.set(`short:${shortId}`, originalUrl, this.redisTTL);
4833
}
@@ -58,12 +43,7 @@ export class PersistentUrlStorageRepository implements ShortenUrlRepository {
5843
Logger.debug(`Cache miss for ${shortId}. Fetching from DynamoDB...`);
5944

6045
// Fall back to DynamoDB
61-
const result = await this.dynamoDbClient.send(
62-
new GetItemCommand({
63-
TableName: this.tableName,
64-
Key: { shortId: { S: shortId } },
65-
}),
66-
);
46+
const result = await this.dynamoDbClient.getItem(shortId, this.tableName);
6747

6848
if (!result.Item) return null;
6949

api/src/infrastructure/repository/shorten-url/shorten-url-repository.factory.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Provider } from '@nestjs/common';
33
import { InMemoryUrlStorageRepository } from './in-memory-url-storage.repository';
44
import { ShortenUrlRepository } from '../../../shorten-url/shorten-url.repository';
55
import { PersistentUrlStorageRepository } from './persistent-url-storage.repository';
6-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
76
import { RedisClientProvider } from '../../provider/redis-client.provider';
7+
import { DynamoDbClientProvider } from '../../provider/dynamo-db-client-provider';
88

99
/**
1010
* Factory provider to select the appropriate repository implementation based on configuration.
@@ -14,8 +14,8 @@ export const ShortenUrlRepositoryFactory: Provider = {
1414
provide: ShortenUrlRepository,
1515
useFactory: (
1616
configService: ConfigService,
17-
redisClientService: RedisClientProvider | null,
18-
dynamoDbClient: DynamoDBClient | null,
17+
redisClientProvider: RedisClientProvider | null,
18+
dynamoDbClientProvider: DynamoDbClientProvider | null,
1919
) => {
2020
const usePersistenceStorage = configService.get<string>(
2121
'USE_PERSISTENT_STORAGE',
@@ -25,12 +25,12 @@ export const ShortenUrlRepositoryFactory: Provider = {
2525
if (usePersistenceStorage === 'true') {
2626
return new PersistentUrlStorageRepository(
2727
configService,
28-
redisClientService,
29-
dynamoDbClient,
28+
redisClientProvider,
29+
dynamoDbClientProvider,
3030
);
3131
}
3232

3333
return new InMemoryUrlStorageRepository();
3434
},
35-
inject: [ConfigService, RedisClientProvider, DynamoDBClient],
35+
inject: [ConfigService, RedisClientProvider, DynamoDbClientProvider],
3636
};

api/src/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ async function bootstrap() {
3636
);
3737

3838
const port = process.env.PORT || 3000;
39-
await app.listen(port).then(() => Logger.log(`Server started on ${port}`));
39+
await app
40+
.listen(port)
41+
.then(() => Logger.log(`API Server started on ${port}`));
4042
}
4143

4244
bootstrap();

docker-compose.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ services:
7575
- ./scripts:/scripts
7676
entrypoint: ["/bin/sh", "-c", "/scripts/init-dynamodb.sh"]
7777

78-
79-
8078
volumes:
8179
redis_data:
8280
driver: local

0 commit comments

Comments
 (0)