Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#8216): propagate api request id to haproxy #9613

Merged
merged 12 commits into from
Nov 7, 2024
6 changes: 6 additions & 0 deletions api/src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ PouchDB.plugin(require('pouchdb-adapter-http'));
PouchDB.plugin(require('pouchdb-session-authentication'));
PouchDB.plugin(require('pouchdb-find'));
PouchDB.plugin(require('pouchdb-mapreduce'));
const asyncLocalStorage = require('./services/async-storage');
const { REQUEST_ID_HEADER } = require('./server-utils');
dianabarsan marked this conversation as resolved.
Show resolved Hide resolved

const { UNIT_TEST_ENV } = process.env;

Expand Down Expand Up @@ -74,6 +76,10 @@ if (UNIT_TEST_ENV) {
const fetch = (url, opts) => {
// Adding audit flag (haproxy) Service that made the request initially.
opts.headers.set('X-Medic-Service', 'api');
const localStorage = asyncLocalStorage.getStore();
if (localStorage?.clientRequest?.id) {
opts.headers.set(REQUEST_ID_HEADER, localStorage.clientRequest.id);
}
return PouchDB.fetch(url, opts);
};

Expand Down
4 changes: 3 additions & 1 deletion api/src/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const dbDocHandler = require('./controllers/db-doc');
const extensionLibs = require('./controllers/extension-libs');
const replication = require('./controllers/replication');
const app = express.Router({ strict: true });
const asyncLocalStorage = require('./services/async-storage');
const moment = require('moment');
const MAX_REQUEST_SIZE = '32mb';

Expand Down Expand Up @@ -158,7 +159,8 @@ if (process.argv.slice(2).includes('--allow-cors')) {

app.use((req, res, next) => {
req.id = uuid.v4();
dianabarsan marked this conversation as resolved.
Show resolved Hide resolved
next();
req.headers[serverUtils.REQUEST_ID_HEADER] = req.id;
dianabarsan marked this conversation as resolved.
Show resolved Hide resolved
asyncLocalStorage.run({ clientRequest: req }, () => next());
dianabarsan marked this conversation as resolved.
Show resolved Hide resolved
});
app.use(getLocale);

Expand Down
2 changes: 2 additions & 0 deletions api/src/server-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const environment = require('@medic/environment');
const isClientHuman = require('./is-client-human');
const logger = require('@medic/logger');
const MEDIC_BASIC_AUTH = 'Basic realm="Medic Web Services"';
const REQUEST_ID_HEADER = 'X-Request-UUID';
const cookie = require('./services/cookie');
const {InvalidArgumentError} = require('@medic/cht-datasource');

Expand Down Expand Up @@ -49,6 +50,7 @@ const promptForBasicAuth = res => {

module.exports = {
MEDIC_BASIC_AUTH: MEDIC_BASIC_AUTH,
REQUEST_ID_HEADER: REQUEST_ID_HEADER,

/*
* Attempts to determine the correct response given the error code.
Expand Down
8 changes: 8 additions & 0 deletions api/src/services/async-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
const { REQUEST_ID_HEADER } = require('../server-utils');

const request = require('@medic/couch-request');
request.initialize(asyncLocalStorage, REQUEST_ID_HEADER);

module.exports = asyncLocalStorage;
dianabarsan marked this conversation as resolved.
Show resolved Hide resolved
37 changes: 37 additions & 0 deletions api/tests/mocha/db.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const sinon = require('sinon');
require('chai').use(require('chai-as-promised'));
const PouchDB = require('pouchdb-core');
const { expect } = require('chai');
const rewire = require('rewire');
const request = require('@medic/couch-request');
Expand All @@ -8,6 +9,7 @@ let db;
let unitTestEnv;

const env = require('@medic/environment');
const asyncLocalStorage = require('../../src/services/async-storage');

describe('db', () => {
beforeEach(() => {
Expand Down Expand Up @@ -404,4 +406,39 @@ describe('db', () => {
.to.be.rejectedWith(Error, `Cannot add security: invalid db name dbanme or role`);
});
});

describe('fetch extension', () => {
it('should set headers where there is an active client request', async () => {
sinon.stub(PouchDB, 'fetch').resolves({
json: sinon.stub().resolves({ result: true }),
ok: true,
});
sinon.stub(asyncLocalStorage, 'getStore').returns({ clientRequest: { id: 'the_id' } });
db = rewire('../../src/db');

await db.medic.info();
const headers = PouchDB.fetch.args.map(arg => arg[1].headers);
expect(headers.length).to.equal(4);
headers.forEach((header) => {
expect(header.get('X-Medic-Service')).to.equal('api');
expect(header.get('X-Request-UUID')).to.equal('the_id');
});
});

it('should work when call is made without an active clinet request', async () => {
sinon.stub(PouchDB, 'fetch').resolves({
json: sinon.stub().resolves({ result: true }),
ok: true,
});
sinon.stub(asyncLocalStorage, 'getStore').returns(undefined);
db = rewire('../../src/db');

await db.medic.info();
const headers = PouchDB.fetch.args.map(arg => arg[1].headers);
expect(headers.length).to.equal(4);
headers.forEach((header) => {
expect(header.get('X-Medic-Service')).to.equal('api');
});
});
});
});
4 changes: 4 additions & 0 deletions api/tests/mocha/server-utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,8 @@ describe('Server utils', () => {
chai.expect(serverUtilsError.calledOnceWithExactly(error, req, res)).to.be.true;
});
});

it('should export request header', () => {
chai.expect(serverUtils.REQUEST_ID_HEADER).to.equal('X-Request-UUID');
});
});
27 changes: 27 additions & 0 deletions api/tests/mocha/services/async-storage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const sinon = require('sinon');
const rewire = require('rewire');
const { expect } = require('chai');
const asyncHooks = require('node:async_hooks');
const request = require('@medic/couch-request');
const serverUtils = require('../../../src/server-utils');

describe('async-storage', () => {
let service;
let asyncLocalStorage;

beforeEach(() => {
asyncLocalStorage = sinon.spy(asyncHooks, 'AsyncLocalStorage');
sinon.stub(request, 'initialize');
});

afterEach(() => {
sinon.restore();
});

it('should initialize async storage and initialize couch-request', async () => {
service = rewire('../../../src/services/async-storage');

expect(asyncLocalStorage.callCount).to.equal(1);
expect(request.initialize.args).to.deep.equal([[service, serverUtils.REQUEST_ID_HEADER]]);
});
});
3 changes: 2 additions & 1 deletion haproxy/default_frontend.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ frontend http-in
http-request capture req.hdr(x-medic-service) len 200 # capture.req.hdr(1)
http-request capture req.hdr(x-medic-user) len 200 # capture.req.hdr(2)
http-request capture req.hdr(user-agent) len 600 # capture.req.hdr(3)
http-request capture req.hdr(x-request-uuid) len 200 # capture.req.hdr(4)
capture response header Content-Length len 10 # capture.res.hdr(0)
http-response set-header Connection Keep-Alive
http-response set-header Keep-Alive timeout=18000
log global
log-format "%ci,%s,%ST,%Ta,%Ti,%TR,%[capture.req.method],%[capture.req.uri],%[capture.req.hdr(1)],%[capture.req.hdr(2)],'%[capture.req.hdr(0),lua.replacePassword]',%B,%Tr,%[capture.res.hdr(0)],'%[capture.req.hdr(3)]'"
log-format "%ci,%s,%ST,%Ta,%Ti,%TR,%[capture.req.method],%[capture.req.uri],%[capture.req.hdr(1)],%[capture.req.hdr(2)],%[capture.req.hdr(4)],'%[capture.req.hdr(0),lua.replacePassword]',%B,%Tr,%[capture.res.hdr(0)],'%[capture.req.hdr(3)]'"
default_backend couchdb-servers

16 changes: 15 additions & 1 deletion shared-libs/couch-request/src/couch-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ const request = require('request-promise-native');
const isPlainObject = require('lodash/isPlainObject');
const environment = require('@medic/environment');
const servername = environment.host;
let asyncLocalStorage;
let requestIdHeader;


const isString = value => typeof value === 'string' || value instanceof String;
const isTrue = value => isString(value) ? value.toLowerCase() === 'true' : value === true;
Expand All @@ -19,11 +22,17 @@ const methods = {
const mergeOptions = (target, source, exclusions = []) => {
for (const [key, value] of Object.entries(source)) {
if (Array.isArray(exclusions) && exclusions.includes(key)) {
return target;
continue;
}
target[key] = value; // locally, mutation is preferable to spreading as it doesn't
// make new objects in memory. Assuming this is a hot path.
}
const localStorage = asyncLocalStorage?.getStore();
if (localStorage?.clientRequest?.id) {
target.headers = target.headers || {};
target.headers[requestIdHeader] = localStorage?.clientRequest.id;
}

return target;
};

Expand Down Expand Up @@ -104,6 +113,11 @@ const getRequestType = (method) => {
};

module.exports = {
initialize: (store, header) => {
asyncLocalStorage = store;
requestIdHeader = header;
},

get: (first, second = {}) => req(methods.GET, first, second),
post: (first, second = {}) => req(methods.POST, first, second),
put: (first, second = {}) => req(methods.PUT, first, second),
Expand Down
Loading
Loading