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
30 changes: 9 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
# ─── Stage 1: Builder ────────────────────────────────────────────────────────
FROM node:20-alpine AS builder

FROM node:20-alpine AS dependencies
WORKDIR /app
RUN corepack enable
COPY server/package.json server/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY server/package*.json ./

RUN npm ci

COPY server/ .

# ─── Stage 2: Runner ─────────────────────────────────────────────────────────
FROM node:20-alpine AS runner

ENV NODE_ENV=production

WORKDIR /app

COPY server/package*.json ./

RUN npm ci --omit=dev

COPY --from=builder /app .

RUN corepack enable
COPY server/package.json server/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod
COPY server/ .
EXPOSE 5000

CMD ["node", "index.js"]
CMD ["node", "index.js"]
43 changes: 42 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: '3.8'
version: "3.8"

services:
mongodb:
Expand All @@ -22,6 +22,9 @@ services:
- .env
environment:
MONGO_URI: mongodb://mongodb:27017/soromint
METRICS_USERNAME: ${METRICS_USERNAME:-prometheus}
METRICS_PASSWORD: ${METRICS_PASSWORD:-soromint-metrics}
LOKI_HOST: http://loki:3100
depends_on:
- mongodb
restart: unless-stopped
Expand All @@ -36,6 +39,9 @@ services:
- .env
environment:
MONGO_URI: mongodb://mongodb:27017/soromint
METRICS_USERNAME: ${METRICS_USERNAME:-prometheus}
METRICS_PASSWORD: ${METRICS_PASSWORD:-soromint-metrics}
LOKI_HOST: http://loki:3100
depends_on:
- mongodb
restart: unless-stopped
Expand All @@ -51,5 +57,40 @@ services:
- server
restart: unless-stopped

prometheus:
image: prom/prometheus:v3.5.0
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
depends_on:
- server

loki:
image: grafana/loki:3.5.3
command: -config.file=/etc/loki/config.yml
ports:
- "3100:3100"
volumes:
- ./monitoring/loki/config.yml:/etc/loki/config.yml:ro
- loki_data:/loki

grafana:
image: grafana/grafana:12.1.0
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
depends_on:
- prometheus
- loki

volumes:
mongodb_data:
loki_data:
grafana_data:
13 changes: 13 additions & 0 deletions monitoring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SoroMint monitoring

The Compose stack starts Prometheus on port 9090, Loki on 3100, and Grafana on 3000. Grafana is provisioned with both data sources and the **SoroMint Server Performance** dashboard.

Set `METRICS_USERNAME` and `METRICS_PASSWORD` before production deployment. Prometheus's sample configuration uses `prometheus` / `soromint-metrics`; keep those values synchronized or mount a secret-managed Prometheus configuration. Set `LOKI_HOST` to enable the Winston Loki transport. `LOKI_BASIC_AUTH` is optional and uses the `username:password` format supported by winston-loki.

Start locally with `docker compose up --build`, then verify metrics with:

```sh
curl -u prometheus:soromint-metrics http://localhost:5000/metrics
```

The endpoint exports Node.js process metrics plus request counts and latency, transaction latency, MongoDB pool usage, active Socket.IO connections, and RPC failures. Application integrations can call `observeTransaction` and `recordRpcFailure` from `server/telemetry/metrics.js`.
63 changes: 63 additions & 0 deletions monitoring/grafana/dashboards/server-performance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"annotations": { "list": [] },
"editable": true,
"panels": [
{
"type": "timeseries",
"title": "API request rate by response code",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
"targets": [
{
"expr": "sum by (status_code) (rate(soromint_http_requests_total[5m]))",
"legendFormat": "{{status_code}}"
}
]
},
{
"type": "timeseries",
"title": "API latency p95",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
"targets": [
{
"expr": "histogram_quantile(0.95, sum by (le) (rate(soromint_http_request_duration_seconds_bucket[5m])))",
"legendFormat": "p95"
}
]
},
{
"type": "stat",
"title": "Active WebSockets",
"gridPos": { "h": 6, "w": 6, "x": 0, "y": 8 },
"targets": [{ "expr": "soromint_websocket_connections_active" }]
},
{
"type": "timeseries",
"title": "MongoDB pool",
"gridPos": { "h": 6, "w": 9, "x": 6, "y": 8 },
"targets": [
{
"expr": "soromint_database_pool_connections",
"legendFormat": "{{state}}"
}
]
},
{
"type": "timeseries",
"title": "RPC failures",
"gridPos": { "h": 6, "w": 9, "x": 15, "y": 8 },
"targets": [
{
"expr": "sum by (endpoint, method) (rate(soromint_rpc_failures_total[5m]))",
"legendFormat": "{{endpoint}} {{method}}"
}
]
}
],
"schemaVersion": 39,
"tags": ["soromint", "operations"],
"templating": { "list": [] },
"time": { "from": "now-6h", "to": "now" },
"title": "SoroMint Server Performance",
"uid": "soromint-server-performance",
"version": 1
}
7 changes: 7 additions & 0 deletions monitoring/grafana/provisioning/dashboards/dashboards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: 1
providers:
- name: SoroMint
folder: SoroMint
type: file
options:
path: /var/lib/grafana/dashboards
11 changes: 11 additions & 0 deletions monitoring/grafana/provisioning/datasources/datasources.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
22 changes: 22 additions & 0 deletions monitoring/loki/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
10 changes: 10 additions & 0 deletions monitoring/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: soromint-server
metrics_path: /metrics
basic_auth:
username: prometheus
password: soromint-metrics
static_configs:
- targets: [server:5000]
7 changes: 7 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,10 @@ DEPLOYMENT_SOFT_LIMIT_PERCENTAGE=80

# Soft limit warning cooldown (hours) - how often to send warnings
SOFT_LIMIT_WARNING_COOLDOWN_HOURS=24

# Operational telemetry
METRICS_USERNAME=prometheus
METRICS_PASSWORD=change-me
LOKI_HOST=http://localhost:3100
LOKI_BASIC_AUTH=
LOG_SERVICE_NAME=soromint-server
26 changes: 19 additions & 7 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ initEnv();
const { scheduleBackups } = require('./services/backup-service');
const { startFeeMonitor } = require('./services/fee-monitor-service');
const { getCacheService } = require('./services/cache-service');
const { startReconciliationWorker } = require('./services/reconciliation-service');
const {
startReconciliationWorker,
} = require('./services/reconciliation-service');

const express = require('express');
const mongoose = require('mongoose');
Expand All @@ -21,7 +23,10 @@ mongoose.plugin(softDeletePlugin);
const cors = require('cors');
const { securityHeaders } = require('./middleware/security-headers');
const { createCorsOptionsDelegate } = require('./config/cors-config');
const { globalReadRateLimiter, globalWriteRateLimiter } = require('./middleware/rate-limiter');
const {
globalReadRateLimiter,
globalWriteRateLimiter,
} = require('./middleware/rate-limiter');

const { initSentry } = require('./config/sentry');
const { errorHandler, notFoundHandler } = require('./middleware/error-handler');
Expand All @@ -35,6 +40,8 @@ const {
} = require('./utils/logger');
const { setupSwagger } = require('./config/swagger');
const { sampler } = require('./services/resource-sampler');
const BillingService = require('./services/billing-service');
const { metricsRouter, metricsMiddleware } = require('./telemetry/metrics');
const authRoutes = require('./routes/auth-routes');
const statusRoutes = require('./routes/status-routes');
const auditRoutes = require('./routes/audit-routes');
Expand Down Expand Up @@ -64,16 +71,18 @@ const createApp = ({
} = {}) => {
const app = express();
const corsMiddleware = cors(createCorsOptionsDelegate());
const fraudMiddleware = FraudDetectionMiddleware.getInstance();

initSentry(app);
app.use(securityHeaders);
app.use(correlationIdMiddleware);
app.use(httpLoggerMiddleware);
app.use(metricsMiddleware);
app.use(corsMiddleware);
app.options('*', corsMiddleware);
app.use(express.json());

app.use('/metrics', metricsRouter);

app.use('/api', globalReadRateLimiter);
app.use('/api', globalWriteRateLimiter);

Expand Down Expand Up @@ -136,15 +145,18 @@ const startServer = async () => {
}

await connectDatabase();

// Initialize billing system
try {
await BillingService.initializeAccountTiers();
logger.info('Billing system initialized successfully');
} catch (error) {
logger.warn('Billing system initialization failed, continuing with existing tiers', {
error: error.message,
});
logger.warn(
'Billing system initialization failed, continuing with existing tiers',
{
error: error.message,
}
);
}

const app = createApp();
Expand Down
27 changes: 13 additions & 14 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,39 @@
"@aws-sdk/client-s3": "^3.600.0",
"@sendgrid/mail": "^8.1.6",
"@sentry/node": "^10.45.0",
"@socket.io/redis-adapter": "^8.3.0",
"@socket.io/redis-emitter": "^5.1.0",
"@stellar/stellar-sdk": "^15.0.1",
"bullmq": "^5.76.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"mongoose": "^8.4.1",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"jsonwebtoken": "^9.0.3",
"dotenv": "^17.4.0",
"envalid": "^8.0.0",
"express": "^4.19.2",
"express-rate-limit": "^8.4.1",
"express-validator": "^7.0.1",
"helmet": "^8.0.0",
"i18next": "^23.7.21",
"i18next-fs-backend": "^2.3.1",
"i18next-http-middleware": "^3.5.0",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.3",
"knex": "^3.2.7",
"mongoose": "^8.4.1",
"multer": "^2.1.1",
"node-cron": "^4.2.1",
"pg": "^8.20.0",
"prom-client": "^15.1.3",
"redis": "^5.11.0",
"socket.io": "^4.7.5",
"@socket.io/redis-adapter": "^8.3.0",
"@socket.io/redis-emitter": "^5.1.0",
"sqlite3": "^6.0.1",
"zod": "^4.3.6",
"stripe": "^14.0.0"
"stripe": "^14.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
"winston-loki": "^6.1.5",
"zod": "^4.3.6"
},


"devDependencies": {
"eslint": "^10.1.0",
"eslint-config-prettier": "^10.1.8",
Expand Down
Loading
Loading