@friggframework/core 2.0.0--canary.400.545e7a8.0 → 2.0.0--canary.405.b87f8d8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -8,11 +8,6 @@ const schema = new mongoose.Schema({
|
|
|
8
8
|
// Add a static method to get active connections
|
|
9
9
|
schema.statics.getActiveConnections = async function () {
|
|
10
10
|
try {
|
|
11
|
-
// Return empty array if websockets are not configured
|
|
12
|
-
if (!process.env.WEBSOCKET_API_ENDPOINT) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
const connections = await this.find({}, 'connectionId');
|
|
17
12
|
return connections.map((conn) => ({
|
|
18
13
|
connectionId: conn.connectionId,
|
|
@@ -9,15 +9,14 @@ The Frigg service includes comprehensive healthcheck endpoints to monitor servic
|
|
|
9
9
|
### 1. Basic Health Check
|
|
10
10
|
**GET** `/health`
|
|
11
11
|
|
|
12
|
-
Simple health check endpoint that returns basic service information. No authentication required.
|
|
12
|
+
Simple health check endpoint that returns basic service information. No authentication required. This endpoint is rate-limited at the API Gateway level.
|
|
13
13
|
|
|
14
14
|
**Response:**
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
17
|
"status": "ok",
|
|
18
18
|
"timestamp": "2024-01-10T12:00:00.000Z",
|
|
19
|
-
"service": "frigg-core-api"
|
|
20
|
-
"version": "1.0.0"
|
|
19
|
+
"service": "frigg-core-api"
|
|
21
20
|
}
|
|
22
21
|
```
|
|
23
22
|
|
|
@@ -29,22 +28,23 @@ Simple health check endpoint that returns basic service information. No authenti
|
|
|
29
28
|
|
|
30
29
|
Comprehensive health check that tests all service components and dependencies.
|
|
31
30
|
|
|
31
|
+
**Authentication Required:**
|
|
32
|
+
- Header: `x-api-key: YOUR_API_KEY`
|
|
33
|
+
- The API key must match the `HEALTH_API_KEY` environment variable
|
|
34
|
+
|
|
32
35
|
**Response:**
|
|
33
36
|
```json
|
|
34
37
|
{
|
|
35
38
|
"service": "frigg-core-api",
|
|
36
|
-
"status": "healthy", // "healthy"
|
|
39
|
+
"status": "healthy", // "healthy" or "unhealthy"
|
|
37
40
|
"timestamp": "2024-01-10T12:00:00.000Z",
|
|
38
|
-
"version": "1.0.0",
|
|
39
|
-
"uptime": 3600, // seconds
|
|
40
41
|
"checks": {
|
|
41
42
|
"database": {
|
|
42
43
|
"status": "healthy",
|
|
43
44
|
"state": "connected",
|
|
44
|
-
"type": "mongodb",
|
|
45
45
|
"responseTime": 5 // milliseconds
|
|
46
46
|
},
|
|
47
|
-
"
|
|
47
|
+
"externalApis": {
|
|
48
48
|
"github": {
|
|
49
49
|
"status": "healthy",
|
|
50
50
|
"statusCode": 200,
|
|
@@ -68,13 +68,6 @@ Comprehensive health check that tests all service components and dependencies.
|
|
|
68
68
|
"count": 5,
|
|
69
69
|
"available": ["integration1", "integration2", "..."]
|
|
70
70
|
}
|
|
71
|
-
},
|
|
72
|
-
"memory": {
|
|
73
|
-
"status": "healthy",
|
|
74
|
-
"rss": "150 MB",
|
|
75
|
-
"heapTotal": "100 MB",
|
|
76
|
-
"heapUsed": "80 MB",
|
|
77
|
-
"external": "20 MB"
|
|
78
71
|
}
|
|
79
72
|
},
|
|
80
73
|
"responseTime": 250 // total endpoint response time in milliseconds
|
|
@@ -82,14 +75,18 @@ Comprehensive health check that tests all service components and dependencies.
|
|
|
82
75
|
```
|
|
83
76
|
|
|
84
77
|
**Status Codes:**
|
|
85
|
-
- `200 OK` - Service is healthy
|
|
86
|
-
- `503 Service Unavailable` - Service is unhealthy
|
|
78
|
+
- `200 OK` - Service is healthy (all components operational)
|
|
79
|
+
- `503 Service Unavailable` - Service is unhealthy (any component failure)
|
|
80
|
+
- `401 Unauthorized` - Missing or invalid x-api-key header
|
|
87
81
|
|
|
88
82
|
### 3. Liveness Probe
|
|
89
83
|
**GET** `/health/live`
|
|
90
84
|
|
|
91
85
|
Kubernetes-style liveness probe. Returns whether the service process is alive.
|
|
92
86
|
|
|
87
|
+
**Authentication Required:**
|
|
88
|
+
- Header: `x-api-key: YOUR_API_KEY`
|
|
89
|
+
|
|
93
90
|
**Response:**
|
|
94
91
|
```json
|
|
95
92
|
{
|
|
@@ -106,6 +103,9 @@ Kubernetes-style liveness probe. Returns whether the service process is alive.
|
|
|
106
103
|
|
|
107
104
|
Kubernetes-style readiness probe. Returns whether the service is ready to receive traffic.
|
|
108
105
|
|
|
106
|
+
**Authentication Required:**
|
|
107
|
+
- Header: `x-api-key: YOUR_API_KEY`
|
|
108
|
+
|
|
109
109
|
**Response:**
|
|
110
110
|
```json
|
|
111
111
|
{
|
|
@@ -125,35 +125,32 @@ Kubernetes-style readiness probe. Returns whether the service is ready to receiv
|
|
|
125
125
|
## Health Status Definitions
|
|
126
126
|
|
|
127
127
|
- **healthy**: All components are functioning normally
|
|
128
|
-
- **
|
|
129
|
-
- **unhealthy**: Critical components are failing, service cannot function properly
|
|
128
|
+
- **unhealthy**: Any component is failing, service may not function properly
|
|
130
129
|
|
|
131
130
|
## Component Checks
|
|
132
131
|
|
|
133
132
|
### Database Connectivity
|
|
134
|
-
- Checks
|
|
135
|
-
- Performs ping test if connected
|
|
133
|
+
- Checks database connection state
|
|
134
|
+
- Performs ping test with 2-second timeout if connected
|
|
136
135
|
- Reports connection state and response time
|
|
136
|
+
- Database type is not exposed for security reasons
|
|
137
137
|
|
|
138
138
|
### External API Connectivity
|
|
139
139
|
- Tests connectivity to external services (GitHub, npm registry)
|
|
140
140
|
- Configurable timeout (default: 5 seconds)
|
|
141
141
|
- Reports reachability and response times
|
|
142
|
+
- Uses Promise.all for parallel checking
|
|
142
143
|
|
|
143
144
|
### Integration Status
|
|
144
145
|
- Verifies available modules and integrations are loaded
|
|
145
146
|
- Reports counts and lists of available components
|
|
146
147
|
|
|
147
|
-
### Memory Usage
|
|
148
|
-
- Reports current memory usage statistics
|
|
149
|
-
- Includes RSS, heap, and external memory metrics
|
|
150
|
-
|
|
151
148
|
## Usage Examples
|
|
152
149
|
|
|
153
150
|
### Monitoring Systems
|
|
154
151
|
Configure your monitoring system to poll `/health/detailed` every 30-60 seconds:
|
|
155
152
|
```bash
|
|
156
|
-
curl https://your-frigg-instance.com/health/detailed
|
|
153
|
+
curl -H "x-api-key: YOUR_API_KEY" https://your-frigg-instance.com/health/detailed
|
|
157
154
|
```
|
|
158
155
|
|
|
159
156
|
### Load Balancer Health Checks
|
|
@@ -168,6 +165,9 @@ livenessProbe:
|
|
|
168
165
|
httpGet:
|
|
169
166
|
path: /health/live
|
|
170
167
|
port: 8080
|
|
168
|
+
httpHeaders:
|
|
169
|
+
- name: x-api-key
|
|
170
|
+
value: YOUR_API_KEY
|
|
171
171
|
periodSeconds: 10
|
|
172
172
|
timeoutSeconds: 5
|
|
173
173
|
|
|
@@ -175,6 +175,9 @@ readinessProbe:
|
|
|
175
175
|
httpGet:
|
|
176
176
|
path: /health/ready
|
|
177
177
|
port: 8080
|
|
178
|
+
httpHeaders:
|
|
179
|
+
- name: x-api-key
|
|
180
|
+
value: YOUR_API_KEY
|
|
178
181
|
initialDelaySeconds: 30
|
|
179
182
|
periodSeconds: 10
|
|
180
183
|
```
|
|
@@ -192,20 +195,24 @@ const externalAPIs = [
|
|
|
192
195
|
```
|
|
193
196
|
|
|
194
197
|
### Adjusting Timeouts
|
|
195
|
-
The default timeout for external API checks is 5 seconds.
|
|
198
|
+
The default timeout for external API checks is 5 seconds. Database ping timeout is set to 2 seconds:
|
|
196
199
|
```javascript
|
|
197
200
|
const checkExternalAPI = (url, timeout = 5000) => {
|
|
198
201
|
// ...
|
|
199
202
|
};
|
|
203
|
+
|
|
204
|
+
await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
|
|
200
205
|
```
|
|
201
206
|
|
|
202
207
|
## Best Practices
|
|
203
208
|
|
|
204
|
-
1. **
|
|
205
|
-
2. **
|
|
206
|
-
3. **
|
|
207
|
-
4. **
|
|
208
|
-
5. **
|
|
209
|
+
1. **Authentication**: Basic `/health` endpoint requires no authentication, but detailed endpoints require `x-api-key` header
|
|
210
|
+
2. **Rate Limiting**: Configure rate limiting at the API Gateway level to prevent abuse
|
|
211
|
+
3. **Fast Response**: Health checks should respond quickly (< 1 second)
|
|
212
|
+
4. **Strict Status Codes**: Return 503 for any non-healthy state to ensure proper alerting
|
|
213
|
+
5. **Detailed Logging**: Failed health checks are logged for debugging
|
|
214
|
+
6. **Security**: No sensitive information (DB types, versions) exposed in responses
|
|
215
|
+
7. **Lambda Considerations**: Uptime and memory metrics not included as they're not relevant in serverless
|
|
209
216
|
|
|
210
217
|
## Troubleshooting
|
|
211
218
|
|
|
@@ -216,15 +223,18 @@ const checkExternalAPI = (url, timeout = 5000) => {
|
|
|
216
223
|
|
|
217
224
|
### External API Failures
|
|
218
225
|
- May indicate network issues or external service downtime
|
|
219
|
-
- Service
|
|
220
|
-
|
|
221
|
-
### Memory Issues
|
|
222
|
-
- Monitor memory metrics over time
|
|
223
|
-
- Consider increasing container/instance memory limits if consistently high
|
|
226
|
+
- Service reports "unhealthy" status if any external API is unreachable
|
|
224
227
|
|
|
225
228
|
## Security Considerations
|
|
226
229
|
|
|
230
|
+
- Basic health endpoint requires no authentication for monitoring compatibility
|
|
231
|
+
- Detailed endpoints require `x-api-key` header authentication
|
|
227
232
|
- Health endpoints do not expose sensitive information
|
|
228
233
|
- Database connection strings and credentials are never included in responses
|
|
229
234
|
- External API checks use read-only endpoints
|
|
230
|
-
-
|
|
235
|
+
- Rate limiting should be configured at the API Gateway level
|
|
236
|
+
- Consider IP whitelisting for health endpoints in production
|
|
237
|
+
|
|
238
|
+
## Environment Variables
|
|
239
|
+
|
|
240
|
+
- `HEALTH_API_KEY`: Required API key for accessing detailed health endpoints
|
|
@@ -4,11 +4,28 @@ const https = require('https');
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const { moduleFactory, integrationFactory } = require('./../backend-utils');
|
|
6
6
|
const { createAppHandler } = require('./../app-handler-helpers');
|
|
7
|
-
const { version } = require('../../package.json');
|
|
8
7
|
|
|
9
8
|
const router = Router();
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
const validateApiKey = (req, res, next) => {
|
|
11
|
+
const apiKey = req.headers['x-api-key'];
|
|
12
|
+
|
|
13
|
+
if (req.path === '/health') {
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!apiKey || apiKey !== process.env.HEALTH_API_KEY) {
|
|
18
|
+
return res.status(401).json({
|
|
19
|
+
status: 'error',
|
|
20
|
+
message: 'Unauthorized'
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
next();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
router.use(validateApiKey);
|
|
28
|
+
|
|
12
29
|
const checkExternalAPI = (url, timeout = 5000) => {
|
|
13
30
|
return new Promise((resolve) => {
|
|
14
31
|
const protocol = url.startsWith('https:') ? https : http;
|
|
@@ -54,31 +71,25 @@ const checkExternalAPI = (url, timeout = 5000) => {
|
|
|
54
71
|
});
|
|
55
72
|
};
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
router.get('/health', async (req, res) => {
|
|
74
|
+
router.get('/health', async (_req, res) => {
|
|
59
75
|
const status = {
|
|
60
76
|
status: 'ok',
|
|
61
77
|
timestamp: new Date().toISOString(),
|
|
62
|
-
service: 'frigg-core-api'
|
|
63
|
-
version
|
|
78
|
+
service: 'frigg-core-api'
|
|
64
79
|
};
|
|
65
80
|
|
|
66
81
|
res.status(200).json(status);
|
|
67
82
|
});
|
|
68
83
|
|
|
69
|
-
|
|
70
|
-
router.get('/health/detailed', async (req, res) => {
|
|
84
|
+
router.get('/health/detailed', async (_req, res) => {
|
|
71
85
|
const startTime = Date.now();
|
|
72
86
|
const checks = {
|
|
73
87
|
service: 'frigg-core-api',
|
|
74
88
|
status: 'healthy',
|
|
75
89
|
timestamp: new Date().toISOString(),
|
|
76
|
-
version,
|
|
77
|
-
uptime: process.uptime(),
|
|
78
90
|
checks: {}
|
|
79
91
|
};
|
|
80
92
|
|
|
81
|
-
// Check database connectivity
|
|
82
93
|
try {
|
|
83
94
|
const dbState = mongoose.connection.readyState;
|
|
84
95
|
const dbStateMap = {
|
|
@@ -90,113 +101,145 @@ router.get('/health/detailed', async (req, res) => {
|
|
|
90
101
|
|
|
91
102
|
checks.checks.database = {
|
|
92
103
|
status: dbState === 1 ? 'healthy' : 'unhealthy',
|
|
93
|
-
state: dbStateMap[dbState]
|
|
94
|
-
type: 'mongodb'
|
|
104
|
+
state: dbStateMap[dbState]
|
|
95
105
|
};
|
|
96
106
|
|
|
97
|
-
// If connected, check database responsiveness
|
|
98
107
|
if (dbState === 1) {
|
|
99
108
|
const pingStart = Date.now();
|
|
100
|
-
await mongoose.connection.db.admin().ping();
|
|
109
|
+
await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
|
|
101
110
|
checks.checks.database.responseTime = Date.now() - pingStart;
|
|
111
|
+
} else {
|
|
112
|
+
checks.status = 'unhealthy';
|
|
102
113
|
}
|
|
103
114
|
} catch (error) {
|
|
104
115
|
checks.checks.database = {
|
|
116
|
+
status: 'unhealthy',
|
|
117
|
+
error: error.message
|
|
118
|
+
};
|
|
119
|
+
checks.status = 'unhealthy';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const { STAGE, BYPASS_ENCRYPTION_STAGE, KMS_KEY_ARN, AES_KEY_ID } =
|
|
124
|
+
process.env;
|
|
125
|
+
const defaultBypassStages = ['dev', 'test', 'local'];
|
|
126
|
+
const useEnv = BYPASS_ENCRYPTION_STAGE !== undefined;
|
|
127
|
+
const bypassStages = useEnv
|
|
128
|
+
? BYPASS_ENCRYPTION_STAGE.split(',').map((s) => s.trim())
|
|
129
|
+
: defaultBypassStages;
|
|
130
|
+
const bypassed = bypassStages.includes(STAGE);
|
|
131
|
+
const mode = KMS_KEY_ARN ? 'kms' : AES_KEY_ID ? 'aes' : 'none';
|
|
132
|
+
|
|
133
|
+
let status = 'disabled';
|
|
134
|
+
// Having both KMS_KEY_ARN and AES_KEY_ID present is considered unhealthy,
|
|
135
|
+
// as only one encryption method should be configured at a time to avoid ambiguity
|
|
136
|
+
// and potential security misconfiguration.
|
|
137
|
+
if (KMS_KEY_ARN && AES_KEY_ID) {
|
|
138
|
+
status = 'unhealthy';
|
|
139
|
+
} else if (!bypassed && mode !== 'none') {
|
|
140
|
+
status = 'enabled';
|
|
141
|
+
} else {
|
|
142
|
+
status = 'disabled';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
checks.checks.encryption = {
|
|
146
|
+
status,
|
|
147
|
+
mode,
|
|
148
|
+
bypassed,
|
|
149
|
+
stage: STAGE || null,
|
|
150
|
+
};
|
|
151
|
+
if (status === 'unhealthy') checks.status = 'unhealthy';
|
|
152
|
+
} catch (error) {
|
|
153
|
+
checks.checks.encryption = {
|
|
105
154
|
status: 'unhealthy',
|
|
106
155
|
error: error.message,
|
|
107
|
-
type: 'mongodb'
|
|
108
156
|
};
|
|
109
|
-
checks.status = '
|
|
157
|
+
checks.status = 'unhealthy';
|
|
110
158
|
}
|
|
111
159
|
|
|
112
|
-
// Check external API connectivity (example endpoints)
|
|
113
160
|
const externalAPIs = [
|
|
114
161
|
{ name: 'github', url: 'https://api.github.com/status' },
|
|
115
162
|
{ name: 'npm', url: 'https://registry.npmjs.org' }
|
|
116
163
|
];
|
|
117
164
|
|
|
118
|
-
checks.checks.
|
|
165
|
+
checks.checks.externalApis = {};
|
|
166
|
+
|
|
167
|
+
const apiChecks = await Promise.all(
|
|
168
|
+
externalAPIs.map(api =>
|
|
169
|
+
checkExternalAPI(api.url).then(result => ({ name: api.name, ...result }))
|
|
170
|
+
)
|
|
171
|
+
);
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
apiChecks.forEach(result => {
|
|
174
|
+
const { name, ...checkResult } = result;
|
|
175
|
+
checks.checks.externalApis[name] = checkResult;
|
|
176
|
+
if (!checkResult.reachable) {
|
|
177
|
+
checks.status = 'unhealthy';
|
|
124
178
|
}
|
|
125
|
-
}
|
|
179
|
+
});
|
|
126
180
|
|
|
127
|
-
// Check available integrations
|
|
128
181
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
182
|
+
const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
|
|
183
|
+
? moduleFactory.moduleTypes
|
|
184
|
+
: [];
|
|
185
|
+
const integrationTypes = Array.isArray(
|
|
186
|
+
integrationFactory.integrationTypes
|
|
187
|
+
)
|
|
188
|
+
? integrationFactory.integrationTypes
|
|
189
|
+
: [];
|
|
190
|
+
|
|
132
191
|
checks.checks.integrations = {
|
|
133
192
|
status: 'healthy',
|
|
134
193
|
modules: {
|
|
135
|
-
count:
|
|
136
|
-
available:
|
|
194
|
+
count: moduleTypes.length,
|
|
195
|
+
available: moduleTypes,
|
|
137
196
|
},
|
|
138
197
|
integrations: {
|
|
139
|
-
count:
|
|
140
|
-
available:
|
|
141
|
-
}
|
|
198
|
+
count: integrationTypes.length,
|
|
199
|
+
available: integrationTypes,
|
|
200
|
+
},
|
|
142
201
|
};
|
|
143
202
|
} catch (error) {
|
|
144
203
|
checks.checks.integrations = {
|
|
145
204
|
status: 'unhealthy',
|
|
146
205
|
error: error.message
|
|
147
206
|
};
|
|
148
|
-
checks.status = '
|
|
207
|
+
checks.status = 'unhealthy';
|
|
149
208
|
}
|
|
150
209
|
|
|
151
|
-
// Memory usage
|
|
152
|
-
const memoryUsage = process.memoryUsage();
|
|
153
|
-
checks.checks.memory = {
|
|
154
|
-
status: 'healthy',
|
|
155
|
-
rss: Math.round(memoryUsage.rss / 1024 / 1024) + ' MB',
|
|
156
|
-
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024) + ' MB',
|
|
157
|
-
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024) + ' MB',
|
|
158
|
-
external: Math.round(memoryUsage.external / 1024 / 1024) + ' MB'
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Overall response time
|
|
162
210
|
checks.responseTime = Date.now() - startTime;
|
|
163
211
|
|
|
164
|
-
|
|
165
|
-
const statusCode = checks.status === 'healthy' ? 200 :
|
|
166
|
-
checks.status === 'degraded' ? 200 : 503;
|
|
212
|
+
const statusCode = checks.status === 'healthy' ? 200 : 503;
|
|
167
213
|
|
|
168
214
|
res.status(statusCode).json(checks);
|
|
169
215
|
});
|
|
170
216
|
|
|
171
|
-
|
|
172
|
-
router.get('/health/live', (req, res) => {
|
|
217
|
+
router.get('/health/live', (_req, res) => {
|
|
173
218
|
res.status(200).json({
|
|
174
219
|
status: 'alive',
|
|
175
220
|
timestamp: new Date().toISOString()
|
|
176
221
|
});
|
|
177
222
|
});
|
|
178
223
|
|
|
179
|
-
|
|
180
|
-
router.get('/health/ready', async (req, res) => {
|
|
224
|
+
router.get('/health/ready', async (_req, res) => {
|
|
181
225
|
const checks = {
|
|
182
226
|
ready: true,
|
|
183
227
|
timestamp: new Date().toISOString(),
|
|
184
228
|
checks: {}
|
|
185
229
|
};
|
|
186
230
|
|
|
187
|
-
// Check database is connected
|
|
188
231
|
const dbState = mongoose.connection.readyState;
|
|
189
232
|
checks.checks.database = dbState === 1;
|
|
190
233
|
|
|
191
|
-
// Check critical services are loaded
|
|
192
234
|
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
235
|
+
const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
|
|
236
|
+
? moduleFactory.moduleTypes
|
|
237
|
+
: [];
|
|
238
|
+
checks.checks.modules = moduleTypes.length > 0;
|
|
195
239
|
} catch (error) {
|
|
196
240
|
checks.checks.modules = false;
|
|
197
241
|
}
|
|
198
242
|
|
|
199
|
-
// Determine overall readiness
|
|
200
243
|
checks.ready = checks.checks.database && checks.checks.modules;
|
|
201
244
|
|
|
202
245
|
const statusCode = checks.ready ? 200 : 503;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
const express = require('express');
|
|
3
|
-
const { router } = require('./health');
|
|
4
|
-
const mongoose = require('mongoose');
|
|
1
|
+
process.env.HEALTH_API_KEY = 'test-api-key';
|
|
5
2
|
|
|
6
|
-
// Mock mongoose connection
|
|
7
3
|
jest.mock('mongoose', () => ({
|
|
4
|
+
set: jest.fn(),
|
|
8
5
|
connection: {
|
|
9
6
|
readyState: 1,
|
|
10
7
|
db: {
|
|
@@ -15,122 +12,192 @@ jest.mock('mongoose', () => ({
|
|
|
15
12
|
}
|
|
16
13
|
}));
|
|
17
14
|
|
|
18
|
-
// Mock backend-utils
|
|
19
15
|
jest.mock('./../backend-utils', () => ({
|
|
20
16
|
moduleFactory: {
|
|
21
|
-
|
|
22
|
-
'test-module': {},
|
|
23
|
-
'another-module': {}
|
|
24
|
-
})
|
|
17
|
+
moduleTypes: ['test-module', 'another-module']
|
|
25
18
|
},
|
|
26
19
|
integrationFactory: {
|
|
27
|
-
|
|
28
|
-
'test-integration': {},
|
|
29
|
-
'another-integration': {}
|
|
30
|
-
})
|
|
20
|
+
integrationTypes: ['test-integration', 'another-integration']
|
|
31
21
|
}
|
|
32
22
|
}));
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
jest.mock('./../app-handler-helpers', () => ({
|
|
25
|
+
createAppHandler: jest.fn((name, router) => ({ name, router }))
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const { router } = require('./health');
|
|
29
|
+
const mongoose = require('mongoose');
|
|
36
30
|
|
|
31
|
+
const mockRequest = (path, headers = {}) => ({
|
|
32
|
+
path,
|
|
33
|
+
headers
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mockResponse = () => {
|
|
37
|
+
const res = {};
|
|
38
|
+
res.status = jest.fn().mockReturnValue(res);
|
|
39
|
+
res.json = jest.fn().mockReturnValue(res);
|
|
40
|
+
return res;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('Health Check Endpoints', () => {
|
|
37
44
|
beforeEach(() => {
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
mongoose.connection.readyState = 1;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Middleware - validateApiKey', () => {
|
|
49
|
+
it('should allow access to /health without authentication', async () => {
|
|
50
|
+
expect(true).toBe(true);
|
|
51
|
+
});
|
|
40
52
|
});
|
|
41
53
|
|
|
42
54
|
describe('GET /health', () => {
|
|
43
|
-
it('should return
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
it('should return basic health status', async () => {
|
|
56
|
+
const req = mockRequest('/health');
|
|
57
|
+
const res = mockResponse();
|
|
58
|
+
|
|
59
|
+
const routeHandler = router.stack.find(layer =>
|
|
60
|
+
layer.route && layer.route.path === '/health'
|
|
61
|
+
).route.stack[0].handle;
|
|
62
|
+
|
|
63
|
+
await routeHandler(req, res);
|
|
64
|
+
|
|
65
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
66
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
67
|
+
status: 'ok',
|
|
68
|
+
timestamp: expect.any(String),
|
|
69
|
+
service: 'frigg-core-api'
|
|
70
|
+
});
|
|
52
71
|
});
|
|
53
72
|
});
|
|
54
73
|
|
|
55
74
|
describe('GET /health/detailed', () => {
|
|
56
|
-
it('should return
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
it('should return detailed health status when healthy', async () => {
|
|
76
|
+
const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
|
|
77
|
+
const res = mockResponse();
|
|
78
|
+
|
|
79
|
+
const originalPromiseAll = Promise.all;
|
|
80
|
+
Promise.all = jest.fn().mockResolvedValue([
|
|
81
|
+
{ name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
|
|
82
|
+
{ name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const routeHandler = router.stack.find(layer =>
|
|
86
|
+
layer.route && layer.route.path === '/health/detailed'
|
|
87
|
+
).route.stack[0].handle;
|
|
88
|
+
|
|
89
|
+
await routeHandler(req, res);
|
|
90
|
+
|
|
91
|
+
Promise.all = originalPromiseAll;
|
|
92
|
+
|
|
93
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
94
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
95
|
+
status: 'healthy',
|
|
96
|
+
service: 'frigg-core-api',
|
|
97
|
+
timestamp: expect.any(String),
|
|
98
|
+
checks: expect.objectContaining({
|
|
99
|
+
database: expect.objectContaining({
|
|
100
|
+
status: 'healthy',
|
|
101
|
+
state: 'connected'
|
|
102
|
+
}),
|
|
103
|
+
integrations: expect.objectContaining({
|
|
104
|
+
status: 'healthy'
|
|
105
|
+
})
|
|
106
|
+
}),
|
|
107
|
+
responseTime: expect.any(Number)
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const response = res.json.mock.calls[0][0];
|
|
111
|
+
expect(response).not.toHaveProperty('version');
|
|
112
|
+
expect(response).not.toHaveProperty('uptime');
|
|
113
|
+
expect(response.checks).not.toHaveProperty('memory');
|
|
114
|
+
expect(response.checks.database).not.toHaveProperty('type');
|
|
69
115
|
});
|
|
70
116
|
|
|
71
|
-
it('should
|
|
72
|
-
|
|
73
|
-
.get('/health/detailed')
|
|
74
|
-
.expect(200);
|
|
117
|
+
it('should return 503 when database is disconnected', async () => {
|
|
118
|
+
mongoose.connection.readyState = 0;
|
|
75
119
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
expect(response.body.checks.database).toHaveProperty('type', 'mongodb');
|
|
79
|
-
});
|
|
120
|
+
const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
|
|
121
|
+
const res = mockResponse();
|
|
80
122
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
const originalPromiseAll = Promise.all;
|
|
124
|
+
Promise.all = jest.fn().mockResolvedValue([
|
|
125
|
+
{ name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
|
|
126
|
+
{ name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
|
|
127
|
+
]);
|
|
85
128
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
129
|
+
const routeHandler = router.stack.find(layer =>
|
|
130
|
+
layer.route && layer.route.path === '/health/detailed'
|
|
131
|
+
).route.stack[0].handle;
|
|
132
|
+
|
|
133
|
+
await routeHandler(req, res);
|
|
134
|
+
|
|
135
|
+
Promise.all = originalPromiseAll;
|
|
136
|
+
|
|
137
|
+
expect(res.status).toHaveBeenCalledWith(503);
|
|
138
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
139
|
+
status: 'unhealthy'
|
|
140
|
+
}));
|
|
89
141
|
});
|
|
90
142
|
});
|
|
91
143
|
|
|
92
144
|
describe('GET /health/live', () => {
|
|
93
|
-
it('should return
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
145
|
+
it('should return alive status', async () => {
|
|
146
|
+
const req = mockRequest('/health/live', { 'x-api-key': 'test-api-key' });
|
|
147
|
+
const res = mockResponse();
|
|
148
|
+
|
|
149
|
+
const routeHandler = router.stack.find(layer =>
|
|
150
|
+
layer.route && layer.route.path === '/health/live'
|
|
151
|
+
).route.stack[0].handle;
|
|
97
152
|
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
routeHandler(req, res);
|
|
154
|
+
|
|
155
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
156
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
157
|
+
status: 'alive',
|
|
158
|
+
timestamp: expect.any(String)
|
|
159
|
+
});
|
|
100
160
|
});
|
|
101
161
|
});
|
|
102
162
|
|
|
103
163
|
describe('GET /health/ready', () => {
|
|
104
|
-
it('should return
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
it('should return ready when all checks pass', async () => {
|
|
165
|
+
const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
|
|
166
|
+
const res = mockResponse();
|
|
167
|
+
|
|
168
|
+
const routeHandler = router.stack.find(layer =>
|
|
169
|
+
layer.route && layer.route.path === '/health/ready'
|
|
170
|
+
).route.stack[0].handle;
|
|
171
|
+
|
|
172
|
+
await routeHandler(req, res);
|
|
173
|
+
|
|
174
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
175
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
176
|
+
ready: true,
|
|
177
|
+
timestamp: expect.any(String),
|
|
178
|
+
checks: {
|
|
179
|
+
database: true,
|
|
180
|
+
modules: true
|
|
181
|
+
}
|
|
182
|
+
});
|
|
113
183
|
});
|
|
114
184
|
|
|
115
185
|
it('should return 503 when database is not connected', async () => {
|
|
116
|
-
// Mock disconnected database
|
|
117
186
|
mongoose.connection.readyState = 0;
|
|
118
187
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
.expect(503);
|
|
188
|
+
const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
|
|
189
|
+
const res = mockResponse();
|
|
122
190
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
191
|
+
const routeHandler = router.stack.find(layer =>
|
|
192
|
+
layer.route && layer.route.path === '/health/ready'
|
|
193
|
+
).route.stack[0].handle;
|
|
194
|
+
|
|
195
|
+
await routeHandler(req, res);
|
|
128
196
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// for easier unit testing
|
|
197
|
+
expect(res.status).toHaveBeenCalledWith(503);
|
|
198
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
199
|
+
ready: false
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
135
202
|
});
|
|
136
203
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.405.b87f8d8.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/boom": "^10.0.1",
|
|
7
7
|
"aws-sdk": "^2.1200.0",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"uuid": "^9.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
26
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
27
|
-
"@friggframework/test": "2.0.0--canary.
|
|
25
|
+
"@friggframework/eslint-config": "2.0.0--canary.405.b87f8d8.0",
|
|
26
|
+
"@friggframework/prettier-config": "2.0.0--canary.405.b87f8d8.0",
|
|
27
|
+
"@friggframework/test": "2.0.0--canary.405.b87f8d8.0",
|
|
28
28
|
"@types/lodash": "4.17.15",
|
|
29
29
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
30
30
|
"chai": "^4.3.6",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
},
|
|
54
54
|
"homepage": "https://github.com/friggframework/frigg#readme",
|
|
55
55
|
"description": "",
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "b87f8d874639f6fbb52c8a7efc7841c879a1286f"
|
|
57
57
|
}
|