Source: components/prometheusmetrics.js

  1. "use strict";
  2. /**
  3. * Prometheus-style /metrics gathering and exporting.
  4. * This class provides a central location to register gauge and counter metrics
  5. * used to generate the <code>/metrics</code> page.
  6. *
  7. * This class depends on having <code>prom-client</code> installed. It
  8. * will attempt to load this module when the constructor is invoked.
  9. *
  10. * @example <caption>A simple metric that counts the keys in an object:</caption>
  11. * var metrics = new PrometheusMetrics();
  12. *
  13. * var orange = {};
  14. * metrics.addGauge({
  15. * name: "oranges",
  16. * help: "current number of oranges",
  17. * refresh: (gauge) => {
  18. * gauge.set({}, Object.keys(oranges).length);
  19. * },
  20. * });
  21. *
  22. * @example <caption>Generating values for multiple gauges in a single collector
  23. * function.</caption>
  24. * var metrics = new PrometheusMetrics();
  25. *
  26. * var oranges_gauge = metrics.addGauge({
  27. * name: "oranges",
  28. * help: "current number of oranges",
  29. * });
  30. * var apples_gauge = metrics.addGauge({
  31. * name: "apples",
  32. * help: "current number of apples",
  33. * });
  34. *
  35. * metrics.addCollector(() => {
  36. * var counts = this._countFruit();
  37. * oranges_gauge.set({}, counts.oranges);
  38. * apples_gauge.set({}, counts.apples);
  39. * });
  40. *
  41. * @example <caption>Using counters</caption>
  42. * var metrics = new PrometheusMetrics();
  43. *
  44. * metrics.addCollector({
  45. * name: "things_made",
  46. * help: "count of things that we have made",
  47. * });
  48. *
  49. * function makeThing() {
  50. * metrics.incCounter("things_made");
  51. * return new Thing();
  52. * }
  53. *
  54. * @constructor
  55. */
  56. function PrometheusMetrics() {
  57. // Only attempt to load these dependencies if metrics are enabled
  58. var client = this._client = require("prom-client");
  59. // prom-client 9.0.0 has to be asked explicitly; previous versions would
  60. // do this by default
  61. if (client.collectDefaultMetrics) {
  62. client.collectDefaultMetrics();
  63. }
  64. this._collectors = []; // executed in order
  65. this._counters = {}; // counter metrics keyed by name
  66. this._timers = {}; // timer metrics (Histograms) keyed by name
  67. }
  68. /**
  69. * Registers some exported metrics that relate to operations of the embedded
  70. * matrix-js-sdk. In particular, a metric is added that counts the number of
  71. * calls to client API endpoints made by the client library.
  72. */
  73. PrometheusMetrics.prototype.registerMatrixSdkMetrics = function() {
  74. var callCounts = this.addCounter({
  75. name: "matrix_api_calls",
  76. help: "Count of the number of Matrix client API calls made",
  77. labels: ["method"],
  78. });
  79. /*
  80. * We'll now annotate a bunch of the methods in MatrixClient to keep counts
  81. * of every time they're called. This seems to be neater than trying to
  82. * intercept all HTTP requests and try to intuit what internal method was
  83. * invoked based on the HTTP URL.
  84. * It's kindof messy to do this because we have to maintain a list of
  85. * client SDK method names, but the only other alternative is to hook the
  86. * 'request' function and attempt to parse methods out by inspecting the
  87. * underlying client API HTTP URLs, and that is even messier. So this is
  88. * the lesser of two evils.
  89. */
  90. var matrixClientPrototype = require("matrix-js-sdk").MatrixClient.prototype;
  91. var CLIENT_METHODS = [
  92. "ban",
  93. "createAlias",
  94. "createRoom",
  95. "getProfileInfo",
  96. "getStateEvent",
  97. "invite",
  98. "joinRoom",
  99. "kick",
  100. "leave",
  101. "register",
  102. "roomState",
  103. "sendEvent",
  104. "sendReceipt",
  105. "sendStateEvent",
  106. "sendTyping",
  107. "setAvatarUrl",
  108. "setDisplayName",
  109. "setPowerLevel",
  110. "setPresence",
  111. "setProfileInfo",
  112. "unban",
  113. "uploadContent",
  114. ];
  115. CLIENT_METHODS.forEach(function(method) {
  116. callCounts.inc({method: method}, 0); // initialise the count to zero
  117. var orig = matrixClientPrototype[method];
  118. matrixClientPrototype[method] = function() {
  119. callCounts.inc({method: method});
  120. return orig.apply(this, arguments);
  121. }
  122. });
  123. };
  124. /**
  125. * Registers some exported metrics that expose counts of various kinds of
  126. * objects within the bridge.
  127. * @param {BridgeGaugesCallback} counterFunc A function that when invoked
  128. * returns the current counts of various items in the bridge.
  129. */
  130. PrometheusMetrics.prototype.registerBridgeGauges = function(counterFunc) {
  131. var matrixRoomsGauge = this.addGauge({
  132. name: "matrix_configured_rooms",
  133. help: "Current count of configured rooms by matrix room ID",
  134. });
  135. var remoteRoomsGauge = this.addGauge({
  136. name: "remote_configured_rooms",
  137. help: "Current count of configured rooms by remote room ID",
  138. });
  139. var matrixGhostsGauge = this.addGauge({
  140. name: "matrix_ghosts",
  141. help: "Current count of matrix-side ghost users",
  142. });
  143. var remoteGhostsGauge = this.addGauge({
  144. name: "remote_ghosts",
  145. help: "Current count of remote-side ghost users",
  146. });
  147. var matrixRoomsByAgeGauge = this.addGauge({
  148. name: "matrix_rooms_by_age",
  149. help: "Current count of matrix rooms partitioned by activity age",
  150. labels: ["age"],
  151. });
  152. var remoteRoomsByAgeGauge = this.addGauge({
  153. name: "remote_rooms_by_age",
  154. help: "Current count of remote rooms partitioned by activity age",
  155. labels: ["age"],
  156. });
  157. var matrixUsersByAgeGauge = this.addGauge({
  158. name: "matrix_users_by_age",
  159. help: "Current count of matrix users partitioned by activity age",
  160. labels: ["age"],
  161. });
  162. var remoteUsersByAgeGauge = this.addGauge({
  163. name: "remote_users_by_age",
  164. help: "Current count of remote users partitioned by activity age",
  165. labels: ["age"],
  166. });
  167. this.addCollector(function () {
  168. var counts = counterFunc();
  169. matrixRoomsGauge.set(counts.matrixRoomConfigs);
  170. remoteRoomsGauge.set(counts.remoteRoomConfigs);
  171. matrixGhostsGauge.set(counts.matrixGhosts);
  172. remoteGhostsGauge.set(counts.remoteGhosts);
  173. counts.matrixRoomsByAge.setGauge(matrixRoomsByAgeGauge);
  174. counts.remoteRoomsByAge.setGauge(remoteRoomsByAgeGauge);
  175. counts.matrixUsersByAge.setGauge(matrixUsersByAgeGauge);
  176. counts.remoteUsersByAge.setGauge(remoteUsersByAgeGauge);
  177. });
  178. };
  179. PrometheusMetrics.prototype.refresh = function() {
  180. this._collectors.forEach(function(f) { f(); });
  181. };
  182. /**
  183. * Adds a new collector function. These collector functions are run whenever
  184. * the /metrics page is about to be generated, allowing code to update values
  185. * of gauges.
  186. * @param {Function} func A new collector function.
  187. * This function is passed no arguments and is not expected to return anything.
  188. * It runs purely to have a side-effect on previously registered gauges.
  189. */
  190. PrometheusMetrics.prototype.addCollector = function(func) {
  191. this._collectors.push(func);
  192. };
  193. /**
  194. * Adds a new gauge metric.
  195. * @param {Object} opts Options
  196. * @param {string=} opts.namespace An optional toplevel namespace name for the
  197. * new metric. Default: <code>"bridge"</code>.
  198. * @param {string} opts.name The variable name for the new metric.
  199. * @param {string} opts.help Descriptive help text for the new metric.
  200. * @param {Array<string>=} opts.labels An optional list of string label names
  201. * @param {Function=} opts.refresh An optional function to invoke to generate a
  202. * new value for the gauge.
  203. * If a refresh function is provided, it is invoked with the gauge as its only
  204. * parameter. The function should call the <code>set()</code> method on this
  205. * gauge in order to provide a new value for it.
  206. * @return {Gauge} A gauge metric.
  207. */
  208. PrometheusMetrics.prototype.addGauge = function(opts) {
  209. var refresh = opts.refresh;
  210. var name = [opts.namespace || "bridge", opts.name].join("_");
  211. var gauge = new this._client.Gauge(name, opts.help, opts.labels || []);
  212. if (opts.refresh) {
  213. this._collectors.push(function() { refresh(gauge); });
  214. }
  215. return gauge;
  216. };
  217. /**
  218. * Adds a new counter metric
  219. * @param {Object} opts Options
  220. * @param {string} opts.namespace An optional toplevel namespace name for the
  221. * new metric. Default: <code>"bridge"</code>.
  222. * @param {string} opts.name The variable name for the new metric.
  223. * @param {string} opts.help Descriptive help text for the new metric.
  224. * Once created, the value of this metric can be incremented with the
  225. * <code>incCounter</code> method.
  226. * @param {Array<string>=} opts.labels An optional list of string label names
  227. * @return {Counter} A counter metric.
  228. */
  229. PrometheusMetrics.prototype.addCounter = function(opts) {
  230. var name = [opts.namespace || "bridge", opts.name].join("_");
  231. var counter = this._counters[opts.name] =
  232. new this._client.Counter(name, opts.help, opts.labels || []);
  233. return counter;
  234. };
  235. /**
  236. * Increments the value of a counter metric
  237. * @param{string} name The name the metric was previously registered as.
  238. * @param{Object} labels Optional object containing additional label values.
  239. */
  240. PrometheusMetrics.prototype.incCounter = function(name, labels) {
  241. if (!this._counters[name]) {
  242. throw new Error("Unrecognised counter metric name '" + name + "'");
  243. }
  244. this._counters[name].inc(labels);
  245. };
  246. /**
  247. * Adds a new timer metric, represented by a prometheus Histogram.
  248. * @param {Object} opts Options
  249. * @param {string} opts.namespace An optional toplevel namespace name for the
  250. * new metric. Default: <code>"bridge"</code>.
  251. * @param {string} opts.name The variable name for the new metric.
  252. * @param {string} opts.help Descriptive help text for the new metric.
  253. * @param {Array<string>=} opts.labels An optional list of string label names
  254. * @return {Histogram} A histogram metric.
  255. * Once created, the value of this metric can be incremented with the
  256. * <code>startTimer</code> method.
  257. */
  258. PrometheusMetrics.prototype.addTimer = function(opts) {
  259. var name = [opts.namespace || "bridge", opts.name].join("_");
  260. var timer = this._timers[opts.name] =
  261. new this._client.Histogram(name, opts.help, opts.labels || []);
  262. return timer;
  263. };
  264. /**
  265. * Begins a new timer observation for a timer metric.
  266. * @param{string} name The name the metric was previously registered as.
  267. * @param{Object} labels Optional object containing additional label values.
  268. * @return {function} A function to be called to end the timer and report the
  269. * observation.
  270. */
  271. PrometheusMetrics.prototype.startTimer = function(name, labels) {
  272. if (!this._timers[name]) {
  273. throw new Error("Unrecognised timer metric name '" + name + "'");
  274. }
  275. return this._timers[name].startTimer(labels);
  276. };
  277. /**
  278. * Registers the <code>/metrics</code> page generating function with the
  279. * containing Express app.
  280. * @param {Bridge} bridge The containing Bridge instance.
  281. */
  282. PrometheusMetrics.prototype.addAppServicePath = function(bridge) {
  283. var register = this._client.register;
  284. bridge.addAppServicePath({
  285. method: "GET",
  286. path: "/metrics",
  287. handler: function(req, res) {
  288. this.refresh();
  289. try {
  290. var exposition = register.metrics();
  291. res.set("Content-Type", "text/plain");
  292. res.send(exposition);
  293. }
  294. catch (e) {
  295. res.status(500);
  296. res.set("Content-Type", "text/plain");
  297. res.send(e.toString());
  298. }
  299. }.bind(this),
  300. });
  301. };
  302. /**
  303. * Invoked at metrics export time to count items in the bridge.
  304. * @callback BridgeGaugesCallback
  305. * @return {BridgeGaugesCounts} An object containing counts of items in the
  306. * bridge.
  307. */
  308. /**
  309. * @typedef BridgeGaugesCounts
  310. * @type {Object}
  311. * @param {number} matrixRoomConfigs The number of distinct matrix room IDs
  312. * known in the configuration.
  313. * @param {number} remoteRoomConfigs The number of distinct remote rooms known
  314. * in the configuration.
  315. * @param {number} matrixGhosts The number of matrix-side ghost users that
  316. * currently exist.
  317. * @param {number} remoteGhosts The number of remote-side ghost users that
  318. * currently exist.
  319. * @param {AgeCounters} matrixRoomsByAge The distribution of distinct matrix
  320. * room IDs by age of the most recently-seen message from them,
  321. * @param {AgeCounters} remoteRoomsByAge The distribution of distinct remote
  322. * rooms by age of the most recently-seen message from them.
  323. * @param {AgeCounters} matrixUsersByAge The distribution of distinct matrix
  324. * users by age of the most recently-seen message from them.
  325. * @param {AgeCounters} remoteUsersByAge The distribution of distinct remote
  326. * users by age of the most recently-seen message from them.
  327. */
  328. module.exports = PrometheusMetrics;