From 0b8689a169e211e9f1cd68b465b2f61eeba832dc Mon Sep 17 00:00:00 2001 From: Santiago Faci Date: Sun, 23 Mar 2025 15:18:04 +0100 Subject: [PATCH] =?UTF-8?q?A=C3=B1adida=20configuraci=C3=B3n=20para=20la?= =?UTF-8?q?=20monitorizaci=C3=B3n=20y=20coregido=20el=20fallo=20del=20id?= =?UTF-8?q?=20en=20la=20operaci=C3=B3n=20de=20registro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/node.js.yml | 4 +- .gitignore | 136 ++++++++++++++++++++++++++++ docker-compose.metrics.yaml | 32 +++++++ package-lock.json | 34 +++++++ package.json | 3 +- prometheus.yml | 11 +++ src/app.js | 44 +++++++++ src/config/metrics.js | 28 ++++++ src/service/cities.js | 6 +- src/test/integration/cities.test.js | 2 +- 10 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 docker-compose.metrics.yaml create mode 100644 prometheus.yml create mode 100644 src/config/metrics.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 2d4297c..d117d77 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -1,7 +1,7 @@ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs -name: Node.js CI +name: Unit and integration tests on: push: @@ -17,7 +17,6 @@ jobs: strategy: matrix: node-version: [18.x, 20.x, 22.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v4 @@ -28,5 +27,6 @@ jobs: cache: 'npm' - run: npm install - run: docker compose -f docker-compose.dev.yaml up -d + - run: sleep 3 - run: npm run unit-test - run: npm run integration-test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9ed5c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/docker-compose.metrics.yaml b/docker-compose.metrics.yaml new file mode 100644 index 0000000..0e20568 --- /dev/null +++ b/docker-compose.metrics.yaml @@ -0,0 +1,32 @@ +version: "3" +services: + prometheus: + image: prom/prometheus + ports: + - 9090:9090 + volumes: + - ./prometheus_data:/prometheus + - ./prometheus.yml:/etc/prometheus/prometheus.yml + command: + - "--config.file=/etc/prometheus/prometheus.yml" + extra_hosts: + - "host.docker.internal:192.168.1.120" + networks: + - localprom + + grafana: + image: grafana/grafana-enterprise + ports: + - 3000:3000 + volumes: + - ./grafana_data:/var/lib/grafana + extra_hosts: + - "host.docker.internal:192.168.1.120" + networks: + - localprom + depends_on: + - prometheus + +networks: + localprom: + driver: bridge \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ae2cf93..aeb124e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "js-yaml": "^4.1.0", "knex": "^3.1.0", "mysql": "^2.18.1", + "prom-client": "^15.1.3", "yargs": "17.7.2" }, "devDependencies": { @@ -1224,6 +1225,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1702,6 +1711,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -5153,6 +5167,18 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5702,6 +5728,14 @@ "node": ">=8.0.0" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index 3d7b5e5..69c742a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "mysql": "^2.18.1", "knex": "^3.1.0", "js-yaml": "^4.1.0", - "yargs": "17.7.2" + "yargs": "17.7.2", + "prom-client": "^15.1.3" }, "devDependencies": { diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..65c3796 --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,11 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: "prometheus_service" + static_configs: + - targets: ["host.docker.internal:9090"] + + - job_name: "nodejs_service" + static_configs: + - targets: ["host.docker.internal:8080"] diff --git a/src/app.js b/src/app.js index a7dcdbc..1fd11aa 100644 --- a/src/app.js +++ b/src/app.js @@ -2,10 +2,54 @@ const express = require('express'); const cities = require('./route/cities'); const { config } = require('./config/configuration'); +const promClient = require('prom-client'); +const { httpRequestDurationSeconds, httpRequestsTotal, inFlightRequests } = require('./config/metrics'); // Use direct imports const app = express(); app.use(express.json()); +// Recoge métricas por defecto +promClient.collectDefaultMetrics(); +// Recoger las métricas configuradas en config/metrics +app.use((req, res, next) => { + // Anota la request en vuelo + inFlightRequests.inc(); + + // Inicia un timer para comenzar a calcular la duración de la request + const end = httpRequestDurationSeconds.startTimer(); + + const method = req.method; + const path = req.route ? req.route.path : req.path; + + // Cada vez que una request termina, se recogen las métricas configuradas + res.on('finish', () => { + const statusCode = res.statusCode; + + // Captura la duración de la petición + end({ method, path, code: statusCode }); + // Incrementa el contador de peticiones HTTP + httpRequestsTotal.inc({ + code: statusCode, + method: method.toLowerCase(), + path: path + }); + + // Decrementa el contador de peticiones en vuelo + inFlightRequests.dec(); + }); + + next(); +}); + +app.get('/metrics', async(req, res) => { + try { + res.set('Content-Type', promClient.register.contentType); + res.end(await promClient.register.metrics()); + } catch (error) { + res.status(500).end(error); + } +}); + app.use('/', cities); app.listen(config.service.port, () => { diff --git a/src/config/metrics.js b/src/config/metrics.js new file mode 100644 index 0000000..b0d8129 --- /dev/null +++ b/src/config/metrics.js @@ -0,0 +1,28 @@ +'use strict'; +const promClient = require('prom-client'); + +// Crea un histograma para registrar la duración de las peticiones HTTP en segundos +const httpRequestDurationSeconds = new promClient.Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['method', 'path', 'code'], +}); + +// Crea un contador de peticiones HTTP totales +const httpRequestsTotal = new promClient.Counter({ + name: 'http_requests_total', + help: 'How many HTTP requests processed, partitioned by status code, method, and HTTP path', + labelNames: ['method', 'path', 'code'], +}); + +// Crea una medición de las peticiones HTTP en vuelo +const inFlightRequests = new promClient.Gauge({ + name: 'http_requests_in_flight', + help: 'Current number of in-flight HTTP requests', +}); + +module.exports = { + httpRequestDurationSeconds, + httpRequestsTotal, + inFlightRequests, +}; diff --git a/src/service/cities.js b/src/service/cities.js index 93d9420..8e2d999 100644 --- a/src/service/cities.js +++ b/src/service/cities.js @@ -36,9 +36,9 @@ const cityExists = (async (name) => { const registerCity = (async (name, population, altitude, foundationDate, area) => { const age = getYearsFromNow(new Date(foundationDate)); const density = getDensity(population, area); + let cityId; const returning = await db('cities') - .returning("id") // TODO Revisar cómo funciona para MariaDB .insert({ name: name, population: population, @@ -47,10 +47,12 @@ const registerCity = (async (name, population, altitude, foundationDate, area) = age: age, area: area, density: density + }).then( async (ids) => { + cityId = ids[0]; }); const result = { - id: returning[0].id, + id: cityId, age: age, density: density }; diff --git a/src/test/integration/cities.test.js b/src/test/integration/cities.test.js index 7a1e892..dc46b9e 100644 --- a/src/test/integration/cities.test.js +++ b/src/test/integration/cities.test.js @@ -43,7 +43,7 @@ describe('cities', () => { }) .end((error, response) => { response.should.have.status(201); - //expect(response.body).to.have.property('id'); + expect(response.body).to.have.property('id'); expect(response.body).to.have.property('name'); expect(response.body).to.have.property('population'); expect(response.body).to.have.property('altitude');