Skip to content

Commit e44c74b

Browse files
committed
feat: add info page for debugging
1 parent a4d3971 commit e44c74b

36 files changed

Lines changed: 564 additions & 54 deletions

.env.template

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ GATEWAY_API_KEY=
5656
DEBUG=false
5757
# Whether 'verbose' level logs should be enabled
5858
VERBOSE=false
59-
# Prevent heap allocation failure
60-
NODE_OPTIONS="--max-old-space-size=8192"
6159
# Enable rate limitting
6260
THROTTLER_ENABLED=true
6361
# Disable iteration for password hashing (not recommended for production)

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ jobs:
7474
- name: Build and Push Docker Images
7575
uses: docker/build-push-action@v6
7676
with:
77+
build-args: |
78+
RELEASE_VERSION=${{ needs.configure.outputs.current_version }}
7779
context: .
7880
file: ${{ matrix.dockerfile }}
7981
push: ${{ needs.configure.outputs.is_release }}

apps/api/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
FROM node:lts-alpine AS base
22
WORKDIR /app
3+
ARG RELEASE_VERSION
34
ENV GATEWAY_DATABASE_URL="file:/dev/null"
45
ENV PNPM_HOME="/pnpm"
56
ENV PATH="$PNPM_HOME:$PATH"

apps/api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"private": true,
66
"license": "Apache-2.0",
77
"scripts": {
8-
"build": "tsx scripts/build.ts",
8+
"build": "NODE_ENV=production tsx scripts/build.ts",
99
"db:generate": "prisma generate",
1010
"dev": "NODE_ENV=development env-cmd -f ../../.env tsx scripts/dev.ts",
1111
"dev:test": "NODE_ENV=test env-cmd -f ../../.env tsx scripts/dev.ts",
@@ -38,6 +38,7 @@
3838
"@opendatacapture/demo": "workspace:*",
3939
"@opendatacapture/instrument-library": "workspace:*",
4040
"@opendatacapture/instrument-utils": "workspace:*",
41+
"@opendatacapture/release-info": "workspace:*",
4142
"@opendatacapture/runtime-core": "workspace:*",
4243
"@opendatacapture/runtime-v1": "workspace:*",
4344
"@opendatacapture/schemas": "workspace:*",

apps/api/scripts/build.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type { BuildOptions } from 'esbuild';
1010
import esbuildPluginTsc from 'esbuild-plugin-tsc';
1111
const require = module.createRequire(import.meta.url);
1212

13+
import { getReleaseInfo } from '@opendatacapture/release-info';
14+
1315
const entryFile = path.resolve(import.meta.dirname, '../src/main.ts');
1416
const outdir = path.resolve(import.meta.dirname, '../dist');
1517
const tsconfig = path.resolve(import.meta.dirname, '../tsconfig.json');
@@ -25,6 +27,9 @@ const options: { external: NonNullable<unknown>; plugins: NonNullable<unknown> }
2527
js: "Object.defineProperties(globalThis, { __dirname: { value: import.meta.dirname, writable: false }, __filename: { value: import.meta.filename, writable: false }, require: { value: (await import('module')).createRequire(import.meta.url), writable: false } });"
2628
},
2729
bundle: true,
30+
define: {
31+
__RELEASE_INFO__: JSON.stringify(await getReleaseInfo())
32+
},
2833
entryPoints: [entryFile],
2934
external: ['@nestjs/microservices', '@nestjs/websockets/socket-module', 'class-transformer', 'class-validator'],
3035
format: 'esm',
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Controller, Get } from '@nestjs/common';
2+
import { ApiTags } from '@nestjs/swagger';
3+
import type { GatewayHealthcheckResult } from '@opendatacapture/schemas/gateway';
4+
5+
import { RouteAccess } from '@/core/decorators/route-access.decorator';
6+
7+
import { GatewayService } from './gateway.service';
8+
9+
@ApiTags('Gateway')
10+
@Controller({ path: 'gateway' })
11+
export class GatewayController {
12+
constructor(private readonly gatewayService: GatewayService) {}
13+
14+
@Get('healthcheck')
15+
@RouteAccess([])
16+
healthcheck(): Promise<GatewayHealthcheckResult> {
17+
return this.gatewayService.healthcheck();
18+
}
19+
}

apps/api/src/gateway/gateway.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { InstrumentsModule } from '@/instruments/instruments.module';
88
import { SessionsModule } from '@/sessions/sessions.module';
99
import { SetupModule } from '@/setup/setup.module';
1010

11+
import { GatewayController } from './gateway.controller';
1112
import { GatewayService } from './gateway.service';
1213
import { GatewaySynchronizer } from './gateway.synchronizer';
1314

1415
@Module({
16+
controllers: [GatewayController],
1517
exports: [GatewayService],
1618
imports: [
1719
forwardRef(() => AssignmentsModule),

apps/api/src/gateway/gateway.service.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import type {
88
MutateAssignmentResponseBody,
99
RemoteAssignment
1010
} from '@opendatacapture/schemas/assignment';
11+
import {
12+
$GatewayHealthcheckSuccessResult,
13+
type GatewayHealthcheckFailureResult,
14+
type GatewayHealthcheckResult
15+
} from '@opendatacapture/schemas/gateway';
1116

1217
import { InstrumentsService } from '@/instruments/instruments.service';
1318

@@ -71,4 +76,30 @@ export class GatewayService {
7176
}
7277
return result.data;
7378
}
79+
80+
async healthcheck(): Promise<GatewayHealthcheckResult> {
81+
const response = await this.httpService.axiosRef.get('/api/healthcheck');
82+
if (response.status !== HttpStatus.OK) {
83+
return {
84+
ok: false,
85+
status: response.status,
86+
statusText: response.statusText
87+
} satisfies GatewayHealthcheckFailureResult;
88+
}
89+
const result = await $GatewayHealthcheckSuccessResult.safeParseAsync(response.data);
90+
if (!result.success) {
91+
const statusText = 'Healthcheck data received from gateway do not match expected structure';
92+
this.logger.error({
93+
data: response.data as unknown,
94+
error: result.error.format(),
95+
message: `ERROR: ${statusText}`
96+
});
97+
return {
98+
ok: false,
99+
status: HttpStatus.INTERNAL_SERVER_ERROR,
100+
statusText
101+
};
102+
}
103+
return result.data;
104+
}
74105
}

apps/api/src/setup/setup.service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ForbiddenException, Injectable } from '@nestjs/common';
22
import { type CreateAdminData } from '@opendatacapture/schemas/setup';
3-
import type { InitAppOptions } from '@opendatacapture/schemas/setup';
3+
import type { InitAppOptions, SetupState } from '@opendatacapture/schemas/setup';
44

55
import { ConfigurationService } from '@/configuration/configuration.service';
66
import { DemoService } from '@/demo/demo.service';
@@ -28,8 +28,10 @@ export class SetupService {
2828
return {
2929
isDemo: Boolean(savedOptions?.isDemo),
3030
isGatewayEnabled: this.configurationService.get('GATEWAY_ENABLED'),
31-
isSetup: Boolean(savedOptions?.isSetup)
32-
};
31+
isSetup: Boolean(savedOptions?.isSetup),
32+
releaseInfo: __RELEASE_INFO__,
33+
uptime: Math.round(process.uptime())
34+
} satisfies SetupState;
3335
}
3436

3537
async initApp({ admin, dummySubjectCount, initDemo, recordsPerSubject }: InitAppOptions) {

apps/api/src/typings/global.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ReleaseInfo } from '@opendatacapture/schemas/setup';
2+
3+
declare global {
4+
const __RELEASE_INFO__: ReleaseInfo;
5+
}

0 commit comments

Comments
 (0)