├── README.md ├── autocannon ├── .env ├── .gitignore ├── main.js ├── package-lock.json ├── package.json ├── results │ └── readme.txt └── test.jpg ├── backend ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode │ └── launch.json ├── Dockerfile ├── README.md ├── nest-cli.json ├── nodeshell.sh ├── package-lock.json ├── package.json ├── prod.sh ├── rundev.sh ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── cron.service.ts │ ├── imageMetadata.shema.ts │ ├── main.ts │ ├── ot-instrumentation.ts │ ├── prom-metrics.ts │ └── uploadedImage.entity.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json ├── docker-compose.metrics.yml ├── docker-compose.metrics_logs.yml ├── docker-compose.metrics_logs_jaeger.yml ├── docker-compose.metrics_logs_tempo.yml ├── docker-compose.nomon.yml ├── logstash.conf ├── prometheus.yml └── tempo-local.yaml /README.md: -------------------------------------------------------------------------------- 1 | # Observable backend demo project 2 | Project for [this](https://habr.com/ru/company/macloud/blog/556518/) post at habr.com 3 | 4 | Shows how to 5 | * Collect Metrics (Prometheus) 6 | * Collect logs 7 | * Collect traces 8 | 9 | for Nest.js project. 10 | 11 | Here is simple Nest.js service, which recieves images, parse and save its metadata to database (Mongo and Posges). 12 | 13 | ## How to use 14 | 15 | Run project without any monitoring: 16 | ``` 17 | docker-compose -f docker-compose.nomon.yml up -d 18 | ``` 19 | 20 | Run project with metrics (Prometheus and Grafana): 21 | ``` 22 | docker-compose -f docker-compose.metrics.yml up -d 23 | ``` 24 | 25 | Run project with metrics and collecting logs (Prometheus, Loki and Grafana): 26 | ``` 27 | docker-compose -f docker-compose.metrics_logs.yml up -d 28 | ``` 29 | 30 | Run project with full monitoring (tempo traces) (Prometheus, Loki, Tempo and Grafana): 31 | ``` 32 | docker-compose -f docker-compose.metrics_logs_tempo.yml up -d 33 | ``` 34 | 35 | Run project with full monitoring (jaeger traces) (Prometheus, Loki, Jaeger and Grafana): 36 | ``` 37 | docker-compose -f docker-compose.metrics_logs_jaeger.yml up -d 38 | ``` 39 | 40 | 41 | ## Warning! 42 | 43 | Its just demo project with most security options disabled. Do not use its as is for production purposes. 44 | -------------------------------------------------------------------------------- /autocannon/.env: -------------------------------------------------------------------------------- 1 | TARGETHOST=localhost -------------------------------------------------------------------------------- /autocannon/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /results/*.json 3 | /node_modules 4 | 5 | #dotenv 6 | .env 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json -------------------------------------------------------------------------------- /autocannon/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | require('dotenv').config() 3 | const autocannon = require("autocannon"); 4 | const { join, basename } = require("path"); 5 | const FormData = require("form-data"); 6 | const { readFileSync, writeFileSync } = require("fs"); 7 | 8 | const hostname = process.env.TARGETHOST; 9 | console.log(`Target host "${hostname}"`) 10 | function getForm() { 11 | const form = new FormData(); 12 | const path = join(process.cwd(), "test.jpg"); 13 | const buffer = readFileSync(path); 14 | form.append("file", buffer, { 15 | filename: basename(path), 16 | }); 17 | return form; 18 | } 19 | const uploadResponses = []; 20 | const instance = autocannon( 21 | { 22 | url: `http://${hostname}:3000/upload`, 23 | connections: 70, 24 | pipelining: 1, 25 | duration: 120, 26 | timeout: 100, 27 | requests: [ 28 | { 29 | method: "POST", 30 | setupRequest(req) { 31 | const form = getForm(); 32 | req.headers = form.getHeaders(); 33 | req.body = form.getBuffer(); 34 | return req; 35 | }, 36 | onResponse(status, body, context) { 37 | if (status === 201) { 38 | uploadResponses.push(JSON.parse(body)); 39 | } 40 | }, 41 | }, 42 | { 43 | method: "GET", 44 | setupRequest(req) { 45 | const rndId = Math.floor(Math.random() * uploadResponses.length); 46 | let rndItem = uploadResponses[rndId]; 47 | if (!rndItem) rndItem = uploadResponses[0]; 48 | const imgId = rndItem.id; 49 | req.href = `http://${hostname}:3000/info/${imgId}`; 50 | req.path = `/info/${imgId}`; 51 | req.pathname = `/info/${imgId}`; 52 | return req; 53 | }, 54 | }, 55 | { 56 | method: "GET", 57 | setupRequest(req) { 58 | const rndId = Math.floor(Math.random() * uploadResponses.length); 59 | const rndItem = uploadResponses[rndId]; 60 | const filename = rndItem.filename; 61 | req.href = `http://${hostname}:3000/images/${filename}`; 62 | req.path = `/images/${filename}`; 63 | req.pathname = `/images/${filename}`; 64 | return req; 65 | }, 66 | }, 67 | ], 68 | }, 69 | (err, result) => { 70 | writeFileSync( 71 | join(process.cwd(), "results", Date.now() + ".json"), 72 | JSON.stringify(result, undefined, 2), 73 | { encoding: "utf8" } 74 | ); 75 | const exclude = new Set([ 76 | "title", 77 | "url", 78 | "socketPath", 79 | "workers", 80 | "statusCodeStats", 81 | "latency", 82 | "requests", 83 | "throughput", 84 | "start", 85 | "finish", 86 | ]); 87 | const displayRes = {}; 88 | for (const key in result) { 89 | if (Object.hasOwnProperty.call(result, key)) { 90 | const element = result[key]; 91 | if (!exclude.has(key)) { 92 | displayRes[key] = element; 93 | } 94 | } 95 | } 96 | console.table([displayRes]); 97 | console.table([result.statusCodeStats]); 98 | const cleanp = (obj, stat) => { 99 | obj.Stat = stat; 100 | return obj; 101 | }; 102 | console.table( 103 | [ 104 | cleanp(result.latency, "Latency, ms"), 105 | cleanp(result.requests, "Req/Sec"), 106 | cleanp(result.throughput, "Bytes/Sec"), 107 | ], 108 | [ 109 | "Stat", 110 | "average", 111 | "stddev", 112 | "min", 113 | "max", 114 | "p10", 115 | "p75", 116 | "p90", 117 | "p99", 118 | "totalCount", 119 | "total", 120 | "sent", 121 | ] 122 | ); 123 | } 124 | ); 125 | -------------------------------------------------------------------------------- /autocannon/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocannon", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.0", 9 | "license": "ISC", 10 | "dependencies": { 11 | "autocannon": "^7.2.0", 12 | "dotenv": "^9.0.0", 13 | "form-data": "^4.0.0" 14 | } 15 | }, 16 | "node_modules/@assemblyscript/loader": { 17 | "version": "0.10.1", 18 | "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", 19 | "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==" 20 | }, 21 | "node_modules/ansi-regex": { 22 | "version": "5.0.0", 23 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 24 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", 25 | "engines": { 26 | "node": ">=8" 27 | } 28 | }, 29 | "node_modules/ansi-styles": { 30 | "version": "4.3.0", 31 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 32 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 33 | "dependencies": { 34 | "color-convert": "^2.0.1" 35 | }, 36 | "engines": { 37 | "node": ">=8" 38 | }, 39 | "funding": { 40 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 41 | } 42 | }, 43 | "node_modules/asynckit": { 44 | "version": "0.4.0", 45 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 46 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 47 | }, 48 | "node_modules/autocannon": { 49 | "version": "7.2.0", 50 | "resolved": "https://registry.npmjs.org/autocannon/-/autocannon-7.2.0.tgz", 51 | "integrity": "sha512-yMXnHdGcHiz+dk/VHsgIXHBn7qK5hIaRS5yKbLzGTvn3REbEJA37xFI7+KkCEAtSOGnMLhd6VTZ69g9GReCClQ==", 52 | "dependencies": { 53 | "chalk": "^4.1.0", 54 | "char-spinner": "^1.0.1", 55 | "cli-table3": "^0.6.0", 56 | "clone": "^2.1.2", 57 | "color-support": "^1.1.1", 58 | "cross-argv": "^1.0.0", 59 | "form-data": "^4.0.0", 60 | "has-async-hooks": "^1.0.0", 61 | "hdr-histogram-js": "^2.0.1", 62 | "hdr-histogram-percentiles-obj": "^3.0.0", 63 | "http-parser-js": "^0.5.2", 64 | "hyperid": "^2.0.3", 65 | "manage-path": "^2.0.0", 66 | "on-net-listen": "^1.1.1", 67 | "pretty-bytes": "^5.4.1", 68 | "progress": "^2.0.3", 69 | "reinterval": "^1.1.0", 70 | "retimer": "^3.0.0", 71 | "semver": "^7.3.2", 72 | "subarg": "^1.0.0", 73 | "timestring": "^6.0.0" 74 | }, 75 | "bin": { 76 | "autocannon": "autocannon.js" 77 | } 78 | }, 79 | "node_modules/base64-js": { 80 | "version": "1.5.1", 81 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 82 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 83 | "funding": [ 84 | { 85 | "type": "github", 86 | "url": "https://github.com/sponsors/feross" 87 | }, 88 | { 89 | "type": "patreon", 90 | "url": "https://www.patreon.com/feross" 91 | }, 92 | { 93 | "type": "consulting", 94 | "url": "https://feross.org/support" 95 | } 96 | ] 97 | }, 98 | "node_modules/chalk": { 99 | "version": "4.1.1", 100 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", 101 | "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", 102 | "dependencies": { 103 | "ansi-styles": "^4.1.0", 104 | "supports-color": "^7.1.0" 105 | }, 106 | "engines": { 107 | "node": ">=10" 108 | }, 109 | "funding": { 110 | "url": "https://github.com/chalk/chalk?sponsor=1" 111 | } 112 | }, 113 | "node_modules/char-spinner": { 114 | "version": "1.0.1", 115 | "resolved": "https://registry.npmjs.org/char-spinner/-/char-spinner-1.0.1.tgz", 116 | "integrity": "sha1-5upnvSR+EHESmDt6sEee02KAAIE=" 117 | }, 118 | "node_modules/cli-table3": { 119 | "version": "0.6.0", 120 | "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", 121 | "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", 122 | "dependencies": { 123 | "object-assign": "^4.1.0", 124 | "string-width": "^4.2.0" 125 | }, 126 | "engines": { 127 | "node": "10.* || >= 12.*" 128 | }, 129 | "optionalDependencies": { 130 | "colors": "^1.1.2" 131 | } 132 | }, 133 | "node_modules/clone": { 134 | "version": "2.1.2", 135 | "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", 136 | "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", 137 | "engines": { 138 | "node": ">=0.8" 139 | } 140 | }, 141 | "node_modules/color-convert": { 142 | "version": "2.0.1", 143 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 144 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 145 | "dependencies": { 146 | "color-name": "~1.1.4" 147 | }, 148 | "engines": { 149 | "node": ">=7.0.0" 150 | } 151 | }, 152 | "node_modules/color-name": { 153 | "version": "1.1.4", 154 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 155 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 156 | }, 157 | "node_modules/color-support": { 158 | "version": "1.1.3", 159 | "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", 160 | "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", 161 | "bin": { 162 | "color-support": "bin.js" 163 | } 164 | }, 165 | "node_modules/colors": { 166 | "version": "1.4.0", 167 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 168 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", 169 | "optional": true, 170 | "engines": { 171 | "node": ">=0.1.90" 172 | } 173 | }, 174 | "node_modules/combined-stream": { 175 | "version": "1.0.8", 176 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 177 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 178 | "dependencies": { 179 | "delayed-stream": "~1.0.0" 180 | }, 181 | "engines": { 182 | "node": ">= 0.8" 183 | } 184 | }, 185 | "node_modules/cross-argv": { 186 | "version": "1.0.0", 187 | "resolved": "https://registry.npmjs.org/cross-argv/-/cross-argv-1.0.0.tgz", 188 | "integrity": "sha512-uAVe/bgNHlPdP1VE4Sk08u9pAJ7o1x/tVQtX77T5zlhYhuwOWtVkPBEtHdvF5cq48VzeCG5i1zN4dQc8pwLYrw==" 189 | }, 190 | "node_modules/delayed-stream": { 191 | "version": "1.0.0", 192 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 193 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 194 | "engines": { 195 | "node": ">=0.4.0" 196 | } 197 | }, 198 | "node_modules/dotenv": { 199 | "version": "9.0.0", 200 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.0.tgz", 201 | "integrity": "sha512-yy3x9XjojW8ROTBePD25AcMoHqGHsvHmtfw8QWlpEXyMMXXPj6brUA464AptUvHuTPRmNz6Sd3ZLNLeJl6dHJA==", 202 | "engines": { 203 | "node": ">=10" 204 | } 205 | }, 206 | "node_modules/emoji-regex": { 207 | "version": "8.0.0", 208 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 209 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 210 | }, 211 | "node_modules/form-data": { 212 | "version": "4.0.0", 213 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 214 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 215 | "dependencies": { 216 | "asynckit": "^0.4.0", 217 | "combined-stream": "^1.0.8", 218 | "mime-types": "^2.1.12" 219 | }, 220 | "engines": { 221 | "node": ">= 6" 222 | } 223 | }, 224 | "node_modules/has-async-hooks": { 225 | "version": "1.0.0", 226 | "resolved": "https://registry.npmjs.org/has-async-hooks/-/has-async-hooks-1.0.0.tgz", 227 | "integrity": "sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw==" 228 | }, 229 | "node_modules/has-flag": { 230 | "version": "4.0.0", 231 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 232 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 233 | "engines": { 234 | "node": ">=8" 235 | } 236 | }, 237 | "node_modules/hdr-histogram-js": { 238 | "version": "2.0.1", 239 | "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.1.tgz", 240 | "integrity": "sha512-uPZxl1dAFnjUFHWLZmt93vUUvtHeaBay9nVNHu38SdOjMSF/4KqJUqa1Seuj08ptU1rEb6AHvB41X8n/zFZ74Q==", 241 | "dependencies": { 242 | "@assemblyscript/loader": "^0.10.1", 243 | "base64-js": "^1.2.0", 244 | "pako": "^1.0.3" 245 | } 246 | }, 247 | "node_modules/hdr-histogram-percentiles-obj": { 248 | "version": "3.0.0", 249 | "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", 250 | "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==" 251 | }, 252 | "node_modules/http-parser-js": { 253 | "version": "0.5.3", 254 | "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", 255 | "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==" 256 | }, 257 | "node_modules/hyperid": { 258 | "version": "2.1.0", 259 | "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-2.1.0.tgz", 260 | "integrity": "sha512-cSakhxbUsaIuqjfvvcUuvl/Fl342J65xgLLYrYxSSr9qmJ/EJK+S8crS6mIlQd/a7i+Pe4D0MgSrtZPLze+aCw==", 261 | "dependencies": { 262 | "uuid": "^3.4.0", 263 | "uuid-parse": "^1.1.0" 264 | } 265 | }, 266 | "node_modules/is-fullwidth-code-point": { 267 | "version": "3.0.0", 268 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 269 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 270 | "engines": { 271 | "node": ">=8" 272 | } 273 | }, 274 | "node_modules/lru-cache": { 275 | "version": "6.0.0", 276 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 277 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 278 | "dependencies": { 279 | "yallist": "^4.0.0" 280 | }, 281 | "engines": { 282 | "node": ">=10" 283 | } 284 | }, 285 | "node_modules/manage-path": { 286 | "version": "2.0.0", 287 | "resolved": "https://registry.npmjs.org/manage-path/-/manage-path-2.0.0.tgz", 288 | "integrity": "sha1-9M+EV7km7u4qg7FzUBQUvHbrlZc=" 289 | }, 290 | "node_modules/mime-db": { 291 | "version": "1.47.0", 292 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", 293 | "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", 294 | "engines": { 295 | "node": ">= 0.6" 296 | } 297 | }, 298 | "node_modules/mime-types": { 299 | "version": "2.1.30", 300 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", 301 | "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", 302 | "dependencies": { 303 | "mime-db": "1.47.0" 304 | }, 305 | "engines": { 306 | "node": ">= 0.6" 307 | } 308 | }, 309 | "node_modules/minimist": { 310 | "version": "1.2.5", 311 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 312 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 313 | }, 314 | "node_modules/object-assign": { 315 | "version": "4.1.1", 316 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 317 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 318 | "engines": { 319 | "node": ">=0.10.0" 320 | } 321 | }, 322 | "node_modules/on-net-listen": { 323 | "version": "1.1.2", 324 | "resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz", 325 | "integrity": "sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg==", 326 | "engines": { 327 | "node": ">=9.4.0 || ^8.9.4" 328 | } 329 | }, 330 | "node_modules/pako": { 331 | "version": "1.0.11", 332 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 333 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 334 | }, 335 | "node_modules/pretty-bytes": { 336 | "version": "5.6.0", 337 | "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", 338 | "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", 339 | "engines": { 340 | "node": ">=6" 341 | }, 342 | "funding": { 343 | "url": "https://github.com/sponsors/sindresorhus" 344 | } 345 | }, 346 | "node_modules/progress": { 347 | "version": "2.0.3", 348 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 349 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 350 | "engines": { 351 | "node": ">=0.4.0" 352 | } 353 | }, 354 | "node_modules/reinterval": { 355 | "version": "1.1.0", 356 | "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", 357 | "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" 358 | }, 359 | "node_modules/retimer": { 360 | "version": "3.0.0", 361 | "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz", 362 | "integrity": "sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==" 363 | }, 364 | "node_modules/semver": { 365 | "version": "7.3.5", 366 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 367 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 368 | "dependencies": { 369 | "lru-cache": "^6.0.0" 370 | }, 371 | "bin": { 372 | "semver": "bin/semver.js" 373 | }, 374 | "engines": { 375 | "node": ">=10" 376 | } 377 | }, 378 | "node_modules/string-width": { 379 | "version": "4.2.2", 380 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", 381 | "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", 382 | "dependencies": { 383 | "emoji-regex": "^8.0.0", 384 | "is-fullwidth-code-point": "^3.0.0", 385 | "strip-ansi": "^6.0.0" 386 | }, 387 | "engines": { 388 | "node": ">=8" 389 | } 390 | }, 391 | "node_modules/strip-ansi": { 392 | "version": "6.0.0", 393 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 394 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 395 | "dependencies": { 396 | "ansi-regex": "^5.0.0" 397 | }, 398 | "engines": { 399 | "node": ">=8" 400 | } 401 | }, 402 | "node_modules/subarg": { 403 | "version": "1.0.0", 404 | "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", 405 | "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", 406 | "dependencies": { 407 | "minimist": "^1.1.0" 408 | } 409 | }, 410 | "node_modules/supports-color": { 411 | "version": "7.2.0", 412 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 413 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 414 | "dependencies": { 415 | "has-flag": "^4.0.0" 416 | }, 417 | "engines": { 418 | "node": ">=8" 419 | } 420 | }, 421 | "node_modules/timestring": { 422 | "version": "6.0.0", 423 | "resolved": "https://registry.npmjs.org/timestring/-/timestring-6.0.0.tgz", 424 | "integrity": "sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==", 425 | "engines": { 426 | "node": ">=8" 427 | } 428 | }, 429 | "node_modules/uuid": { 430 | "version": "3.4.0", 431 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 432 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 433 | "bin": { 434 | "uuid": "bin/uuid" 435 | } 436 | }, 437 | "node_modules/uuid-parse": { 438 | "version": "1.1.0", 439 | "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", 440 | "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==" 441 | }, 442 | "node_modules/yallist": { 443 | "version": "4.0.0", 444 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 445 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 446 | } 447 | }, 448 | "dependencies": { 449 | "@assemblyscript/loader": { 450 | "version": "0.10.1", 451 | "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", 452 | "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==" 453 | }, 454 | "ansi-regex": { 455 | "version": "5.0.0", 456 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 457 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 458 | }, 459 | "ansi-styles": { 460 | "version": "4.3.0", 461 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 462 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 463 | "requires": { 464 | "color-convert": "^2.0.1" 465 | } 466 | }, 467 | "asynckit": { 468 | "version": "0.4.0", 469 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 470 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 471 | }, 472 | "autocannon": { 473 | "version": "7.2.0", 474 | "resolved": "https://registry.npmjs.org/autocannon/-/autocannon-7.2.0.tgz", 475 | "integrity": "sha512-yMXnHdGcHiz+dk/VHsgIXHBn7qK5hIaRS5yKbLzGTvn3REbEJA37xFI7+KkCEAtSOGnMLhd6VTZ69g9GReCClQ==", 476 | "requires": { 477 | "chalk": "^4.1.0", 478 | "char-spinner": "^1.0.1", 479 | "cli-table3": "^0.6.0", 480 | "clone": "^2.1.2", 481 | "color-support": "^1.1.1", 482 | "cross-argv": "^1.0.0", 483 | "form-data": "^4.0.0", 484 | "has-async-hooks": "^1.0.0", 485 | "hdr-histogram-js": "^2.0.1", 486 | "hdr-histogram-percentiles-obj": "^3.0.0", 487 | "http-parser-js": "^0.5.2", 488 | "hyperid": "^2.0.3", 489 | "manage-path": "^2.0.0", 490 | "on-net-listen": "^1.1.1", 491 | "pretty-bytes": "^5.4.1", 492 | "progress": "^2.0.3", 493 | "reinterval": "^1.1.0", 494 | "retimer": "^3.0.0", 495 | "semver": "^7.3.2", 496 | "subarg": "^1.0.0", 497 | "timestring": "^6.0.0" 498 | } 499 | }, 500 | "base64-js": { 501 | "version": "1.5.1", 502 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 503 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 504 | }, 505 | "chalk": { 506 | "version": "4.1.1", 507 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", 508 | "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", 509 | "requires": { 510 | "ansi-styles": "^4.1.0", 511 | "supports-color": "^7.1.0" 512 | } 513 | }, 514 | "char-spinner": { 515 | "version": "1.0.1", 516 | "resolved": "https://registry.npmjs.org/char-spinner/-/char-spinner-1.0.1.tgz", 517 | "integrity": "sha1-5upnvSR+EHESmDt6sEee02KAAIE=" 518 | }, 519 | "cli-table3": { 520 | "version": "0.6.0", 521 | "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", 522 | "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", 523 | "requires": { 524 | "colors": "^1.1.2", 525 | "object-assign": "^4.1.0", 526 | "string-width": "^4.2.0" 527 | } 528 | }, 529 | "clone": { 530 | "version": "2.1.2", 531 | "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", 532 | "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" 533 | }, 534 | "color-convert": { 535 | "version": "2.0.1", 536 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 537 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 538 | "requires": { 539 | "color-name": "~1.1.4" 540 | } 541 | }, 542 | "color-name": { 543 | "version": "1.1.4", 544 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 545 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 546 | }, 547 | "color-support": { 548 | "version": "1.1.3", 549 | "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", 550 | "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" 551 | }, 552 | "colors": { 553 | "version": "1.4.0", 554 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 555 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", 556 | "optional": true 557 | }, 558 | "combined-stream": { 559 | "version": "1.0.8", 560 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 561 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 562 | "requires": { 563 | "delayed-stream": "~1.0.0" 564 | } 565 | }, 566 | "cross-argv": { 567 | "version": "1.0.0", 568 | "resolved": "https://registry.npmjs.org/cross-argv/-/cross-argv-1.0.0.tgz", 569 | "integrity": "sha512-uAVe/bgNHlPdP1VE4Sk08u9pAJ7o1x/tVQtX77T5zlhYhuwOWtVkPBEtHdvF5cq48VzeCG5i1zN4dQc8pwLYrw==" 570 | }, 571 | "delayed-stream": { 572 | "version": "1.0.0", 573 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 574 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 575 | }, 576 | "dotenv": { 577 | "version": "9.0.0", 578 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.0.tgz", 579 | "integrity": "sha512-yy3x9XjojW8ROTBePD25AcMoHqGHsvHmtfw8QWlpEXyMMXXPj6brUA464AptUvHuTPRmNz6Sd3ZLNLeJl6dHJA==" 580 | }, 581 | "emoji-regex": { 582 | "version": "8.0.0", 583 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 584 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 585 | }, 586 | "form-data": { 587 | "version": "4.0.0", 588 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 589 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 590 | "requires": { 591 | "asynckit": "^0.4.0", 592 | "combined-stream": "^1.0.8", 593 | "mime-types": "^2.1.12" 594 | } 595 | }, 596 | "has-async-hooks": { 597 | "version": "1.0.0", 598 | "resolved": "https://registry.npmjs.org/has-async-hooks/-/has-async-hooks-1.0.0.tgz", 599 | "integrity": "sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw==" 600 | }, 601 | "has-flag": { 602 | "version": "4.0.0", 603 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 604 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 605 | }, 606 | "hdr-histogram-js": { 607 | "version": "2.0.1", 608 | "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.1.tgz", 609 | "integrity": "sha512-uPZxl1dAFnjUFHWLZmt93vUUvtHeaBay9nVNHu38SdOjMSF/4KqJUqa1Seuj08ptU1rEb6AHvB41X8n/zFZ74Q==", 610 | "requires": { 611 | "@assemblyscript/loader": "^0.10.1", 612 | "base64-js": "^1.2.0", 613 | "pako": "^1.0.3" 614 | } 615 | }, 616 | "hdr-histogram-percentiles-obj": { 617 | "version": "3.0.0", 618 | "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", 619 | "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==" 620 | }, 621 | "http-parser-js": { 622 | "version": "0.5.3", 623 | "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", 624 | "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==" 625 | }, 626 | "hyperid": { 627 | "version": "2.1.0", 628 | "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-2.1.0.tgz", 629 | "integrity": "sha512-cSakhxbUsaIuqjfvvcUuvl/Fl342J65xgLLYrYxSSr9qmJ/EJK+S8crS6mIlQd/a7i+Pe4D0MgSrtZPLze+aCw==", 630 | "requires": { 631 | "uuid": "^3.4.0", 632 | "uuid-parse": "^1.1.0" 633 | } 634 | }, 635 | "is-fullwidth-code-point": { 636 | "version": "3.0.0", 637 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 638 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 639 | }, 640 | "lru-cache": { 641 | "version": "6.0.0", 642 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 643 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 644 | "requires": { 645 | "yallist": "^4.0.0" 646 | } 647 | }, 648 | "manage-path": { 649 | "version": "2.0.0", 650 | "resolved": "https://registry.npmjs.org/manage-path/-/manage-path-2.0.0.tgz", 651 | "integrity": "sha1-9M+EV7km7u4qg7FzUBQUvHbrlZc=" 652 | }, 653 | "mime-db": { 654 | "version": "1.47.0", 655 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", 656 | "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" 657 | }, 658 | "mime-types": { 659 | "version": "2.1.30", 660 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", 661 | "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", 662 | "requires": { 663 | "mime-db": "1.47.0" 664 | } 665 | }, 666 | "minimist": { 667 | "version": "1.2.5", 668 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 669 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 670 | }, 671 | "object-assign": { 672 | "version": "4.1.1", 673 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 674 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 675 | }, 676 | "on-net-listen": { 677 | "version": "1.1.2", 678 | "resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz", 679 | "integrity": "sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg==" 680 | }, 681 | "pako": { 682 | "version": "1.0.11", 683 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 684 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 685 | }, 686 | "pretty-bytes": { 687 | "version": "5.6.0", 688 | "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", 689 | "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" 690 | }, 691 | "progress": { 692 | "version": "2.0.3", 693 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 694 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" 695 | }, 696 | "reinterval": { 697 | "version": "1.1.0", 698 | "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", 699 | "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" 700 | }, 701 | "retimer": { 702 | "version": "3.0.0", 703 | "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz", 704 | "integrity": "sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==" 705 | }, 706 | "semver": { 707 | "version": "7.3.5", 708 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 709 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 710 | "requires": { 711 | "lru-cache": "^6.0.0" 712 | } 713 | }, 714 | "string-width": { 715 | "version": "4.2.2", 716 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", 717 | "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", 718 | "requires": { 719 | "emoji-regex": "^8.0.0", 720 | "is-fullwidth-code-point": "^3.0.0", 721 | "strip-ansi": "^6.0.0" 722 | } 723 | }, 724 | "strip-ansi": { 725 | "version": "6.0.0", 726 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 727 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 728 | "requires": { 729 | "ansi-regex": "^5.0.0" 730 | } 731 | }, 732 | "subarg": { 733 | "version": "1.0.0", 734 | "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", 735 | "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", 736 | "requires": { 737 | "minimist": "^1.1.0" 738 | } 739 | }, 740 | "supports-color": { 741 | "version": "7.2.0", 742 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 743 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 744 | "requires": { 745 | "has-flag": "^4.0.0" 746 | } 747 | }, 748 | "timestring": { 749 | "version": "6.0.0", 750 | "resolved": "https://registry.npmjs.org/timestring/-/timestring-6.0.0.tgz", 751 | "integrity": "sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==" 752 | }, 753 | "uuid": { 754 | "version": "3.4.0", 755 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 756 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 757 | }, 758 | "uuid-parse": { 759 | "version": "1.1.0", 760 | "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", 761 | "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==" 762 | }, 763 | "yallist": { 764 | "version": "4.0.0", 765 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 766 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 767 | } 768 | } 769 | } 770 | -------------------------------------------------------------------------------- /autocannon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocannon", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Mikhail Sokolov", 10 | "license": "ISC", 11 | "dependencies": { 12 | "autocannon": "^7.2.0", 13 | "dotenv": "^9.0.0", 14 | "form-data": "^4.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /autocannon/results/readme.txt: -------------------------------------------------------------------------------- 1 | This is folder for results -------------------------------------------------------------------------------- /autocannon/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debagger/observable-backend/922269d3048e3d6172d5dd2555691722ab0ddeda/autocannon/test.jpg -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /backend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "address": "127.0.0.1", 9 | "localRoot": "${workspaceFolder}", 10 | "name": "Attach to Remote", 11 | "port": 9229, 12 | "remoteRoot": "/home/backend", 13 | "request": "attach", 14 | "skipFiles": [ 15 | "/**" 16 | ], 17 | "type": "node" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | WORKDIR /home/backend 3 | COPY ./src ./src/ 4 | COPY ["package.json", "nest-cli.json", "tsconfig.build.json", "tsconfig.json", "./"] 5 | RUN npm i 6 | RUN npm run build 7 | CMD dist/main -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /backend/nodeshell.sh: -------------------------------------------------------------------------------- 1 | docker run -v `pwd`:/home/project -w /home/project -it --rm node:lts bash -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.6.15", 25 | "@nestjs/core": "^7.6.15", 26 | "@nestjs/mongoose": "^7.2.4", 27 | "@nestjs/platform-express": "^7.6.15", 28 | "@nestjs/schedule": "^0.4.3", 29 | "@nestjs/typeorm": "^7.1.5", 30 | "@opentelemetry/core": "^0.19.0", 31 | "@opentelemetry/exporter-jaeger": "^0.19.0", 32 | "@opentelemetry/instrumentation-express": "^0.16.0", 33 | "@opentelemetry/instrumentation-http": "^0.19.0", 34 | "@opentelemetry/node": "^0.19.0", 35 | "@opentelemetry/tracing": "^0.19.0", 36 | "@types/sharp": "^0.28.0", 37 | "exif-reader": "^1.0.3", 38 | "express-prom-bundle": "^6.3.6", 39 | "icc": "^2.0.0", 40 | "iptc-reader": "^1.0.0", 41 | "mongoose": "^5.12.5", 42 | "nestjs-pino": "^1.4.0", 43 | "opentelemetry-instrumentation-mongoose": "^0.4.0", 44 | "pg": "^8.6.0", 45 | "prom-client": "^13.1.0", 46 | "reflect-metadata": "^0.1.13", 47 | "rimraf": "^3.0.2", 48 | "rxjs": "^6.6.6", 49 | "sharp": "^0.28.1", 50 | "ts-node-iptc": "^1.0.11", 51 | "typeorm": "^0.2.32", 52 | "xml2js": "^0.4.23" 53 | }, 54 | "devDependencies": { 55 | "@nestjs/cli": "^7.6.0", 56 | "@nestjs/schematics": "^7.3.0", 57 | "@nestjs/testing": "^7.6.15", 58 | "@types/cron": "^1.7.2", 59 | "@types/express": "^4.17.11", 60 | "@types/jest": "^26.0.22", 61 | "@types/multer": "^1.4.5", 62 | "@types/node": "^14.14.36", 63 | "@types/supertest": "^2.0.10", 64 | "@typescript-eslint/eslint-plugin": "^4.19.0", 65 | "@typescript-eslint/parser": "^4.19.0", 66 | "eslint": "^7.22.0", 67 | "eslint-config-prettier": "^8.1.0", 68 | "eslint-plugin-prettier": "^3.3.1", 69 | "jest": "^26.6.3", 70 | "prettier": "^2.2.1", 71 | "supertest": "^6.1.3", 72 | "ts-jest": "^26.5.4", 73 | "ts-loader": "^8.0.18", 74 | "ts-node": "^9.1.1", 75 | "tsconfig-paths": "^3.9.0", 76 | "typescript": "^4.2.3" 77 | }, 78 | "jest": { 79 | "moduleFileExtensions": [ 80 | "js", 81 | "json", 82 | "ts" 83 | ], 84 | "rootDir": "src", 85 | "testRegex": ".*\\.spec\\.ts$", 86 | "transform": { 87 | "^.+\\.(t|j)s$": "ts-jest" 88 | }, 89 | "collectCoverageFrom": [ 90 | "**/*.(t|j)s" 91 | ], 92 | "coverageDirectory": "../coverage", 93 | "testEnvironment": "node" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /backend/prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npm i 3 | npm run build 4 | exec npm run start:prod -------------------------------------------------------------------------------- /backend/rundev.sh: -------------------------------------------------------------------------------- 1 | docker run -v `pwd`:/home/project -w /home/project -p 3000:3000 -it --rm node npm run start:dev -------------------------------------------------------------------------------- /backend/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /backend/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Controller, 4 | Get, 5 | Post, 6 | UnsupportedMediaTypeException, 7 | UploadedFile, 8 | UseInterceptors, 9 | Request, 10 | Response, 11 | Param, 12 | NotFoundException, 13 | } from '@nestjs/common'; 14 | import { AppService } from './app.service'; 15 | import { 16 | Express, 17 | Request as ExpressRequest, 18 | Response as ExpressResponse, 19 | } from 'express'; 20 | import { FileInterceptor } from '@nestjs/platform-express'; 21 | import { join } from 'path'; 22 | import { createReadStream } from 'fs'; 23 | import { stat, access } from 'fs/promises'; 24 | import { imageReadBytesCounter, imageReadCountCounter } from './prom-metrics'; 25 | @Controller() 26 | export class AppController { 27 | constructor(private readonly appService: AppService) {} 28 | 29 | @Get() 30 | getHello(): string { 31 | return this.appService.getHello(); 32 | } 33 | 34 | @Get('/images/:id.jpg') 35 | async getImage(@Param('id') id: string, @Response() resp: ExpressResponse) { 36 | const intId = Number(id); 37 | if (!Number.isInteger(intId)) 38 | throw new NotFoundException( 39 | `id is '${id}'.jpg must contains only numbers`, 40 | ); 41 | const imageInfo = await this.appService.getImageInfo(intId); 42 | if (!imageInfo.info) 43 | throw new NotFoundException(`Image '${id}' not found by the service`); 44 | const path = join('/images', imageInfo.info.filename); 45 | try { 46 | await access(path); 47 | } catch (error) { 48 | throw new NotFoundException('File not found'); 49 | } 50 | 51 | const fileStat = await stat(path); 52 | resp.setHeader('content-length', fileStat.size); 53 | resp.setHeader('content-type', 'image/jpeg'); 54 | 55 | const stream = createReadStream(path); 56 | stream.pipe(resp); 57 | stream.once('end', () => { 58 | imageReadCountCounter.inc(1); 59 | imageReadBytesCounter.inc(fileStat.size); 60 | }); 61 | } 62 | 63 | @Get('info/:id') 64 | async getInfo(@Param('id') id: string) { 65 | const intId = Number(id); 66 | if (!Number.isInteger(intId)) 67 | throw new NotFoundException(`id is '${id}' but must be an integer`); 68 | return this.appService.getImageInfo(intId); 69 | } 70 | 71 | @Post('upload') 72 | @UseInterceptors(FileInterceptor('file')) 73 | async upload( 74 | @Request() req: ExpressRequest, 75 | @UploadedFile() file: Express.Multer.File, 76 | ) { 77 | if (file.mimetype !== 'image/jpeg') 78 | throw new UnsupportedMediaTypeException( 79 | "Only 'image/jpeg' mimetype supported", 80 | ); 81 | 82 | try { 83 | //const metadata = await this.appService.getImageMetadata(file.buffer); 84 | const saveResult = await this.appService.saveImage( 85 | file.buffer, 86 | req.socket.remoteAddress, 87 | ); 88 | return saveResult; 89 | } catch (error) { 90 | throw new BadRequestException(error.toString()); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { AppController } from './app.controller'; 5 | import { AppService } from './app.service'; 6 | import { UploadedImage } from './uploadedImage.entity'; 7 | import { MongooseModule } from '@nestjs/mongoose'; 8 | import { ImageMetadata, ImageMetadataSchema } from './imageMetadata.shema'; 9 | import { ScheduleModule } from '@nestjs/schedule'; 10 | import { CronService } from './cron.service'; 11 | import { LoggerModule } from 'nestjs-pino'; 12 | import { Request, Response } from 'express'; 13 | 14 | @Module({ 15 | imports: [ 16 | LoggerModule.forRoot({ 17 | pinoHttp: { 18 | reqCustomProps: (req: Request, res: Response) => { 19 | const traceID = res.getHeader('trace-id'); 20 | console.log(`reqCustomProps traceID = ${traceID}`); 21 | return { traceID }; 22 | }, 23 | }, 24 | }), 25 | MongooseModule.forRoot('mongodb://mongo/images'), 26 | MongooseModule.forFeature([ 27 | { name: ImageMetadata.name, schema: ImageMetadataSchema }, 28 | ]), 29 | TypeOrmModule.forRoot({ 30 | type: 'postgres', 31 | host: 'db', 32 | port: 5432, 33 | username: 'images', 34 | password: 'password', 35 | database: 'images', 36 | entities: [UploadedImage], 37 | synchronize: true, 38 | }), 39 | TypeOrmModule.forFeature([UploadedImage]), 40 | ScheduleModule.forRoot(), 41 | ], 42 | controllers: [AppController], 43 | providers: [AppService, CronService], 44 | }) 45 | export class AppModule {} 46 | -------------------------------------------------------------------------------- /backend/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as sharp from 'sharp'; 3 | import * as exifReader from 'exif-reader'; 4 | import * as icc from 'icc'; 5 | import { IptcParser } from 'ts-node-iptc'; 6 | import { parseString as xmlParse } from 'xml2js'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { Repository } from 'typeorm'; 9 | import { UploadedImage } from './uploadedImage.entity'; 10 | import { join } from 'path'; 11 | import { writeFile } from 'fs/promises'; 12 | import { InjectModel } from '@nestjs/mongoose'; 13 | import { ImageMetadata, ImageMetadataDocument } from './imageMetadata.shema'; 14 | import { LeanDocument, Model } from 'mongoose'; 15 | import { imageWriteBytesCounter, imageWriteCountCounter } from './prom-metrics'; 16 | 17 | @Injectable() 18 | export class AppService { 19 | constructor( 20 | @InjectRepository(UploadedImage) 21 | private uploadedImageRepository: Repository, 22 | @InjectModel(ImageMetadata.name) 23 | private ImageMetadataModel: Model, 24 | ) {} 25 | 26 | getHello(): string { 27 | return 'Hello World!'; 28 | } 29 | 30 | private async getImageMetadata(buffer: Buffer): Promise { 31 | const meta = await sharp(buffer).metadata(); 32 | if (meta.exif) { 33 | try { 34 | meta.exif = exifReader(meta.exif); 35 | } catch (err) { 36 | //todo log error 37 | } 38 | } 39 | if (meta.icc) { 40 | try { 41 | meta.icc = icc.parse(meta.icc); 42 | } catch (err) {} 43 | } 44 | 45 | if (meta.iptc) { 46 | try { 47 | meta.iptc = IptcParser.readIPTCData(meta.iptc) as Buffer; 48 | } catch (err) {} 49 | } 50 | 51 | if (meta.xmp) { 52 | try { 53 | const xpmString = meta.xmp.toString('utf8'); 54 | const decodedXmp = await new Promise((resolve, reject) => 55 | xmlParse(xpmString, (err, result) => { 56 | if (err) return reject(err); 57 | resolve(result); 58 | }), 59 | ); 60 | meta.xmp = decodedXmp as Buffer; 61 | } catch (error) {} 62 | } 63 | 64 | return meta; 65 | } 66 | 67 | async saveImage(buf: Buffer, ip: string) { 68 | const uploadedImage = new UploadedImage(); 69 | uploadedImage.date = new Date(); 70 | uploadedImage.size = buf.byteLength; 71 | uploadedImage.ip = ip; 72 | await this.uploadedImageRepository.save([uploadedImage]); 73 | 74 | const id = uploadedImage.id; 75 | const filename = 76 | new Intl.NumberFormat('en-US', { 77 | minimumIntegerDigits: 8, 78 | useGrouping: false, 79 | }).format(id) + '.jpg'; 80 | const path = join('/images', filename); 81 | 82 | await writeFile(path, buf); 83 | imageWriteCountCounter.inc(1); 84 | imageWriteBytesCounter.inc(buf.byteLength); 85 | 86 | uploadedImage.filename = filename; 87 | await this.uploadedImageRepository.save([uploadedImage]); 88 | 89 | await this.saveImageMetadata(buf, id); 90 | 91 | return uploadedImage; 92 | } 93 | 94 | private async saveImageMetadata(buf: Buffer, id: number) { 95 | try { 96 | const clear$ = function (obj: any) { 97 | for (const key in obj) { 98 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 99 | const element = obj[key]; 100 | if (typeof element === 'object') { 101 | clear$(element); 102 | } 103 | if (key.startsWith('$')) { 104 | delete obj[key]; 105 | obj['_' + key] = element; 106 | } 107 | } 108 | } 109 | }; 110 | const meta = await this.getImageMetadata(buf); 111 | clear$(meta); 112 | const createdMeta = new this.ImageMetadataModel(meta); 113 | createdMeta.id = id; 114 | await createdMeta.save(); 115 | } catch (error) { 116 | console.log(error); 117 | } 118 | } 119 | 120 | async getImageInfo(id: number) { 121 | const result = {} as { 122 | info?: UploadedImage; 123 | metadata?: LeanDocument; 124 | }; 125 | const imginfo = await this.uploadedImageRepository.findOne({ id }); 126 | result.info = imginfo; 127 | if (imginfo) { 128 | imginfo.lastAccessDate = new Date(); 129 | await this.uploadedImageRepository.save([imginfo]); 130 | const meta = await this.ImageMetadataModel.findOne({ id }).exec(); 131 | result.metadata = meta.toObject(); 132 | } 133 | return result; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /backend/src/cron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PinoLogger } from 'nestjs-pino'; 3 | import { Cron } from '@nestjs/schedule'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { UploadedImage } from './uploadedImage.entity'; 6 | import { LessThan, Repository } from 'typeorm'; 7 | import { InjectModel } from '@nestjs/mongoose'; 8 | import { ImageMetadata, ImageMetadataDocument } from './imageMetadata.shema'; 9 | import { Model } from 'mongoose'; 10 | import { join } from 'path'; 11 | import { unlink, opendir, stat } from 'fs/promises'; 12 | import { 13 | imageDeletedCountCounter, 14 | imageTotalFileBytesCountGauge, 15 | imageTotalFilesCountGauge, 16 | } from './prom-metrics'; 17 | 18 | @Injectable() 19 | export class CronService { 20 | constructor( 21 | @InjectRepository(UploadedImage) 22 | private uploadedImageRepository: Repository, 23 | @InjectModel(ImageMetadata.name) 24 | private ImageMetadataModel: Model, 25 | private readonly logger: PinoLogger, 26 | ) { 27 | logger.setContext(CronService.name); 28 | } 29 | @Cron('0 * * * * *') 30 | async collectImageDirCountAndSize() { 31 | try { 32 | this.logger.info('Collect metrics for /image dir'); 33 | const dircontent = await opendir('/images'); 34 | let totalSize = 0; 35 | let count = 0; 36 | for await (const dirent of dircontent) { 37 | if (dirent.isFile() && dirent.name.endsWith('.jpg')) { 38 | const _stat = await stat(join('/images', dirent.name)); 39 | totalSize += _stat.size; 40 | count += 1; 41 | } 42 | } 43 | imageTotalFileBytesCountGauge.set(totalSize); 44 | imageTotalFilesCountGauge.set(count); 45 | } catch (err) { 46 | this.logger.error(err, 'Error when collect metrics for /image dir'); 47 | } 48 | this.logger.info('Collect metrics for /image dir finished'); 49 | } 50 | private removeOldImagesRun = false; 51 | @Cron('45 * * * * *') 52 | async removeOldImages() { 53 | if (this.removeOldImagesRun) return; 54 | try { 55 | this.removeOldImagesRun = true; 56 | const minutesAgo = new Date(Date.now() - 60000 * 10); 57 | const unasccesedImages = await this.uploadedImageRepository.find({ 58 | where: { lastAccessDate: null, date: LessThan(minutesAgo) }, 59 | }); 60 | 61 | this.logger.info( 62 | `Found ${unasccesedImages.length} unaccessed images more then 10 minutes old`, 63 | ); 64 | for (const item of unasccesedImages) { 65 | await this.deleteImage(item); 66 | this.logger.info(`Image id = ${item.id} deleted`); 67 | } 68 | const tenMinlastaccImages = await this.uploadedImageRepository.find({ 69 | where: { lastAccessDate: LessThan(minutesAgo) }, 70 | }); 71 | this.logger.info( 72 | `Found ${unasccesedImages.length} images with more then 10 minutes last access`, 73 | ); 74 | 75 | for (const item of tenMinlastaccImages) { 76 | await this.deleteImage(item); 77 | this.logger.info(`Image id = ${item.id} deleted`); 78 | } 79 | } catch (err) { 80 | this.logger.error(err, 'Unexpected error when remove old images'); 81 | } finally { 82 | this.removeOldImagesRun = false; 83 | } 84 | } 85 | 86 | private async deleteImage(data: UploadedImage) { 87 | try { 88 | await this.uploadedImageRepository.delete({ id: data.id }); 89 | } catch (err) { 90 | this.logger.error( 91 | err, 92 | `Error when deleting record id: ${data?.id} from db`, 93 | ); 94 | } 95 | 96 | try { 97 | await this.ImageMetadataModel.findOneAndDelete({ id: data.id }); 98 | } catch (error) { 99 | this.logger.error( 100 | error, 101 | `Error when delete image id: ${data?.id} metadata from db`, 102 | ); 103 | } 104 | 105 | try { 106 | const path = join('/images', data.filename); 107 | await unlink(path); 108 | imageDeletedCountCounter.inc(1); 109 | } catch (error) { 110 | this.logger.error( 111 | error, 112 | `Error when delete image id: ${data?.id} from disk`, 113 | ); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /backend/src/imageMetadata.shema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document, Schema as SchemaType } from 'mongoose'; 3 | 4 | export type ImageMetadataDocument = ImageMetadata & Document; 5 | 6 | @Schema() 7 | export class ImageMetadata { 8 | @Prop() 9 | id: number; 10 | /** Number value of the EXIF Orientation header, if present */ 11 | @Prop() 12 | orientation?: number; 13 | 14 | /** Name of decoder used to decompress image data e.g. jpeg, png, webp, gif, svg */ 15 | @Prop() 16 | format?: string; 17 | 18 | /** Total size of image in bytes, for Stream and Buffer input only */ 19 | @Prop() 20 | size?: number; 21 | 22 | /** Number of pixels wide (EXIF orientation is not taken into consideration) */ 23 | @Prop() 24 | width?: number; 25 | 26 | /** Number of pixels high (EXIF orientation is not taken into consideration) */ 27 | @Prop() 28 | height?: number; 29 | 30 | /** Name of colour space interpretation */ 31 | @Prop() 32 | space?: string; 33 | 34 | /** Number of bands e.g. 3 for sRGB, 4 for CMYK */ 35 | @Prop() 36 | channels?: number; 37 | 38 | /** Name of pixel depth format e.g. uchar, char, ushort, float ... */ 39 | @Prop() 40 | depth?: string; 41 | 42 | /** Number of pixels per inch (DPI), if present */ 43 | @Prop() 44 | density?: number; 45 | 46 | /** String containing JPEG chroma subsampling, 4:2:0 or 4:4:4 for RGB, 4:2:0:4 or 4:4:4:4 for CMYK */ 47 | @Prop() 48 | chromaSubsampling: string; 49 | 50 | /** Boolean indicating whether the image is interlaced using a progressive scan */ 51 | @Prop() 52 | isProgressive?: boolean; 53 | 54 | /** Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP */ 55 | @Prop() 56 | pages?: number; 57 | 58 | /** Number of pixels high each page in a multi-page image will be. */ 59 | @Prop() 60 | pageHeight?: number; 61 | 62 | /** Number of times to loop an animated image, zero refers to a continuous loop. */ 63 | @Prop() 64 | loop?: number; 65 | 66 | /** Delay in ms between each page in an animated image, provided as an array of integers. */ 67 | @Prop() 68 | delay?: number[]; 69 | 70 | /** Number of the primary page in a HEIF image */ 71 | @Prop() 72 | pagePrimary?: number; 73 | 74 | /** Boolean indicating the presence of an embedded ICC profile */ 75 | @Prop() 76 | hasProfile?: boolean; 77 | 78 | /** Boolean indicating the presence of an alpha transparency channel */ 79 | @Prop() 80 | hasAlpha?: boolean; 81 | 82 | /** Buffer containing raw EXIF data, if present */ 83 | @Prop({ type: SchemaType.Types.Mixed }) 84 | exif?: any; 85 | 86 | /** Buffer containing raw ICC profile data, if present */ 87 | @Prop({ type: SchemaType.Types.Mixed }) 88 | icc?: any; 89 | 90 | /** Buffer containing raw IPTC data, if present */ 91 | @Prop({ type: SchemaType.Types.Mixed }) 92 | iptc?: any; 93 | 94 | /** Buffer containing raw XMP data, if present */ 95 | @Prop({ type: SchemaType.Types.Mixed }) 96 | xmp?: any; 97 | 98 | /** Buffer containing raw TIFFTAG_PHOTOSHOP data, if present */ 99 | // @Prop({type: Buffer}) 100 | // tifftagPhotoshop?: Buffer; 101 | } 102 | 103 | export const ImageMetadataSchema = SchemaFactory.createForClass(ImageMetadata); 104 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | //Instrumentation must be setup before first require of Express 2 | import { tracer, openTelemetryEnabled } from './ot-instrumentation'; 3 | import { metricsEnable, metricsMiddleware } from './prom-metrics'; 4 | import { Logger } from 'nestjs-pino'; 5 | 6 | import { Request, Response, NextFunction } from 'express'; 7 | 8 | import { NestFactory } from '@nestjs/core'; 9 | import { AppModule } from './app.module'; 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create(AppModule, { 13 | logger: false, 14 | }); 15 | //Add trace-id header to response if openTelemetry tracing enabled 16 | if (openTelemetryEnabled) { 17 | app.use((req: Request, res: Response, next: NextFunction) => { 18 | const span = tracer.startSpan('Add trace-id header'); 19 | const traceId = span.context().traceId; 20 | res.setHeader('trace-id', traceId); 21 | next(); 22 | span.end(); 23 | }); 24 | } 25 | //Add metrics if enabled 26 | if (metricsEnable) { 27 | app.use(metricsMiddleware); 28 | } 29 | app.useLogger(app.get(Logger)); 30 | 31 | await app.listen(3000); 32 | console.log('App started'); 33 | } 34 | bootstrap(); 35 | -------------------------------------------------------------------------------- /backend/src/ot-instrumentation.ts: -------------------------------------------------------------------------------- 1 | import { NodeTracerProvider } from '@opentelemetry/node'; 2 | import { registerInstrumentations } from '@opentelemetry/instrumentation'; 3 | import { BatchSpanProcessor } from '@opentelemetry/tracing'; 4 | import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; 5 | import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; 6 | import { MongooseInstrumentation } from 'opentelemetry-instrumentation-mongoose'; 7 | import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; 8 | import { trace } from '@opentelemetry/api'; 9 | 10 | export const openTelemetryEnabled = process.env.OT_TRACING_ENABLED === 'true'; 11 | export const openTelemetryHost = process.env.OT_TRACING_HOST; 12 | 13 | console.log('OT_TRACING_ENABLED =', process.env.OT_TRACING_ENABLED); 14 | console.log('OT_TRACING_HOST = ', process.env.OT_TRACING_HOST); 15 | 16 | function enableOTInstrumentation() { 17 | console.log('OpenTelemetry instrmentation setup begins...'); 18 | const provider = new NodeTracerProvider(); 19 | provider.register(); 20 | 21 | registerInstrumentations({ 22 | instrumentations: [ 23 | // Express instrumentation expects HTTP layer to be instrumented 24 | new HttpInstrumentation(), 25 | new ExpressInstrumentation(), 26 | new MongooseInstrumentation(), 27 | ], 28 | // tracerProvider: provider, 29 | }); 30 | 31 | provider.addSpanProcessor( 32 | new BatchSpanProcessor( 33 | new JaegerExporter({ 34 | host: openTelemetryHost, 35 | port: 6832, 36 | serviceName: 'backend', 37 | }), 38 | ), 39 | ); 40 | console.log('OpenTelemetry tracing enabled'); 41 | } 42 | 43 | if (openTelemetryEnabled) { 44 | if (openTelemetryHost) { 45 | enableOTInstrumentation(); 46 | } else { 47 | console.log( 48 | 'To enable OpenTelemetry tracing add OT_TRACING_HOST evironment variable', 49 | ); 50 | } 51 | } else { 52 | console.log( 53 | 'OpenTelemetry tracing disabled. To enable OpenTelemetry tracing set OT_TRACING_ENABLED to "true"' + 54 | ' and add OT_TRACING_HOST evironment variable', 55 | ); 56 | } 57 | 58 | export const tracer = 59 | openTelemetryEnabled && openTelemetryHost 60 | ? trace.getTracer('backend') 61 | : undefined; 62 | -------------------------------------------------------------------------------- /backend/src/prom-metrics.ts: -------------------------------------------------------------------------------- 1 | import * as promBundle from 'express-prom-bundle'; 2 | import * as promClient from 'prom-client'; 3 | export const metricsEnable = process.env.PROM_METRICS_ENABLE === 'true'; 4 | 5 | console.log('PROM_METRICS_ENABLE =', process.env.PROM_METRICS_ENABLE); 6 | 7 | export const metricsMiddleware = metricsEnable 8 | ? promBundle({ 9 | includeMethod: true, 10 | promClient: { 11 | collectDefaultMetrics: {}, 12 | }, 13 | }) 14 | : undefined; 15 | 16 | export const metricsClient = promClient; 17 | 18 | export const imageReadCountCounter = new promClient.Counter({ 19 | name: 'image_file_read_count', 20 | help: 'Image files reads count', 21 | }); 22 | 23 | export const imageReadBytesCounter = new promClient.Counter({ 24 | name: 'image_file_read_bytes', 25 | help: 'Image files reads bytes', 26 | }); 27 | 28 | export const imageWriteCountCounter = new promClient.Counter({ 29 | name: 'image_file_write_count', 30 | help: 'Image files writes count', 31 | }); 32 | 33 | export const imageWriteBytesCounter = new promClient.Counter({ 34 | name: 'image_file_write_bytes', 35 | help: 'Image files writes bytes', 36 | }); 37 | 38 | export const imageDeletedCountCounter = new promClient.Counter({ 39 | name: 'image_file_deleted_count', 40 | help: 'Image files deleted count', 41 | }); 42 | 43 | export const imageTotalFilesCountGauge = new promClient.Gauge({ 44 | name: 'image_total_files_count', 45 | help: 'Image total files in /image directory', 46 | }); 47 | 48 | export const imageTotalFileBytesCountGauge = new promClient.Gauge({ 49 | name: 'image_total_files_bytes', 50 | help: 'Image total files size in /image directory', 51 | }); 52 | 53 | if (metricsMiddleware) { 54 | console.log('Metrics enabled'); 55 | } else { 56 | console.log( 57 | 'Metrics disabled. To enable metrics set env PROM_METRICS_ENABLE: "true"', 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/uploadedImage.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class UploadedImage { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | ip: string; 10 | 11 | @Column() 12 | date: Date; 13 | 14 | @Column({nullable: true}) 15 | lastAccessDate?: Date; 16 | 17 | @Column() 18 | size:number; 19 | 20 | @Column({nullable: true}) 21 | filename?:string; 22 | 23 | 24 | } -------------------------------------------------------------------------------- /backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.metrics.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | volumes: 3 | imagesdata: 4 | grafanadata: 5 | postgresdata: 6 | mongodata: 7 | tempodata: 8 | services: 9 | backend: 10 | image: node:lts 11 | volumes: 12 | - ./backend:/home/backend 13 | - imagesdata:/images 14 | working_dir: /home/backend 15 | environment: 16 | OT_TRACING_ENABLED: "false" 17 | PROM_METRICS_ENABLE: "true" 18 | ports: 19 | - 3000:3000 20 | entrypoint: ["/bin/sh"] 21 | command: ["prod.sh"] 22 | restart: always 23 | db: 24 | image: postgres 25 | restart: always 26 | expose: 27 | - "5432" 28 | volumes: 29 | - postgresdata:/var/lib/postgresql/data 30 | environment: 31 | POSTGRES_PASSWORD: password 32 | POSTGRES_USER: images 33 | adminer: 34 | image: adminer 35 | restart: always 36 | ports: 37 | - 8080:8080 38 | mongo: 39 | image: mongo 40 | restart: always 41 | volumes: 42 | - mongodata:/data/db 43 | mongo-express: 44 | image: mongo-express 45 | restart: always 46 | ports: 47 | - 8081:8081 48 | # environment: 49 | # ME_CONFIG_MONGODB_ADMINUSERNAME: root 50 | # ME_CONFIG_MONGODB_ADMINPASSWORD: example 51 | prometheus: 52 | image: prom/prometheus 53 | ports: 54 | - 9090:9090 55 | volumes: 56 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 57 | mongo-exporter: 58 | image: bitnami/mongodb-exporter 59 | ports: 60 | - 9091:9091 61 | command: ["--mongodb.uri=mongodb://mongo", "--web.listen-address=0.0.0.0:9091"] 62 | pg-exporter: 63 | image: bitnami/postgres-exporter 64 | ports: 65 | - 9092:9092 66 | environment: 67 | DATA_SOURCE_NAME: sslmode=disable user=images password=password host=db 68 | PG_EXPORTER_WEB_LISTEN_ADDRESS: 0.0.0.0:9092 69 | grafana: 70 | image: grafana/grafana 71 | ports: 72 | - 3001:3000 73 | volumes: 74 | - grafanadata:/var/lib/grafana 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docker-compose.metrics_logs.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | volumes: 3 | imagesdata: 4 | grafanadata: 5 | postgresdata: 6 | mongodata: 7 | tempodata: 8 | services: 9 | backend: 10 | image: node:lts 11 | volumes: 12 | - ./backend:/home/backend 13 | - imagesdata:/images 14 | working_dir: /home/backend 15 | environment: 16 | OT_TRACING_ENABLED: "false" 17 | PROM_METRICS_ENABLE: "true" 18 | ports: 19 | - 3000:3000 20 | entrypoint: ["/bin/sh"] 21 | command: ["prod.sh"] 22 | restart: always 23 | logging: 24 | driver: loki 25 | options: 26 | loki-url: "http://localhost:3100/loki/api/v1/push" 27 | loki-pipeline-stages: | 28 | - json: 29 | expressions: 30 | output: msg 31 | level: level 32 | timestamp: time 33 | pid: pid 34 | hostname: hostname 35 | context: context 36 | traceID: traceID 37 | db: 38 | image: postgres 39 | restart: always 40 | expose: 41 | - "5432" 42 | volumes: 43 | - postgresdata:/var/lib/postgresql/data 44 | environment: 45 | POSTGRES_PASSWORD: password 46 | POSTGRES_USER: images 47 | logging: 48 | driver: loki 49 | options: 50 | loki-url: "http://localhost:3100/loki/api/v1/push" 51 | adminer: 52 | image: adminer 53 | restart: always 54 | ports: 55 | - 8080:8080 56 | mongo: 57 | image: mongo 58 | restart: always 59 | volumes: 60 | - mongodata:/data/db 61 | logging: 62 | driver: loki 63 | options: 64 | loki-url: "http://localhost:3100/loki/api/v1/push" 65 | mongo-express: 66 | image: mongo-express 67 | restart: always 68 | ports: 69 | - 8081:8081 70 | prometheus: 71 | image: prom/prometheus 72 | ports: 73 | - 9090:9090 74 | volumes: 75 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 76 | mongo-exporter: 77 | image: bitnami/mongodb-exporter 78 | ports: 79 | - 9091:9091 80 | command: ["--mongodb.uri=mongodb://mongo", "--web.listen-address=0.0.0.0:9091"] 81 | pg-exporter: 82 | image: bitnami/postgres-exporter 83 | ports: 84 | - 9092:9092 85 | environment: 86 | DATA_SOURCE_NAME: sslmode=disable user=images password=password host=db 87 | PG_EXPORTER_WEB_LISTEN_ADDRESS: 0.0.0.0:9092 88 | grafana: 89 | image: grafana/grafana 90 | ports: 91 | - 3001:3000 92 | volumes: 93 | - grafanadata:/var/lib/grafana 94 | loki: 95 | image: grafana/loki:2.0.0 96 | ports: 97 | - "3100:3100" 98 | command: -config.file=/etc/loki/local-config.yaml 99 | 100 | -------------------------------------------------------------------------------- /docker-compose.metrics_logs_jaeger.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | volumes: 3 | imagesdata: 4 | grafanadata: 5 | postgresdata: 6 | mongodata: 7 | tempodata: 8 | jaegerdata: 9 | services: 10 | backend: 11 | image: node:lts 12 | volumes: 13 | - ./backend:/home/backend 14 | - imagesdata:/images 15 | working_dir: /home/backend 16 | environment: 17 | OT_TRACING_ENABLED: "true" 18 | OT_TRACING_HOST: "jaeger" 19 | PROM_METRICS_ENABLE: "true" 20 | ports: 21 | - 3000:3000 22 | entrypoint: ["/bin/sh"] 23 | command: ["prod.sh"] 24 | restart: always 25 | logging: 26 | driver: loki 27 | options: 28 | loki-url: "http://localhost:3100/loki/api/v1/push" 29 | loki-pipeline-stages: | 30 | - json: 31 | expressions: 32 | output: msg 33 | level: level 34 | timestamp: time 35 | pid: pid 36 | hostname: hostname 37 | context: context 38 | traceID: traceID 39 | db: 40 | image: postgres 41 | restart: always 42 | expose: 43 | - "5432" 44 | volumes: 45 | - postgresdata:/var/lib/postgresql/data 46 | environment: 47 | POSTGRES_PASSWORD: password 48 | POSTGRES_USER: images 49 | logging: 50 | driver: loki 51 | options: 52 | loki-url: "http://localhost:3100/loki/api/v1/push" 53 | adminer: 54 | image: adminer 55 | restart: always 56 | ports: 57 | - 8080:8080 58 | mongo: 59 | image: mongo 60 | restart: always 61 | volumes: 62 | - mongodata:/data/db 63 | logging: 64 | driver: loki 65 | options: 66 | loki-url: "http://localhost:3100/loki/api/v1/push" 67 | # environment: 68 | # MONGO_INITDB_ROOT_USERNAME: root 69 | # MONGO_INITDB_ROOT_PASSWORD: example 70 | # logging: 71 | # driver: gelf 72 | # options: 73 | # gelf-address: "udp://localhost:12201" 74 | # tag: "demo2_redis" 75 | mongo-express: 76 | image: mongo-express 77 | restart: always 78 | ports: 79 | - 8081:8081 80 | # environment: 81 | # ME_CONFIG_MONGODB_ADMINUSERNAME: root 82 | # ME_CONFIG_MONGODB_ADMINPASSWORD: example 83 | prometheus: 84 | image: prom/prometheus 85 | ports: 86 | - 9090:9090 87 | volumes: 88 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 89 | mongo-exporter: 90 | image: bitnami/mongodb-exporter 91 | ports: 92 | - 9091:9091 93 | command: 94 | ["--mongodb.uri=mongodb://mongo", "--web.listen-address=0.0.0.0:9091"] 95 | pg-exporter: 96 | image: bitnami/postgres-exporter 97 | ports: 98 | - 9092:9092 99 | environment: 100 | DATA_SOURCE_NAME: sslmode=disable user=images password=password host=db 101 | PG_EXPORTER_WEB_LISTEN_ADDRESS: 0.0.0.0:9092 102 | grafana: 103 | image: grafana/grafana 104 | ports: 105 | - 3001:3000 106 | volumes: 107 | - grafanadata:/var/lib/grafana 108 | loki: 109 | image: grafana/loki:2.0.0 110 | ports: 111 | - "3100:3100" 112 | command: -config.file=/etc/loki/local-config.yaml 113 | jaeger: 114 | image: jaegertracing/all-in-one 115 | environment: 116 | SPAN_STORAGE_TYPE: badger 117 | BADGER_EPHEMERAL: "false" 118 | BADGER_DIRECTORY_VALUE: /badger/data 119 | BADGER_DIRECTORY_KEY: /badger/key 120 | volumes: 121 | - jaegerdata:/badger 122 | ports: 123 | - "6832/udp" 124 | - 16686:16686 125 | -------------------------------------------------------------------------------- /docker-compose.metrics_logs_tempo.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | volumes: 3 | imagesdata: 4 | grafanadata: 5 | postgresdata: 6 | mongodata: 7 | tempodata: 8 | services: 9 | backend: 10 | image: node:lts 11 | volumes: 12 | - ./backend:/home/backend 13 | - imagesdata:/images 14 | working_dir: /home/backend 15 | environment: 16 | OT_TRACING_ENABLED: "true" 17 | PROM_METRICS_ENABLE: "true" 18 | ports: 19 | - 3000:3000 20 | entrypoint: ["/bin/sh"] 21 | command: ["prod.sh"] 22 | restart: always 23 | logging: 24 | driver: loki 25 | options: 26 | loki-url: "http://localhost:3100/loki/api/v1/push" 27 | loki-pipeline-stages: | 28 | - json: 29 | expressions: 30 | output: msg 31 | level: level 32 | timestamp: time 33 | pid: pid 34 | hostname: hostname 35 | context: context 36 | traceID: traceID 37 | db: 38 | image: postgres 39 | restart: always 40 | expose: 41 | - "5432" 42 | volumes: 43 | - postgresdata:/var/lib/postgresql/data 44 | environment: 45 | POSTGRES_PASSWORD: password 46 | POSTGRES_USER: images 47 | logging: 48 | driver: loki 49 | options: 50 | loki-url: "http://localhost:3100/loki/api/v1/push" 51 | adminer: 52 | image: adminer 53 | restart: always 54 | ports: 55 | - 8080:8080 56 | mongo: 57 | image: mongo 58 | restart: always 59 | volumes: 60 | - mongodata:/data/db 61 | logging: 62 | driver: loki 63 | options: 64 | loki-url: "http://localhost:3100/loki/api/v1/push" 65 | # environment: 66 | # MONGO_INITDB_ROOT_USERNAME: root 67 | # MONGO_INITDB_ROOT_PASSWORD: example 68 | # logging: 69 | # driver: gelf 70 | # options: 71 | # gelf-address: "udp://localhost:12201" 72 | # tag: "demo2_redis" 73 | mongo-express: 74 | image: mongo-express 75 | restart: always 76 | ports: 77 | - 8081:8081 78 | # environment: 79 | # ME_CONFIG_MONGODB_ADMINUSERNAME: root 80 | # ME_CONFIG_MONGODB_ADMINPASSWORD: example 81 | prometheus: 82 | image: prom/prometheus 83 | ports: 84 | - 9090:9090 85 | volumes: 86 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 87 | mongo-exporter: 88 | image: bitnami/mongodb-exporter 89 | ports: 90 | - 9091:9091 91 | command: ["--mongodb.uri=mongodb://mongo", "--web.listen-address=0.0.0.0:9091"] 92 | pg-exporter: 93 | image: bitnami/postgres-exporter 94 | ports: 95 | - 9092:9092 96 | environment: 97 | DATA_SOURCE_NAME: sslmode=disable user=images password=password host=db 98 | PG_EXPORTER_WEB_LISTEN_ADDRESS: 0.0.0.0:9092 99 | grafana: 100 | image: grafana/grafana 101 | ports: 102 | - 3001:3000 103 | volumes: 104 | - grafanadata:/var/lib/grafana 105 | loki: 106 | image: grafana/loki:2.0.0 107 | ports: 108 | - "3100:3100" 109 | command: -config.file=/etc/loki/local-config.yaml 110 | tempo: 111 | image: grafana/tempo:latest 112 | command: ["-config.file=/etc/tempo.yaml"] 113 | volumes: 114 | - ./tempo-local.yaml:/etc/tempo.yaml 115 | - tempodata:/tmp/tempo 116 | ports: 117 | # - "14268" # jaeger ingest, Jaeger - Thrift HTTP 118 | # - "14250" # Jaeger - GRPC 119 | # - "55680" # OpenTelemetry 120 | # - "3100" # tempo 121 | # - "6831/udp" # Jaeger - Thrift Compact 122 | - "6832/udp" # Jaeger - Thrift Binary 123 | 124 | # elasticsearch: 125 | # image: elasticsearch:7.12.1 126 | # environment: 127 | # - discovery.type=single-node 128 | # - ES_JAVA_OPTS=-Xms250m -Xmx250m 129 | # ports: 130 | # - 9200:9200 131 | # - 9300:9300 132 | # logstash: 133 | # image: logstash:7.12.1 134 | # links: 135 | # - elasticsearch 136 | # environment: 137 | # - ES_JAVA_OPTS=-Xms250m -Xmx250m 138 | # volumes: 139 | # - ./logstash.conf:/etc/logstash/logstash.conf 140 | # command: logstash -f /etc/logstash/logstash.conf 141 | # ports: 142 | # - 12201:12201/udp 143 | # depends_on: 144 | # - elasticsearch 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /docker-compose.nomon.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | volumes: 3 | imagesdata: 4 | grafanadata: 5 | postgresdata: 6 | mongodata: 7 | tempodata: 8 | services: 9 | backend: 10 | image: node:lts 11 | volumes: 12 | - ./backend:/home/backend 13 | - imagesdata:/images 14 | working_dir: /home/backend 15 | environment: 16 | OT_TRACING_ENABLED: "false" 17 | PROM_METRICS_ENABLE: "false" 18 | ports: 19 | - 3000:3000 20 | entrypoint: ["/bin/sh"] 21 | command: ["prod.sh"] 22 | restart: always 23 | db: 24 | image: postgres 25 | restart: always 26 | expose: 27 | - "5432" 28 | volumes: 29 | - postgresdata:/var/lib/postgresql/data 30 | environment: 31 | POSTGRES_PASSWORD: password 32 | POSTGRES_USER: images 33 | adminer: 34 | image: adminer 35 | restart: always 36 | ports: 37 | - 8080:8080 38 | mongo: 39 | image: mongo 40 | restart: always 41 | volumes: 42 | - mongodata:/data/db 43 | mongo-express: 44 | image: mongo-express 45 | restart: always 46 | ports: 47 | - 8081:8081 48 | -------------------------------------------------------------------------------- /logstash.conf: -------------------------------------------------------------------------------- 1 | input { 2 | gelf { 3 | port => 12201 4 | } 5 | } 6 | output { 7 | elasticsearch { 8 | hosts => ["elasticsearch:9200"] 9 | index => "logstash-%{+YYYY-MM-dd}" 10 | } 11 | } -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | scrape_configs: 4 | - job_name: 'nodejs' 5 | honor_labels: true 6 | static_configs: 7 | - targets: ['backend:3000'] 8 | - job_name: "mongodb" 9 | honor_labels: true 10 | static_configs: 11 | - targets: ['mongo-exporter:9091'] 12 | - job_name: "postgres" 13 | scrape_timeout: 9s 14 | honor_labels: true 15 | static_configs: 16 | - targets: ['pg-exporter:9092'] 17 | - job_name: "tempo" 18 | scrape_timeout: 9s 19 | honor_labels: true 20 | static_configs: 21 | - targets: ['tempo:3100'] -------------------------------------------------------------------------------- /tempo-local.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | 6 | distributor: 7 | receivers: # this configuration will listen on all ports and protocols that tempo is capable of. 8 | jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can 9 | protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/master/receiver 10 | # thrift_http: # 11 | # grpc: # for a production deployment you should only enable the receivers you need! 12 | thrift_binary: 13 | # thrift_compact: 14 | # zipkin: 15 | # otlp: 16 | # protocols: 17 | # http: 18 | # grpc: 19 | # opencensus: 20 | 21 | ingester: 22 | trace_idle_period: 10s # the length of time after a trace has not received spans to consider it complete and flush it 23 | max_block_bytes: 1_000_000 # cut the head block when it hits this size or ... 24 | #traces_per_block: 1_000_000 25 | max_block_duration: 5m # this much time passes 26 | 27 | compactor: 28 | compaction: 29 | compaction_window: 1h # blocks in this time window will be compacted together 30 | max_compaction_objects: 1000000 # maximum size of compacted blocks 31 | block_retention: 1h 32 | compacted_block_retention: 10m 33 | 34 | storage: 35 | trace: 36 | backend: local # backend configuration to use 37 | wal: 38 | path: /tmp/tempo/wal # where to store the the wal locally 39 | local: 40 | path: /tmp/tempo/blocks 41 | pool: 42 | max_workers: 100 # the worker pool mainly drives querying, but is also used for polling the blocklist 43 | queue_depth: 10000 --------------------------------------------------------------------------------