Merge branch 'main' into fix/mobile-view
Run Tests on Branches / Detect Changes (push) Successful in 11s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m45s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Detect Changes (push) Successful in 11s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m45s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
This commit is contained in:
@@ -2055,6 +2055,12 @@ app.put('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) => {
|
||||
const id = parseInt(req.params.id);
|
||||
const { price, stock } = req.body;
|
||||
|
||||
// Get current state before update to detect stock becoming available
|
||||
const current = await userDbGet(
|
||||
'SELECT * FROM pharmacy_medicines WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
await userDbRun(
|
||||
'UPDATE pharmacy_medicines SET price = ?, stock = ? WHERE id = ?',
|
||||
[price || null, stock || 0, id]
|
||||
@@ -2069,6 +2075,16 @@ app.put('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) => {
|
||||
return res.status(404).json({ error: 'Relationship not found' });
|
||||
}
|
||||
|
||||
// Send push if stock went from 0 to available
|
||||
if (current && (current.stock || 0) === 0 && (stock || 0) > 0) {
|
||||
const pharmacy = await userDbGet('SELECT id, name FROM pharmacies WHERE id = ?', [updated.pharmacy_id]);
|
||||
sendPushForMedicine({
|
||||
medicine_nregistro: updated.medicine_nregistro,
|
||||
medicine_name: updated.medicine_name,
|
||||
pharmacy,
|
||||
}).catch(err => console.error('[push] stock-change fanout error:', err));
|
||||
}
|
||||
|
||||
res.json(updated);
|
||||
appMetrics.pharmacyMedicineLinkTotal.add(1, { op: 'update' });
|
||||
} catch (error) {
|
||||
|
||||
+62
-23
@@ -1,87 +1,126 @@
|
||||
// OpenTelemetry metrics for the FarmaFinder backend.
|
||||
//
|
||||
// Uses the global MeterProvider configured in `tracing.js` (imported first in
|
||||
// server.js). Metrics flow OTLP → Alloy → Prometheus remote_write, exactly like
|
||||
// the tracing pipeline, so no extra Prometheus scrape job is required.
|
||||
// IMPORTANT: In ESM, static imports are hoisted and evaluated BEFORE the
|
||||
// module body runs. Since server.js does `await import('./src/tracing.js')`
|
||||
// to call sdk.start(), but statically imports this file, the meter MUST be
|
||||
// lazily initialised — otherwise metrics.getMeter() runs before the SDK
|
||||
// configures the global MeterProvider and we get a NoopMeter.
|
||||
//
|
||||
// Metrics flow OTLP → Alloy → Prometheus remote_write, exactly like the
|
||||
// tracing pipeline, so no extra Prometheus scrape job is required.
|
||||
import { metrics } from '@opentelemetry/api';
|
||||
|
||||
const meter = metrics.getMeter('farmafinder-backend', '1.0.0');
|
||||
let _meter;
|
||||
|
||||
function getMeter() {
|
||||
if (!_meter) {
|
||||
_meter = metrics.getMeter('farmafinder-backend', '1.0.0');
|
||||
}
|
||||
return _meter;
|
||||
}
|
||||
|
||||
// Lazy instrument wrappers — each property getter creates the instrument on
|
||||
// first access, by which point sdk.start() has run and the real MeterProvider
|
||||
// is in place. The returned instruments are plain OTel Counter / Histogram
|
||||
// objects, so all callers (appMetrics.foo.add(1)) work unchanged.
|
||||
|
||||
function lazyCounter(name, opts) {
|
||||
let inst;
|
||||
return {
|
||||
get current() {
|
||||
if (!inst) inst = getMeter().createCounter(name, opts);
|
||||
return inst;
|
||||
},
|
||||
add(value, attrs) { this.current.add(value, attrs); },
|
||||
};
|
||||
}
|
||||
|
||||
function lazyHistogram(name, opts) {
|
||||
let inst;
|
||||
return {
|
||||
get current() {
|
||||
if (!inst) inst = getMeter().createHistogram(name, opts);
|
||||
return inst;
|
||||
},
|
||||
record(value, attrs) { this.current.record(value, attrs); },
|
||||
};
|
||||
}
|
||||
|
||||
// --- Heartbeat -------------------------------------------------------------
|
||||
// Emitted continuously so we can alert on "backend not sending telemetry"
|
||||
// (remote_write has no `up` metric).
|
||||
export const heartbeatTotal = meter.createCounter('app_heartbeat_total', {
|
||||
export const heartbeatTotal = lazyCounter('app_heartbeat_total', {
|
||||
description: 'Heartbeat ticks from the backend process (liveness signal).',
|
||||
});
|
||||
|
||||
// --- Medicine search / CIMA upstream --------------------------------------
|
||||
export const medicineSearchesTotal = meter.createCounter('medicine_searches_total', {
|
||||
export const medicineSearchesTotal = lazyCounter('medicine_searches_total', {
|
||||
description: 'Total medicine searches handled (cache or upstream).',
|
||||
});
|
||||
|
||||
export const cimaRequestsTotal = meter.createCounter('cima_requests_total', {
|
||||
export const cimaRequestsTotal = lazyCounter('cima_requests_total', {
|
||||
description: 'Total upstream CIMA API requests.',
|
||||
});
|
||||
|
||||
export const cimaRequestDuration = meter.createHistogram('cima_request_duration_ms', {
|
||||
export const cimaRequestDuration = lazyHistogram('cima_request_duration_ms', {
|
||||
description: 'Duration of upstream CIMA API requests.',
|
||||
unit: 'ms',
|
||||
});
|
||||
|
||||
export const cacheHitsTotal = meter.createCounter('cache_hits_total', {
|
||||
export const cacheHitsTotal = lazyCounter('cache_hits_total', {
|
||||
description: 'Total Redis cache hits.',
|
||||
});
|
||||
|
||||
export const cacheMissesTotal = meter.createCounter('cache_misses_total', {
|
||||
export const cacheMissesTotal = lazyCounter('cache_misses_total', {
|
||||
description: 'Total Redis cache misses (fell through to upstream).',
|
||||
});
|
||||
|
||||
// --- Auth ------------------------------------------------------------------
|
||||
export const loginSuccessTotal = meter.createCounter('login_success_total', {
|
||||
export const loginSuccessTotal = lazyCounter('login_success_total', {
|
||||
description: 'Successful login attempts.',
|
||||
});
|
||||
|
||||
export const loginFailureTotal = meter.createCounter('login_failure_total', {
|
||||
export const loginFailureTotal = lazyCounter('login_failure_total', {
|
||||
description: 'Failed login attempts.',
|
||||
});
|
||||
|
||||
export const rateLimitRejectedTotal = meter.createCounter('rate_limit_rejected_total', {
|
||||
export const rateLimitRejectedTotal = lazyCounter('rate_limit_rejected_total', {
|
||||
description: 'Requests rejected by a rate limiter.',
|
||||
});
|
||||
|
||||
// --- Admin operations ------------------------------------------------------
|
||||
export const pharmacyWriteTotal = meter.createCounter('pharmacy_write_total', {
|
||||
export const pharmacyWriteTotal = lazyCounter('pharmacy_write_total', {
|
||||
description: 'Admin pharmacy write operations.',
|
||||
});
|
||||
|
||||
export const pharmacyMedicineLinkTotal = meter.createCounter('pharmacy_medicine_link_total', {
|
||||
export const pharmacyMedicineLinkTotal = lazyCounter('pharmacy_medicine_link_total', {
|
||||
description: 'Pharmacy-medicine link operations.',
|
||||
});
|
||||
|
||||
// --- Push notifications ----------------------------------------------------
|
||||
export const pushSentTotal = meter.createCounter('push_sent_total', {
|
||||
export const pushSentTotal = lazyCounter('push_sent_total', {
|
||||
description: 'Push notifications successfully sent.',
|
||||
});
|
||||
|
||||
export const pushFailedTotal = meter.createCounter('push_failed_total', {
|
||||
export const pushFailedTotal = lazyCounter('push_failed_total', {
|
||||
description: 'Push notifications that failed to send.',
|
||||
});
|
||||
|
||||
// --- Datastores ------------------------------------------------------------
|
||||
export const dbQueryDuration = meter.createHistogram('db_query_duration_ms', {
|
||||
export const dbQueryDuration = lazyHistogram('db_query_duration_ms', {
|
||||
description: 'Duration of user DB queries.',
|
||||
unit: 'ms',
|
||||
});
|
||||
|
||||
export const dbErrorsTotal = meter.createCounter('db_errors_total', {
|
||||
export const dbErrorsTotal = lazyCounter('db_errors_total', {
|
||||
description: 'User DB query errors.',
|
||||
});
|
||||
|
||||
export const redisErrorsTotal = meter.createCounter('redis_errors_total', {
|
||||
export const redisErrorsTotal = lazyCounter('redis_errors_total', {
|
||||
description: 'Redis client errors.',
|
||||
});
|
||||
|
||||
export const redisCmdDuration = meter.createHistogram('redis_cmd_duration_ms', {
|
||||
export const redisCmdDuration = lazyHistogram('redis_cmd_duration_ms', {
|
||||
description: 'Duration of Redis commands.',
|
||||
unit: 'ms',
|
||||
});
|
||||
@@ -89,11 +128,11 @@ export const redisCmdDuration = meter.createHistogram('redis_cmd_duration_ms', {
|
||||
// --- HTTP (explicit, so we get status class + per-route breakdowns) --------
|
||||
// OTel's http auto-instrumentation records duration but not a discrete
|
||||
// error count, so we track requests explicitly for error-rate alerting.
|
||||
export const httpRequestsTotal = meter.createCounter('http_requests_total', {
|
||||
export const httpRequestsTotal = lazyCounter('http_requests_total', {
|
||||
description: 'Total HTTP requests, labelled by route and status class.',
|
||||
});
|
||||
|
||||
export const httpRequestDuration = meter.createHistogram('http_request_duration_ms', {
|
||||
export const httpRequestDuration = lazyHistogram('http_request_duration_ms', {
|
||||
description: 'HTTP request duration.',
|
||||
unit: 'ms',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user