├── .gitignore ├── README.adoc ├── docker ├── docker-compose.yml ├── grafana-frontend-dashboard.json └── prometheus.yml ├── docs ├── README.adoc ├── appendix.adoc ├── create-student-service.adoc ├── images │ ├── Click_add_Datasource.png │ ├── Configure_Datasource.png │ ├── Display_Dashboard.png │ ├── Filter-Prometheus.png │ ├── Grafana_Login.png │ ├── Import_Json_File.png │ └── Select_Dashboard.png ├── introduction.adoc ├── microprofile-config.adoc ├── microprofile-deploy.adoc ├── microprofile-fault-tolerance.adoc ├── microprofile-health.adoc ├── microprofile-jwt-rbac.adoc ├── microprofile-metrics.adoc └── microprofile-rest-client.adoc ├── jwt ├── jwt-token.json ├── jwtenizr-config.json ├── jwtenizr.jar ├── jwtenizr.sh ├── microprofile-config.properties └── token.jwt └── working └── README.adoc /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Developing Microservices with Eclipse Microprofile 2 | 3 | == Introduction 4 | 5 | 6 | This tutorial shows, in a https://github.com/jclingan/oreilly-microprofile-quarkus-hands-on/tree/master/docs[hands-on, step-by-step manner], how to develop an application using a significant portion of the MicroProfile core specifications, including MicroProfile Config, Rest Client, Fault Tolerance, Metrics, Health, and JWT RBAC. This tutorial assumes some familiarity with JAX-RS and CDI, although having experience with Spring DI and Spring MVC is likely sufficient. 7 | 8 | NOTE: This hands-on tutorial accompanies an https://learning.oreilly.com/live-training/[O'Reilly Live training] on https://learning.oreilly.com/live-training/courses/developing-microservices-with-eclipse-microprofile/0636920360094/[Developing Microservices with Eclipse MicroProfile], but can also be used as a standalone tutorial. If you have an O'Reilly subscription that supports the training, feel free to sign up! 9 | 10 | 11 | == Requirements 12 | * Java 8 or Java 11 13 | * Maven 3.5.3+ 14 | * https://docs.docker.com/compose/install/[Docker Compose] 15 | * OPTIONAL: https://curl.haxx.se/download.html[curl]. The instructions use curl, but tools like https://httpie.org/[HTTPie] or even a browser is sufficient 16 | * OPTIONAL: https://www.graalvm.org/downloads/[GraalVM 19.2.1] Community Edition (for native compilation, JDK 8 only) 17 | 18 | == Additional Informatioin 19 | * Source code is https://github.com/jclingan/oreilly-microprofile-quarkus-hands-on[available in GitHub]. 20 | * Tutorial https://github.com/jclingan/oreilly-microprofile-quarkus-hands-on/tree/master/docs[Instructions] 21 | * The accompanying slides are available at [red]#Location TBD# -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | grafana: 4 | image: grafana/grafana:5.4.2 5 | ports: 6 | - "3000:3000" 7 | depends_on: 8 | - prom 9 | 10 | prom: 11 | image: prom/prometheus:v2.6.0 12 | volumes: 13 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 14 | command: "--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus" 15 | ports: 16 | - 9090:9090 17 | depends_on: 18 | - student 19 | - frontend 20 | 21 | student: 22 | image: acme/student:1.0 23 | ports: 24 | - "8081:8081" 25 | healthcheck: 26 | test: curl --fail -s http://localhost:8081/health/live || exit 1 27 | timeout: 5s 28 | interval: 1s 29 | retries: 0 30 | 31 | 32 | frontend: 33 | image: acme/frontend:1.0 34 | ports: 35 | - "8080:8080" 36 | -------------------------------------------------------------------------------- /docker/grafana-frontend-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 1, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "cacheTimeout": null, 23 | "colorBackground": false, 24 | "colorValue": false, 25 | "colors": [ 26 | "#299c46", 27 | "rgba(237, 129, 40, 0.89)", 28 | "#d44a3a" 29 | ], 30 | "format": "none", 31 | "gauge": { 32 | "maxValue": 25, 33 | "minValue": 0, 34 | "show": true, 35 | "thresholdLabels": true, 36 | "thresholdMarkers": true 37 | }, 38 | "gridPos": { 39 | "h": 4, 40 | "w": 7, 41 | "x": 5, 42 | "y": 0 43 | }, 44 | "id": 12, 45 | "interval": null, 46 | "links": [], 47 | "mappingType": 1, 48 | "mappingTypes": [ 49 | { 50 | "name": "value to text", 51 | "value": 1 52 | }, 53 | { 54 | "name": "range to text", 55 | "value": 2 56 | } 57 | ], 58 | "maxDataPoints": 100, 59 | "nullPointMode": "connected", 60 | "nullText": null, 61 | "postfix": "", 62 | "postfixFontSize": "50%", 63 | "prefix": "", 64 | "prefixFontSize": "50%", 65 | "rangeMaps": [ 66 | { 67 | "from": "null", 68 | "text": "N/A", 69 | "to": "null" 70 | } 71 | ], 72 | "sparkline": { 73 | "fillColor": "rgba(31, 118, 189, 0.18)", 74 | "full": false, 75 | "lineColor": "rgb(31, 120, 193)", 76 | "show": false 77 | }, 78 | "tableColumn": "", 79 | "targets": [ 80 | { 81 | "expr": "application_numberOfStudents", 82 | "format": "time_series", 83 | "intervalFactor": 1, 84 | "legendFormat": "", 85 | "refId": "A" 86 | } 87 | ], 88 | "thresholds": "", 89 | "title": "Number of Students", 90 | "type": "singlestat", 91 | "valueFontSize": "80%", 92 | "valueMaps": [ 93 | { 94 | "op": "=", 95 | "text": "N/A", 96 | "value": "null" 97 | } 98 | ], 99 | "valueName": "avg" 100 | }, 101 | { 102 | "aliasColors": {}, 103 | "bars": false, 104 | "dashLength": 10, 105 | "dashes": false, 106 | "fill": 1, 107 | "gridPos": { 108 | "h": 5, 109 | "w": 7, 110 | "x": 17, 111 | "y": 0 112 | }, 113 | "id": 8, 114 | "legend": { 115 | "avg": false, 116 | "current": false, 117 | "max": false, 118 | "min": false, 119 | "show": true, 120 | "total": false, 121 | "values": false 122 | }, 123 | "lines": true, 124 | "linewidth": 1, 125 | "links": [], 126 | "nullPointMode": "null", 127 | "percentage": false, 128 | "pointradius": 1, 129 | "points": true, 130 | "renderer": "flot", 131 | "seriesOverrides": [], 132 | "spaceLength": 10, 133 | "stack": false, 134 | "steppedLine": false, 135 | "targets": [ 136 | { 137 | "expr": "application_ft_org_acme_FrontendResource_listStudents_fallback_calls_total", 138 | "format": "time_series", 139 | "interval": "", 140 | "intervalFactor": 1, 141 | "legendFormat": "Number of listStudents Fallback Calls", 142 | "refId": "A" 143 | } 144 | ], 145 | "thresholds": [], 146 | "timeFrom": null, 147 | "timeRegions": [], 148 | "timeShift": null, 149 | "title": "listStudents Fallback", 150 | "tooltip": { 151 | "shared": true, 152 | "sort": 0, 153 | "value_type": "individual" 154 | }, 155 | "type": "graph", 156 | "xaxis": { 157 | "buckets": null, 158 | "mode": "time", 159 | "name": null, 160 | "show": true, 161 | "values": [] 162 | }, 163 | "yaxes": [ 164 | { 165 | "format": "short", 166 | "label": "Number of Calls", 167 | "logBase": 1, 168 | "max": null, 169 | "min": null, 170 | "show": true 171 | }, 172 | { 173 | "format": "short", 174 | "label": null, 175 | "logBase": 1, 176 | "max": null, 177 | "min": null, 178 | "show": true 179 | } 180 | ], 181 | "yaxis": { 182 | "align": false, 183 | "alignLevel": null 184 | } 185 | }, 186 | { 187 | "aliasColors": {}, 188 | "bars": false, 189 | "dashLength": 10, 190 | "dashes": false, 191 | "datasource": "Prometheus", 192 | "fill": 1, 193 | "gridPos": { 194 | "h": 9, 195 | "w": 12, 196 | "x": 0, 197 | "y": 4 198 | }, 199 | "id": 2, 200 | "legend": { 201 | "avg": false, 202 | "current": false, 203 | "max": false, 204 | "min": false, 205 | "show": true, 206 | "total": false, 207 | "values": false 208 | }, 209 | "lines": true, 210 | "linewidth": 1, 211 | "links": [], 212 | "nullPointMode": "null", 213 | "percentage": false, 214 | "pointradius": 5, 215 | "points": false, 216 | "renderer": "flot", 217 | "seriesOverrides": [], 218 | "spaceLength": 10, 219 | "stack": false, 220 | "steppedLine": false, 221 | "targets": [ 222 | { 223 | "expr": "application_FrontendCounter_listStudents_total", 224 | "format": "time_series", 225 | "hide": false, 226 | "interval": "", 227 | "intervalFactor": 1, 228 | "refId": "A" 229 | } 230 | ], 231 | "thresholds": [], 232 | "timeFrom": null, 233 | "timeRegions": [], 234 | "timeShift": null, 235 | "title": "Frontend /list Calls", 236 | "tooltip": { 237 | "shared": true, 238 | "sort": 0, 239 | "value_type": "individual" 240 | }, 241 | "type": "graph", 242 | "xaxis": { 243 | "buckets": null, 244 | "mode": "time", 245 | "name": null, 246 | "show": true, 247 | "values": [] 248 | }, 249 | "yaxes": [ 250 | { 251 | "format": "short", 252 | "label": "Number of Calls", 253 | "logBase": 1, 254 | "max": null, 255 | "min": null, 256 | "show": true 257 | }, 258 | { 259 | "format": "short", 260 | "label": null, 261 | "logBase": 1, 262 | "max": null, 263 | "min": null, 264 | "show": true 265 | } 266 | ], 267 | "yaxis": { 268 | "align": false, 269 | "alignLevel": null 270 | } 271 | }, 272 | { 273 | "aliasColors": {}, 274 | "bars": false, 275 | "dashLength": 10, 276 | "dashes": false, 277 | "datasource": "Prometheus", 278 | "description": "Graphs time spent in each circuit breaker states", 279 | "fill": 1, 280 | "gridPos": { 281 | "h": 9, 282 | "w": 12, 283 | "x": 12, 284 | "y": 5 285 | }, 286 | "id": 10, 287 | "legend": { 288 | "avg": false, 289 | "current": false, 290 | "max": false, 291 | "min": false, 292 | "show": true, 293 | "total": false, 294 | "values": false 295 | }, 296 | "lines": true, 297 | "linewidth": 1, 298 | "links": [], 299 | "nullPointMode": "null", 300 | "percentage": false, 301 | "pointradius": 5, 302 | "points": false, 303 | "renderer": "flot", 304 | "seriesOverrides": [], 305 | "spaceLength": 10, 306 | "stack": false, 307 | "steppedLine": false, 308 | "targets": [ 309 | { 310 | "expr": "application_ft_org_acme_FrontendResource_listStudents_circuitbreaker_closed_total/10000000000", 311 | "format": "time_series", 312 | "instant": false, 313 | "interval": "", 314 | "intervalFactor": 1, 315 | "legendFormat": "Closed", 316 | "refId": "A" 317 | }, 318 | { 319 | "expr": "application_ft_org_acme_FrontendResource_listStudents_circuitbreaker_halfOpen_total/10000000000", 320 | "format": "time_series", 321 | "interval": "", 322 | "intervalFactor": 1, 323 | "legendFormat": "Half-Open", 324 | "refId": "B" 325 | }, 326 | { 327 | "expr": "application_ft_org_acme_FrontendResource_listStudents_circuitbreaker_open_total/1000000000", 328 | "format": "time_series", 329 | "interval": "", 330 | "intervalFactor": 1, 331 | "legendFormat": "Open", 332 | "refId": "C" 333 | } 334 | ], 335 | "thresholds": [], 336 | "timeFrom": null, 337 | "timeRegions": [], 338 | "timeShift": null, 339 | "title": "List Students Circuit Breaker", 340 | "tooltip": { 341 | "shared": true, 342 | "sort": 0, 343 | "value_type": "individual" 344 | }, 345 | "type": "graph", 346 | "xaxis": { 347 | "buckets": null, 348 | "mode": "time", 349 | "name": null, 350 | "show": true, 351 | "values": [] 352 | }, 353 | "yaxes": [ 354 | { 355 | "format": "short", 356 | "label": "Seconds", 357 | "logBase": 1, 358 | "max": null, 359 | "min": null, 360 | "show": true 361 | }, 362 | { 363 | "format": "short", 364 | "label": null, 365 | "logBase": 1, 366 | "max": null, 367 | "min": null, 368 | "show": true 369 | } 370 | ], 371 | "yaxis": { 372 | "align": false, 373 | "alignLevel": null 374 | } 375 | } 376 | ], 377 | "refresh": "5s", 378 | "schemaVersion": 16, 379 | "style": "dark", 380 | "tags": [], 381 | "templating": { 382 | "list": [] 383 | }, 384 | "time": { 385 | "from": "now-5m", 386 | "to": "now" 387 | }, 388 | "timepicker": { 389 | "refresh_intervals": [ 390 | "5s", 391 | "10s", 392 | "30s", 393 | "1m", 394 | "5m", 395 | "15m", 396 | "30m", 397 | "1h", 398 | "2h", 399 | "1d" 400 | ], 401 | "time_options": [ 402 | "5m", 403 | "15m", 404 | "1h", 405 | "6h", 406 | "12h", 407 | "24h", 408 | "2d", 409 | "7d", 410 | "30d" 411 | ] 412 | }, 413 | "timezone": "", 414 | "title": "Frontend Dashboard", 415 | "uid": "eD1Tu-LWz", 416 | "version": 1 417 | } -------------------------------------------------------------------------------- /docker/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 1s 3 | evaluation_interval: 1s 4 | 5 | scrape_configs: 6 | #- job_name: prometheus 7 | # static_configs: 8 | # - targets: ['localhost:9090'] 9 | 10 | - job_name: frontend 11 | static_configs: 12 | - targets: ['frontend:8080'] -------------------------------------------------------------------------------- /docs/README.adoc: -------------------------------------------------------------------------------- 1 | = Developing Microservices with MicroProfile using Quarkus 2 | 3 | 4 | :toc: 5 | 6 | include::introduction.adoc[] 7 | 8 | <<< 9 | 10 | include::create-student-service.adoc[] 11 | 12 | <<< 13 | 14 | include::microprofile-config.adoc[] 15 | 16 | <<< 17 | 18 | include::microprofile-rest-client.adoc[] 19 | 20 | <<< 21 | 22 | include::microprofile-fault-tolerance.adoc[] 23 | 24 | <<< 25 | 26 | include::microprofile-metrics.adoc[] 27 | 28 | <<< 29 | 30 | include::microprofile-health.adoc[] 31 | 32 | <<< 33 | 34 | include::microprofile-jwt-rbac.adoc[] 35 | 36 | <<< 37 | 38 | include::microprofile-deploy.adoc[] 39 | 40 | <<< 41 | 42 | include::appendix.adoc[] 43 | 44 | -------------------------------------------------------------------------------- /docs/appendix.adoc: -------------------------------------------------------------------------------- 1 | == Appendix 2 | 3 | === Deploy to docker using native builds of `student` and `frontend`. 4 | 5 | // ********************************************* 6 | 7 | . Stop student and frontend apps running in development mode to avoid port conflicts 8 | + 9 | -- 10 | .Terminal 1 11 | [source/bash] 12 | ---- 13 | # Press CTRL-C to stop Quarkus (student) 14 | ---- 15 | 16 | .Terminal 2 17 | [source/bash] 18 | ---- 19 | # Press CTRL-C to stop Quarkus (frontend) 20 | ---- 21 | -- 22 | . Compile `student` native binary 23 | + 24 | -- 25 | .Terminal 1 26 | ---- 27 | $ cd tutorial/working/student 28 | $ mvn clean package -Dnative -Dquarkus.native.container-build=true -DskipTests 29 | ---- 30 | 31 | NOTE: Native compilation is resource intensive. It may take up to 2+ minutes and utilize a lot of RAM. The Docker VM may need to be set to 4GB+ (Docker icon in menu bar or system tray-> Preferences-> Advanced). 32 | 33 | .Terminal 1 Output 34 | .... 35 | ... 36 | ... 37 | [INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] docker run -v /Users/jclingan/Documents/Personal/OReilly/MicroProfile/solution/solution/student/target/student-1.0-SNAPSHOT-native-image-source-jar:/project:z --rm quay.io/quarkus/ubi-quarkus-native-image:19.2.1 -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=1 --initialize-at-build-time= -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -jar student-1.0-SNAPSHOT-runner.jar -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:+AddAllCharsets -H:EnableURLProtocols=http -H:-JNI --no-server -H:-UseServiceLoaderFeature -H:+StackTrace student-1.0-SNAPSHOT-runner 38 | [student-1.0-SNAPSHOT-runner:23] classlist: 11,461.31 ms 39 | [student-1.0-SNAPSHOT-runner:23] (cap): 1,627.85 ms 40 | [student-1.0-SNAPSHOT-runner:23] setup: 3,978.77 ms 41 | 03:35:11,103 INFO [org.jbo.threads] JBoss Threads version 3.0.0.Final 42 | [student-1.0-SNAPSHOT-runner:23] (typeflow): 21,123.43 ms 43 | [student-1.0-SNAPSHOT-runner:23] (objects): 17,071.98 ms 44 | [student-1.0-SNAPSHOT-runner:23] (features): 472.98 ms 45 | [student-1.0-SNAPSHOT-runner:23] analysis: 40,365.68 ms 46 | [student-1.0-SNAPSHOT-runner:23] (clinit): 719.13 ms 47 | [student-1.0-SNAPSHOT-runner:23] universe: 1,880.23 ms 48 | [student-1.0-SNAPSHOT-runner:23] (parse): 3,293.33 ms 49 | [student-1.0-SNAPSHOT-runner:23] (inline): 7,098.10 ms 50 | [student-1.0-SNAPSHOT-runner:23] (compile): 26,555.96 ms 51 | [student-1.0-SNAPSHOT-runner:23] compile: 38,699.39 ms 52 | [student-1.0-SNAPSHOT-runner:23] image: 2,898.33 ms 53 | [student-1.0-SNAPSHOT-runner:23] write: 635.92 ms 54 | [student-1.0-SNAPSHOT-runner:23] [total]: 100,194.70 ms 55 | [INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 106387ms 56 | [INFO] ------------------------------------------------------------------------ 57 | [INFO] BUILD SUCCESS 58 | [INFO] ------------------------------------------------------------------------ 59 | [INFO] Total time: 01:48 min 60 | [INFO] Finished at: 2020-01-10T19:36:34-08:00 61 | [INFO] ------------------------------------------------------------------------ 62 | .... 63 | -- 64 | 65 | . Compile `frontend` service 66 | + 67 | -- 68 | .Terminal 1 69 | ---- 70 | $ cd tutorial/working/frontend 71 | $ mvn clean package -Dnative -Dquarkus.native.container-build=true -DskipTests 72 | ---- 73 | -- 74 | + 75 | 76 | // ********************************************* 77 | 78 | . Create docker containers 79 | + 80 | -- 81 | .Terminal 1 82 | ---- 83 | $ cd tutorial/working/student 84 | $ docker build -t acme/student:1.0 -f src/main/docker/Dockerfile.native . <1> 85 | $ cd tutorial/working/frontend 86 | $ docker build -t acme/frontend:1.0 -f src/main/docker/Dockerfile.native . <1> 87 | ---- 88 | <1> Note the change to Dockerfile.native 89 | -- 90 | + 91 | // ********************************************* 92 | 93 | . Start student, frontend, prometheus, and grafana. 94 | + 95 | -- 96 | .Terminal 1 97 | [source,bash] 98 | ---- 99 | $ cd tutorial/working/docker 100 | $ docker-compose up 101 | ---- 102 | -- 103 | 104 | <<< 105 | 106 | === Using jwtenizr 107 | 108 | https://github.com/AdamBien/jwtenizr[jwtenizr] is a handy tool created by http://adam-bien.com[Adam Bien] to create MicroProfile-ready tokens. This tutorial includes a `jwt` subdirectory that includes: 109 | 110 | * *jwtenizr.jar*: A conveniently pre-compiled jar file. Feel free to download and compile your own jar file. 111 | * *jwtenizr.sh*: A convient shell script that invokes the jar file 112 | * *Pre-generated artifacts*: Makes explaining this tutorial consistent over time. Includes: 113 | ** *jwt-token.json*: Contains pe-defined token contents. An important aspect to call out is that the `exp` (expiration) claim was added to ensure this token does not expire until Jan 2070. Without this addition, jwtenizr will use an expiration time of 1000 seconds (~16 minutes) from when the token was created. 114 | ** The remainder of the artifacts is documented in the https://github.com/AdamBien/jwtenizr[jwtenizr github]. 115 | 116 | To generate a new token with the default 1000 second expiration time, remove the `exp` line from jwt-token.json, which would then look as follows: 117 | 118 | .jwt-token.json 119 | [source,json] 120 | ---- 121 | { 122 | "iss":"airhacks", 123 | "jti":"airhacks-jwt-unique-id-12342142", 124 | "sub":"user/43971", 125 | "upn":"demo@acme.org", 126 | "myc":"My Custom Claim", 127 | "groups":[ 128 | "user", 129 | "admin" 130 | ] 131 | } 132 | ---- 133 | 134 | ==== Generate a new token with a customized expiration (Mac/Linux) 135 | 136 | . Get the current date 137 | + 138 | -- 139 | .Terminal Window 140 | [source,bash] 141 | ---- 142 | # Get the number of seconds since the Epoch (Jan 1, 1970) and add to it (200 seconds in the following example): 143 | 144 | $ $((`date +%s` + 200)) <1> 145 | 1579412832 <2> 146 | ---- 147 | 148 | <1> The first `$` is the command prompt :-) 149 | <2> The output from the evaluated expression. This is the current time plus 200 seconds. Replace `200` with whatever value you want. 150 | -- 151 | 152 | . Place that number as the expiration claim in `jwt-token.json`: 153 | + 154 | -- 155 | .jwt-token.json 156 | [source,json] 157 | ---- 158 | { 159 | "iss":"airhacks", 160 | "jti":"airhacks-jwt-unique-id-12342142", 161 | "sub":"user/43971", 162 | "exp":1579412832, <1> 163 | "upn":"demo@acme.org", 164 | "myc":"My Custom Claim", 165 | "groups":[ 166 | "user", 167 | "admin" 168 | ] 169 | } 170 | ---- 171 | 172 | <1> Add the expiration claim with the expiration date acquired from the prior step. 173 | -- 174 | 175 | . Re-run the jwtenizer to generate an updated token 176 | + 177 | -- 178 | .Terminal 179 | [source, bash] 180 | ---- 181 | $ cd tutorial/jwt 182 | $ ./jwtenizr.sh 183 | ---- 184 | 185 | NOTE: While running `jwtenizr.sh` will not add the `exp` claim to `jwt-token.json`, copying the generated token from `token.jwt` and pasting it into http://jwt.io[jwt.io] will show the token does include the `exp` claim. 186 | 187 | -- 188 | 189 | . Use the new token in requests to the `frontend` 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /docs/create-student-service.adoc: -------------------------------------------------------------------------------- 1 | == Create Student Service 2 | 3 | Create a simple Quarkus project as a starting point - the "student" service. A default RESTful endpoint is generated. While this tutorial utilizes the mvn command line tooling, projects can also be generated at http://code.quarkus.io[code.quarkus.io] using a Web UI. 4 | 5 | . Go to the project's base directory and create project using maven 6 | + 7 | -- 8 | 9 | .Terminal 1 10 | [source,bash] 11 | ---- 12 | $ cd tutorial/working 13 | $ mvn io.quarkus:quarkus-maven-plugin:1.3.0.Final:create \ 14 | -DprojectGroupId=org.acme \ 15 | -DprojectArtifactId=student \ 16 | -DclassName="org.acme.StudentResource" \ 17 | -Dpath="/student" \ 18 | -Dextensions="resteasy-jsonb,smallrye-health" 19 | ---- 20 | -- 21 | + 22 | // ********************************************* 23 | ''' 24 | 25 | . Optional: To see a list of available extensions, type: 26 | 27 | + 28 | -- 29 | 30 | .Terminal 1 31 | [source,bash] 32 | ---- 33 | $ mvn quarkus:list-extensions 34 | ---- 35 | -- 36 | + 37 | // ********************************************* 38 | ''' 39 | 40 | . Open project in your favorite IDE 41 | .. Open StudentResource.java and peruse the JAX-RS source code 42 | 43 | + 44 | -- 45 | .StudentResource.java 46 | [source,java] 47 | ---- 48 | package org.acme; 49 | 50 | import javax.ws.rs.GET; 51 | import javax.ws.rs.Path; 52 | import javax.ws.rs.Produces; 53 | import javax.ws.rs.core.MediaType; 54 | 55 | @Path("/student") 56 | public class StudentResource { 57 | 58 | @GET 59 | @Produces(MediaType.TEXT_PLAIN) 60 | public String hello() { 61 | return "hello"; 62 | } 63 | } 64 | ---- 65 | 66 | NOTE: Quarkus does not require a JAX-RS Application class, but will use it if provided. In addition, Quarkus RESTful resources are singletons (`@javax.inject.Singleton`). These are developer convenience features. These points are covered in the https://quarkus.io/guides/getting-started#the-jax-rs-resources[Quarkus Getting Started Guide]. 67 | -- 68 | 69 | + 70 | // ********************************************* 71 | ''' 72 | 73 | . Start Quarkus in developer mode 74 | 75 | + 76 | -- 77 | .Terminal 1 78 | [source,bash] 79 | ---- 80 | $ mvn compile quarkus:dev 81 | ---- 82 | -- 83 | + 84 | // ********************************************* 85 | ''' 86 | 87 | . Check that the endpoint returns "hello" 88 | 89 | + 90 | -- 91 | .Terminal 2 92 | [source,bash] 93 | ---- 94 | $ curl -i localhost:8080/student 95 | ---- 96 | .Output 97 | .... 98 | HTTP/1.1 200 OK 99 | Content-Length: 5 100 | Content-Type: text/plain;charset=UTF-8 101 | 102 | hello 103 | .... 104 | -- 105 | + 106 | // ********************************************* 107 | ''' 108 | 109 | . Try out live reload. In the StudentResource.java hello() method, replace "Hello" with "Howdy" and save the file 110 | + 111 | .StudentResource.java 112 | [source,java] 113 | ---- 114 | public class StudentResource { 115 | 116 | @GET 117 | @Produces(MediaType.TEXT_PLAIN) 118 | public String hello() { 119 | return "Howdy"; <1> 120 | } 121 | } 122 | ---- 123 | <1> Replace "Hello" with "Howdy" 124 | 125 | + 126 | 127 | .Terminal 2 128 | [source,bash] 129 | ---- 130 | $ curl -i localhost:8080/student 131 | ---- 132 | + 133 | .Terminal 2 Output 134 | .... 135 | HTTP/1.1 200 OK 136 | Content-Length: 5 137 | Content-Type: text/plain;charset=UTF-8 138 | 139 | Howdy 140 | .... 141 | 142 | + 143 | // ********************************************* 144 | ''' 145 | 146 | . In StudentResource.java, create a list of Strings: 147 | + 148 | -- 149 | .StudentResource.java 150 | [source,java] 151 | ---- 152 | @Path("/student") 153 | public class StudentResource { 154 | List students = new ArrayList<>(); <1> 155 | ---- 156 | <1> Add this line 157 | -- 158 | + 159 | // ********************************************* 160 | ''' 161 | 162 | . Add a method called `listStudents()` at the "/list" path that returns the students as a JSON array 163 | + 164 | -- 165 | .StudentResource.java 166 | [source,java] 167 | ---- 168 | @GET 169 | @Path("/list") 170 | @Produces(MediaType.APPLICATION_JSON) 171 | public List listStudents() { 172 | return students; 173 | } 174 | ---- 175 | -- 176 | + 177 | // ********************************************* 178 | ''' 179 | 180 | . Check the output 181 | + 182 | .Terminal 2 183 | -- 184 | [source,bash] 185 | ---- 186 | $ curl -i localhost:8080/student/list 187 | ---- 188 | -- 189 | 190 | + 191 | -- 192 | 193 | .Terminal 2 Output 194 | .... 195 | HTTP/1.1 200 OK 196 | Content-Length: 2 197 | Content-Type: application/json 198 | 199 | [] 200 | .... 201 | -- 202 | -------------------------------------------------------------------------------- /docs/images/Click_add_Datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/docs/images/Click_add_Datasource.png -------------------------------------------------------------------------------- /docs/images/Configure_Datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/docs/images/Configure_Datasource.png -------------------------------------------------------------------------------- /docs/images/Display_Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/docs/images/Display_Dashboard.png -------------------------------------------------------------------------------- /docs/images/Filter-Prometheus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/docs/images/Filter-Prometheus.png -------------------------------------------------------------------------------- /docs/images/Grafana_Login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/docs/images/Grafana_Login.png -------------------------------------------------------------------------------- /docs/images/Import_Json_File.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/docs/images/Import_Json_File.png -------------------------------------------------------------------------------- /docs/images/Select_Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/docs/images/Select_Dashboard.png -------------------------------------------------------------------------------- /docs/introduction.adoc: -------------------------------------------------------------------------------- 1 | == Introduction 2 | 3 | This tutorial builds two Java services from scratch that incrementally add Eclipse MicroProfile APIs. The first is a `student` service that returns a list of students, which is invoke by a `frontend` service. This domain model is simple so the focus is spent on the MicroProfile APIs themselves. 4 | 5 | The documented steps in this tutorial follow a pattern: 6 | 7 | . Write code 8 | . Run a command to test code (curl) 9 | . Check output 10 | 11 | This pattern flows quickly due to Quarkus' Live Coding feature, and makes checking code changes near-instantaneous so each change can be immediately evaluated. 12 | 13 | NOTE: This tutorial will require three terminal windows. Each command block will display the terminal where the command is to be run - _Terminal 1_, _Terminal 2_, or _Terminal 3_. As the tutorial progresses, it helps to organize the terminals on your screen so they can all be viewed at the same time. In addition, each code block will show the name of the file to be edited (ex: "StudentResource.java") 14 | 15 | == Minimum Requirements 16 | 17 | . Java 8, 11, 12, 13, or 14 18 | . Native compilation - GraalVM 19.3.1 or 20.0.0 19 | .. Assign at least 4GB RAM and 2 vcpu to your Docker VM 20 | .. ... or be very, very patient 21 | . Maven 3.6.2+ 22 | 23 | == Setup Local Repository 24 | 25 | . Clone the project to your local system 26 | + 27 | -- 28 | .Terminal 1 29 | ---- 30 | $ git clone \ 31 | https://github.com/jclingan/oreilly-microprofile-tutorial.git \ 32 | tutorial 33 | ---- 34 | .Terminal 1 Output 35 | .... 36 | Cloning into 'tutorial'... 37 | remote: Enumerating objects: 130, done. 38 | remote: Counting objects: 100% (130/130), done. 39 | remote: Compressing objects: 100% (92/92), done. 40 | remote: Total 130 (delta 29), reused 120 (delta 22), pack-reused 0 41 | Receiving objects: 100% (130/130), 2.50 MiB | 644.00 KiB/s, done. 42 | Resolving deltas: 100% (29/29), done. 43 | .... 44 | -- 45 | -------------------------------------------------------------------------------- /docs/microprofile-config.adoc: -------------------------------------------------------------------------------- 1 | == MicroProfile Config 2 | 3 | This section covers the MicroProfile CDI injection-based API for externalizing configuration. In these instructions, configuration parameters are stored in `src/main/resources/META-INF/microprofile-config.properties`. 4 | 5 | NOTE: While Quarkus supports MicroProfile APIs, it also supports much more than MicroProfile like Spring and Vert.x APIs. For that reason, https://quarkus.io/guides/[the Quarkus guides] refer to the more framework-agnostic `src/main/resources/application.properties`. Because this tutorial focuses on MicroProfile, it will use `src/main/resources/META-INF/microprofile-config.properties`. MicroProfile supports both, and property values defined in `application.properties` take precedence over values defined in `microprofile-config.properties`. 6 | 7 | . Create a `doDelay()` method to delay 3000 milliseconds and print "Waiting 3000 milliseconds" to stdout. 8 | + 9 | -- 10 | .StudentResource.java 11 | [source,java] 12 | ---- 13 | void doDelay() { 14 | int delayTime; 15 | try { 16 | delayTime=3000; 17 | System.out.println("** Waiting " + delayTime + "ms **"); 18 | TimeUnit.MILLISECONDS.sleep(delayTime); 19 | } catch (InterruptedException e) { 20 | e.printStackTrace(); 21 | } 22 | } 23 | ---- 24 | -- 25 | + 26 | // ********************************************* 27 | ''' 28 | 29 | . In `listStudents()`, call `doDelay()` 30 | + 31 | -- 32 | .StudentResource.java 33 | [source,java] 34 | ---- 35 | @GET 36 | @Path("/list") 37 | @Produces(MediaType.APPLICATION_JSON) 38 | public List listStudents() { 39 | doDelay(); // <1> 40 | return students; 41 | } 42 | ---- 43 | <1> Insert `doDelay()` call 44 | -- 45 | + 46 | // ********************************************* 47 | ''' 48 | 49 | . Check the endpoint, which should take longer to complete 50 | + 51 | -- 52 | .Terminal 2 53 | [source, bash] 54 | ---- 55 | $ curl -i http://localhost:8080/student/list 56 | ---- 57 | .Terminal 2 Output (after 3 seconds) 58 | .... 59 | HTTP/1.1 200 OK 60 | Content-Length: 2 61 | Content-Type: application/json 62 | 63 | [] 64 | .... 65 | 66 | .Terminal 1 Output (after 3 seconds) 67 | .... 68 | ** Waiting 3000ms ** <1> 69 | .... 70 | 71 | <1> Output from `doDelay()` 72 | -- 73 | + 74 | // ********************************************* 75 | ''' 76 | 77 | . Inject `delay` property value into variable `delay` 78 | + 79 | -- 80 | .StudentResource.java 81 | [source,java] 82 | ---- 83 | @Inject 84 | @ConfigProperty(name="delay") 85 | int delay; 86 | ---- 87 | -- 88 | + 89 | // ********************************************* 90 | ''' 91 | 92 | . In `doDelay()`, replace hard-coded "3000" with the `delay` variable 93 | + 94 | -- 95 | .StudentResource.java 96 | [source,java] 97 | ---- 98 | void doDelay() { 99 | int delayTime; 100 | try { 101 | delayTime=delay; // <1> 102 | System.out.println("** Waiting " + delayTime + "ms **"); 103 | TimeUnit.MILLISECONDS.sleep(delayTime); 104 | } catch (InterruptedException e) { 105 | e.printStackTrace(); 106 | } 107 | } 108 | ---- 109 | <1> Replace `3000` with `delay`, as shown 110 | -- 111 | // ********************************************* 112 | ''' 113 | 114 | . Verify an error is generated because the `delay` property has not been defined. 115 | + 116 | -- 117 | .Terminal 2 118 | [source, bash] 119 | ---- 120 | $ curl -i http://localhost:8080/student/list 121 | ---- 122 | 123 | .Terminal 2 Output (Stack Trace) 124 | .... 125 | Caused by: javax.enterprise.inject.spi.DeploymentException: No config value of type [int] exists for: delay 126 | .... 127 | -- 128 | + 129 | // ********************************************* 130 | ''' 131 | 132 | . Define `delay` property in `src/main/resources/META-INF/microprofile-config.properties`: 133 | + 134 | -- 135 | .microprofile-config.properties 136 | [source, property] 137 | ---- 138 | # Configuration file 139 | # key = value 140 | 141 | delay=2500 <1> 142 | ---- 143 | <1> Add this line 144 | -- 145 | + 146 | // ********************************************* 147 | ''' 148 | 149 | . Verify `delay` property is read. 150 | + 151 | -- 152 | .Terminal 2 153 | [source, bash] 154 | ---- 155 | $ curl -i http://localhost:8080/student/list 156 | ---- 157 | .Terminal 2 Output (after 2.5 seconds) 158 | .... 159 | HTTP/1.1 200 OK 160 | Content-Length: 2 161 | Content-Type: application/json 162 | 163 | [] 164 | .... 165 | 166 | .Terminal 1 Output 167 | .... 168 | ** Waiting 2500ms ** 169 | .... 170 | -- 171 | + 172 | // ********************************************* 173 | ''' 174 | 175 | . Comment out `delay` in `microprofile-config.properties` 176 | + 177 | -- 178 | .microprofile-config.properties 179 | [source,properties] 180 | ---- 181 | # Configuration file 182 | # key = value 183 | 184 | #delay=2500 <1> 185 | ---- 186 | <1> Comment out `delay` 187 | 188 | -- 189 | + 190 | // ********************************************* 191 | ''' 192 | 193 | 194 | . Update the `@ConfigProperty` annotation with a default value of 2000. 195 | + 196 | -- 197 | .StudentResource.java 198 | [source,java] 199 | ---- 200 | @Inject 201 | @ConfigProperty(name="delay", defaultValue="2000") <1> 202 | int delay; 203 | ---- 204 | <1> Insert `defaultValue=2000` 205 | -- 206 | + 207 | // ********************************************* 208 | ''' 209 | 210 | . Verify `defaultValue` is read. 211 | + 212 | -- 213 | .Terminal 2 214 | [source,bash] 215 | ---- 216 | $ curl -i http://localhost:8080/student/list 217 | ---- 218 | .Terminal 2 Output (after 2 seconds) 219 | .... 220 | HTTP/1.1 200 OK 221 | Content-Length: 2 222 | Content-Type: application/json 223 | 224 | [] 225 | .... 226 | 227 | .Terminal 1 Output 228 | .... 229 | ** Waiting 2000ms ** 230 | .... 231 | -- 232 | + 233 | // ********************************************* 234 | ''' 235 | 236 | . Stop running Quarkus process. 237 | + 238 | -- 239 | .Terminal 1 240 | [source, bash] 241 | ---- 242 | # Press CTRL-C to stop Quarkus 243 | ---- 244 | -- 245 | + 246 | // ********************************************* 247 | ''' 248 | 249 | . Define `DELAY` environmental variable 250 | + 251 | -- 252 | .Terminal 1 253 | [source, bash] 254 | ---- 255 | export DELAY=4000 256 | ---- 257 | -- 258 | + 259 | // ********************************************* 260 | ''' 261 | 262 | . Restart Quarkus. 263 | + 264 | -- 265 | .Terminal 1 266 | [source,bash] 267 | ---- 268 | $ mvn compile quarkus:dev 269 | ---- 270 | -- 271 | + 272 | // ********************************************* 273 | ''' 274 | 275 | . Verify the `DELAY` environment variable overrides the value in the property file. 276 | 277 | + 278 | -- 279 | 280 | .Terminal 2 281 | [source,bash] 282 | ---- 283 | $ curl -i http://localhost:8080/student/list 284 | ---- 285 | .Terminal 2 Output (after 4 seconds) 286 | .... 287 | HTTP/1.1 200 OK 288 | Content-Length: 2 289 | Content-Type: application/json 290 | 291 | [] 292 | .... 293 | 294 | .Terminal 1 Output 295 | .... 296 | ** Waiting 4000ms ** 297 | .... 298 | -- 299 | + 300 | // ********************************************* 301 | ''' 302 | 303 | . Stop Quarkus 304 | + 305 | -- 306 | .Terminal 1 307 | [source, bash] 308 | ---- 309 | # Press CTRL-C to stop Quarkus 310 | ---- 311 | -- 312 | + 313 | // ********************************************* 314 | ''' 315 | 316 | . Re-start Quarkus and define system property via CLI. 317 | + 318 | -- 319 | .Terminal 1 320 | [source, bash] 321 | ---- 322 | $ mvn compile quarkus:dev -Ddelay=5000 323 | ---- 324 | -- 325 | + 326 | // ********************************************* 327 | ''' 328 | 329 | . Verify the `DELAY` system property overrides the value in the property file. In _Terminal 2_, type 330 | + 331 | -- 332 | .Terminal 2 333 | [source, bash] 334 | ---- 335 | $ curl -i http://localhost:8080/student/list 336 | ---- 337 | .Terminal 2 Output (after 5 seconds) 338 | .... 339 | HTTP/1.1 200 OK 340 | Content-Length: 2 341 | Content-Type: application/json 342 | 343 | [] 344 | .... 345 | 346 | .Terminal 1 Output 347 | .... 348 | ** Waiting 5000ms ** 349 | .... 350 | -- 351 | + 352 | // ********************************************* 353 | ''' 354 | 355 | . Clean up by stopping Quarkus and unsetting DELAY environment variable 356 | + 357 | -- 358 | .Terminal 1 359 | [source, bash] 360 | ---- 361 | # *** First, press CTRL-C to stop Quarkus *** 362 | # Next, remove DELAY environment variable 363 | unset DELAY 364 | ---- 365 | -- 366 | + 367 | // ********************************************* 368 | ''' 369 | 370 | . Change Quarkus HTTP port to 8082. Update microprofile-config.properties to look as follows: 371 | + 372 | -- 373 | .microprofile-config.properties 374 | [source, property] 375 | ---- 376 | #delay=2500 377 | quarkus.http.port=8082 // <1> 378 | ---- 379 | <1> Insert `quarkus.http.port` property 380 | -- 381 | + 382 | // ********************************************* 383 | ''' 384 | 385 | . Restart Quarkus without defining `delay` system property and change debug port. 386 | + 387 | -- 388 | .Terminal 1 389 | [source, bash] 390 | ---- 391 | $ mvn compile quarkus:dev -Ddebug=5006 392 | ---- 393 | -- 394 | + 395 | // ********************************************* 396 | ''' 397 | 398 | . Verify updated property 399 | + 400 | -- 401 | .Terminal 2 402 | [source,property] 403 | ---- 404 | # Note the port change to 8082! 405 | $ curl -i http://localhost:8082/student/list 406 | ---- 407 | 408 | .Terminal 2 Output (after 2 seconds) 409 | .... 410 | HTTP/1.1 200 OK 411 | Content-Length: 2 412 | Content-Type: application/json 413 | 414 | [] 415 | .... 416 | 417 | .Terminal 1 Output 418 | .... 419 | ** Waiting 2000ms ** 420 | .... 421 | -- 422 | + 423 | // ********************************************* 424 | ''' 425 | 426 | . In MicroProfile Config, comma-separated properties can be read as a `List`. Add the following to `microprofile-config.properties` to initialize the student list: 427 | + 428 | -- 429 | .microprofile-config.properties 430 | [source] 431 | ---- 432 | students=Duke,John,Jane,Arun,Christina 433 | ---- 434 | -- 435 | + 436 | // ********************************************* 437 | ''' 438 | 439 | . Inject students into student list. Change `List` students to: 440 | + 441 | -- 442 | .StudentResource.java 443 | [source,java] 444 | ---- 445 | @Inject // <1> 446 | @ConfigProperty(name = "students") 447 | List students = new ArrayList<>(); 448 | ---- 449 | <1> Add @Inject and @ConfigProperty annotations 450 | -- 451 | + 452 | // ********************************************* 453 | ''' 454 | . Verify that the students have been injected. 455 | + 456 | -- 457 | .Terminal 1 458 | [source, bash] 459 | ---- 460 | $ curl -i http://localhost:8082/student/list 461 | ---- 462 | .Terminal 2 Output (after 2 seconds) 463 | .... 464 | HTTP/1.1 200 OK 465 | Content-Length: 41 466 | Content-Type: application/json 467 | 468 | ["Duke","John","Jane","Arun","Christina"] 469 | .... 470 | 471 | .Terminal 1 Output 472 | .... 473 | ** Waiting 2000ms ** 474 | .... 475 | -- 476 | -------------------------------------------------------------------------------- /docs/microprofile-deploy.adoc: -------------------------------------------------------------------------------- 1 | == Packaging, deploying, and monitoring 2 | 3 | Build applications as a thin jar files, package them as docker files and start them. Once running, view health endpoint state and view application metrics in grafana. 4 | 5 | . Update student URI for docker deployment. 6 | + 7 | -- 8 | .frontend/src/main/resources/META-INF/microprofile-config.properties 9 | [source,properties] 10 | ---- 11 | #StudentService/mp-rest/uri=http://localhost:8082 <1> 12 | %dev.StudentService/mp-rest/uri=http://localhost:8082 <2> 13 | %prod.StudentService/mp-rest/uri=http://student:8082 <3> 14 | ---- 15 | <1> Comment out current uri 16 | <2> Properties prefixed with `%dev.` will be used while in development mode only (`mvn compile quarkus:dev`). 17 | <3> Properties prefixed with `%prod.` will be used when running in native mode or with `java -jar` only. When running via the docker-compose, the host will be "student". 18 | 19 | NOTE: Quarkus supports multiple configuration profiles. There is also a `%test.` profile when running tests. Properties with no profile defined are always utilized, as has been the case so far in this lab. 20 | -- 21 | // ********************************************* 22 | ''' 23 | 24 | . Package applications as docker files 25 | + 26 | -- 27 | 28 | WARNING: Make sure docker daemon is running and is accessible (ex: `docker info` shows proper results) 29 | 30 | .Package student as thin jar and create a docker image 31 | 32 | [source,bash] 33 | ---- 34 | $ cd tutorial/working/student 35 | $ mvn clean package -DskipTests 36 | $ docker build -t acme/student:1.0 -f src/main/docker/Dockerfile.jvm . 37 | ---- 38 | 39 | .Package frontend as thin jar and create a docker image 40 | ---- 41 | $ cd tutorial/working/frontend 42 | $ mvn clean package -DskipTests 43 | $ docker build -t acme/frontend:1.0 -f src/main/docker/Dockerfile.jvm . 44 | ---- 45 | -- 46 | + 47 | // ********************************************* 48 | ''' 49 | 50 | . Stop student and frontend apps running in development mode to avoid port conflicts 51 | + 52 | -- 53 | .Terminal 1 54 | [source/bash] 55 | ---- 56 | # Press CTRL-C to stop Quarkus (student) 57 | ---- 58 | 59 | .Terminal 2 60 | [source/bash] 61 | ---- 62 | # Press CTRL-C to stop Quarkus (frontend) 63 | ---- 64 | -- 65 | + 66 | // ********************************************* 67 | ''' 68 | 69 | . Start student, frontend, prometheus, and grafana. 70 | + 71 | -- 72 | .Terminal 1 73 | [source,bash] 74 | ---- 75 | $ cd tutorial/working/docker 76 | $ docker-compose up 77 | ---- 78 | -- 79 | + 80 | // ********************************************* 81 | ''' 82 | 83 | . View student service health 84 | + 85 | -- 86 | 87 | .Terminal 2 88 | ---- 89 | $ docker-compose ps 90 | ---- 91 | .Terminal 2 Output 92 | .... 93 | Name Command State Ports 94 | ------------------------------------------------------------- 95 | docker_fronte /deployments/ Up 0.0.0.0:8080- 96 | nd_1 run-java.sh >8080/tcp, 97 | 8778/tcp, 98 | 9779/tcp 99 | docker_grafan /run.sh Up 0.0.0.0:3000- 100 | a_1 >3000/tcp 101 | docker_prom_1 /bin/promethe Up 0.0.0.0:9090- 102 | us --config.f >9090/tcp 103 | docker_studen /deployments/ Up 8080/tcp, 0.0 104 | t_1 run-java.sh (unhealthy) .0.0:8082->80 <1> 105 | 81/tcp, 106 | 8778/tcp, 107 | 9779/tcp 108 | .... 109 | 110 | <1> Run `docker-compose ps` until both `(healthy)` and `(unhealthy)` are displayed. In container orchestration environment, these pods containers would be restarted. 111 | -- 112 | + 113 | // ********************************************* 114 | ''' 115 | 116 | . Get Prometheus IP address 117 | + 118 | -- 119 | .Terminal 2 120 | [source, bash] 121 | ---- 122 | $ docker inspect docker_prom_1 123 | ---- 124 | 125 | .Terminal 2 Output 126 | .... 127 | ... 128 | ... 129 | "NetworkSettings": { 130 | "Bridge": "", 131 | "SandboxID": "acf7dc6b9591f7992fb3053639a38cc98f91281e98582b8a8a420026506d88b8", 132 | "HairpinMode": false, 133 | "LinkLocalIPv6Address": "", 134 | "LinkLocalIPv6PrefixLen": 0, 135 | "Ports": { 136 | "9090/tcp": [ 137 | { 138 | "HostIp": "0.0.0.0", 139 | "HostPort": "9090" 140 | } 141 | ] 142 | }, 143 | "SandboxKey": "/var/run/docker/netns/acf7dc6b9591", 144 | "SecondaryIPAddresses": null, 145 | "SecondaryIPv6Addresses": null, 146 | "EndpointID": "", 147 | "Gateway": "", 148 | "GlobalIPv6Address": "", 149 | "GlobalIPv6PrefixLen": 0, 150 | "IPAddress": "", 151 | "IPPrefixLen": 0, 152 | "IPv6Gateway": "", 153 | "MacAddress": "", 154 | "Networks": { 155 | "docker_default": { 156 | "IPAMConfig": null, 157 | "Links": null, 158 | "Aliases": [ 159 | "prom", 160 | "ea79e602a5e5" 161 | ], 162 | "NetworkID": "b4e117d0c94abe2a49a94164883405a8140acca16a1bc376f865b0dca5b839cc", 163 | "EndpointID": "bd1307800d42bb303d79d2ac8cf7bf856d84c8b84d7d4077b112822c8fb711e6", 164 | "Gateway": "172.18.0.1", 165 | "IPAddress": "172.18.0.4", <1> 166 | "IPPrefixLen": 16, 167 | "IPv6Gateway": "", 168 | "GlobalIPv6Address": "", 169 | "GlobalIPv6PrefixLen": 0, 170 | "MacAddress": "02:42:ac:12:00:04", 171 | "DriverOpts": null 172 | } 173 | } 174 | } 175 | } 176 | ] 177 | ... 178 | ... 179 | .... 180 | <1> IP address iis 172.18.0.4. IP address may vary. 181 | 182 | NOTE: `docker inspect docker_prom_1 | grep IPAddress` should also make the IP address quickly apparent. 183 | -- 184 | // ********************************************* 185 | ''' 186 | 187 | . Log in to Grafana 188 | .. Point browser to http://localhost:3000/login. 189 | 190 | + 191 | -- 192 | .user:admin, password:admin 193 | image::images/Grafana_Login.png[Grafana-Login,400,250] 194 | -- 195 | // ********************************************* 196 | ''' 197 | 198 | . Add a data source 199 | + 200 | -- 201 | .Click "Add datasource` 202 | image::images/Click_add_Datasource.png[Add-Datasource,600,100] 203 | -- 204 | // ********************************************* 205 | ''' 206 | 207 | . Filter and select Prometheus 208 | + 209 | -- 210 | .Filter by Prometheus and click Prometheus 211 | image::images/Filter-Prometheus.png[Filter Prometheus,500,300] 212 | -- 213 | + 214 | // ********************************************* 215 | ''' 216 | 217 | . Configure Prometheus Data Source 218 | + 219 | -- 220 | *Use the IP address retrieved with `docker inspect` command above* 221 | 222 | .Configure URL using IP Address and save & test it 223 | image::images/Configure_Datasource.png[Configure Datasource,400,300] 224 | -- 225 | + 226 | // ********************************************* 227 | ''' 228 | 229 | . Import JSON File 230 | + 231 | -- 232 | .Import JSON File 233 | image::images/Import_Json_File.png[Grafana-Login,400,300] 234 | -- 235 | + 236 | // ********************************************* 237 | ''' 238 | 239 | . Select Dashboard - tutorial/working/docker/grafana-frontend-dashboard.json 240 | + 241 | -- 242 | .Select 243 | image::images/Select_Dashboard.png[Grafana-Login,400,300] 244 | -- 245 | + 246 | // ********************************************* 247 | ''' 248 | 249 | . Generate load by running curl a random number of times 250 | + 251 | -- 252 | .Terminal 2 253 | ---- 254 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 255 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 256 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 257 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 258 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 259 | ---- 260 | --image::http://localhost:3000/public/img/grafana_icon.svg[] 261 | image::http://localhost:3000/public/img/grafana_typelogo.svg[] 262 | + 263 | // ********************************************* 264 | ''' 265 | 266 | . Stop the student service 267 | + 268 | -- 269 | .Terminal 2 270 | ---- 271 | $ docker-compose stop student 272 | ---- 273 | -- 274 | + 275 | // ********************************************* 276 | ''' 277 | 278 | . Generate load by running curl a random number of times with the circuit breaker in an open state. 279 | + 280 | -- 281 | .Terminal 2 282 | ---- 283 | $ curl -i localhost:8080/student/list 284 | $ curl -i localhost:8080/student/list 285 | ---- 286 | -- 287 | + 288 | // ********************************************* 289 | ''' 290 | 291 | . View the Grafana dashboard 292 | + 293 | .View Grafana Dashboard 294 | image::images/Display_Dashboard.png[Grafana-Refresh,600,450] 295 | + 296 | -- 297 | Some interesting notes on the dashboard: 298 | 299 | * During metrics gathering, the goal was to stop and start the student service to force some circuit breaker time in the half-open state (yellow line in lower-right hand graph). Relative to the other states, a small amount of time is spent in half-open state (due to small window [`requestVolumeThreshold`] and small `successThreshold`). 300 | * Because of time spent with the student service stopped, there is growth in fallback calls 301 | * The lower-left hand graph uses the MicroProfile Metrics default metric name being graphed. The other graphs uses custom names defined in the dashboard itself 302 | * The proportionally large mean time spent in `listStudents()` (roughly 10 seconds) is due to the number or retries combined with the delay between requests - `@Retry(maxRetries = 4, delay = 1000)` 303 | * While not implemented in this tutorial, these metrics could easily be business-oriented metrics, like 'show the average number of students retrieved per course' to display a live statistic related to class size. 304 | -- 305 | -------------------------------------------------------------------------------- /docs/microprofile-fault-tolerance.adoc: -------------------------------------------------------------------------------- 1 | == MicroProfile Fault Tolerance 2 | 3 | This section will utilize fault tolerance patterns in the frontend service to handle problematic conditions caused by the student service. 4 | 5 | . Add a timeout to `FrontendResource.listStudents()` 6 | + 7 | -- 8 | .FrontendResource.java 9 | [source,java] 10 | ---- 11 | @Timeout <1> 12 | @GET 13 | @Path("/list") 14 | @Produces(MediaType.APPLICATION_JSON) 15 | public List listStudents() { 16 | List students = student.listStudents(); 17 | 18 | return students; 19 | } 20 | ---- 21 | <1> Add `@Timeout` annotation, which defaults to 1000ms 22 | -- 23 | + 24 | // ********************************************* 25 | ''' 26 | . Check endpoint. Verify `org.eclipse.microprofile.faultolerance.exceptions.TimeoutException` is thrown. 27 | + 28 | -- 29 | .Terminal 3 30 | [source,bash] 31 | ---- 32 | $ curl -i localhost:8080/frontend/list 33 | ---- 34 | .Terminal 3 Output 35 | .... 36 | # Stack trace ... 37 | org.jboss.resteasy.spi.UnhandledException: org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException: Timeout[org.acme.FrontendResource#listStudents] timed out 38 | # Stack trace ... 39 | .... 40 | .Terminal 2 Output 41 | .... 42 | # Stack trace ... 43 | org.jboss.resteasy.spi.UnhandledException: org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException: Timeout[org.acme.FrontendResource#listStudents] timed out 44 | # Stack trace ... 45 | .... 46 | 47 | .Terminal 1 Output 48 | .... 49 | ** Waiting 2000ms ** 50 | .... 51 | -- 52 | + 53 | // ********************************************* 54 | ''' 55 | <<< 56 | 57 | . Add a src/main/java/org/acme/ListStudentsFallbackHandler.java fallback class to provide alternative logic when an exception is thrown 58 | + 59 | -- 60 | .ListStudentsFallbackHandler.java 61 | [source,java] 62 | ---- 63 | package org.acme; 64 | 65 | import java.util.Arrays; 66 | import java.util.List; 67 | import javax.inject.Inject; 68 | import org.eclipse.microprofile.faulttolerance.ExecutionContext; 69 | import org.eclipse.microprofile.faulttolerance.FallbackHandler; 70 | import org.eclipse.microprofile.metrics.MetricRegistry; 71 | import org.eclipse.microprofile.metrics.MetricRegistry.Type; 72 | import org.eclipse.microprofile.metrics.annotation.RegistryType; 73 | 74 | /* 75 | A fallback handler has access to the ExecutionContext, which can 76 | be used to determine where the failure originated (class and method) 77 | and the cause of the failure 78 | Print the cause of the failure to stdout, and return a list of "top students" 79 | */ 80 | public class ListStudentsFallbackHandler implements FallbackHandler> { 81 | @Inject 82 | @RegistryType(type = Type.APPLICATION) 83 | MetricRegistry registry; 84 | 85 | @Override 86 | public List handle(ExecutionContext ctx) { 87 | List students = Arrays.asList("Smart Sam", 88 | "Genius Gabby", 89 | "AStudent Angie", 90 | "Intelligent Irene"); 91 | String failure; 92 | registry.counter("listStudentsCounter").inc(); 93 | if (ctx.getFailure() == null) { 94 | failure = "unknown"; 95 | } else { 96 | failure = ctx.getFailure().getMessage(); 97 | } 98 | System.out.println("Exception " + failure); 99 | System.out.println("listStudentsFallbackCounter: " 100 | + registry.counter( 101 | "ft.org.acme.FrontendResource.listStudents.fallback.calls.total") 102 | .getCount()); 103 | return students; 104 | } 105 | } 106 | ---- 107 | -- 108 | + 109 | // ********************************************* 110 | ''' 111 | 112 | . Call the fallback method to provide alternative logic when an exception is thrown 113 | + 114 | -- 115 | .FrontendResource.java 116 | [source,java] 117 | ---- 118 | @Fallback(value = ListStudentsFallbackHandler.class) <1> 119 | @Timeout 120 | @GET 121 | @Path("/list") 122 | @Produces(MediaType.APPLICATION_JSON) 123 | public List listStudents() { 124 | List students; 125 | 126 | students = student.listStudents(); 127 | 128 | return students; 129 | } 130 | ---- 131 | <1> Add `@Fallback` annotation 132 | -- 133 | + 134 | // ********************************************* 135 | ''' 136 | . Check endpoint. Verify the fallback student list is retrieved 137 | + 138 | -- 139 | .Terminal 3 140 | [source,bash] 141 | ---- 142 | $ curl -i localhost:8080/frontend/list 143 | ---- 144 | 145 | .Terminal 2 Output 146 | .... 147 | Exception Timeout[org.acme.FrontendResource#listStudents] timed out 148 | listStudentsFallbackCounter: 1 149 | .... 150 | 151 | .Terminal 3 Output 152 | .... 153 | HTTP/1.1 200 OK 154 | Content-Length: 66 155 | Content-Type: application/json 156 | 157 | ["Smart Sam","Genius Gabby","A-Student Angie","Intelligent Irene"] 158 | .... 159 | 160 | .Terminal 1 Output 161 | .... 162 | ** Waiting 2000ms ** 163 | .... 164 | -- 165 | + 166 | // ********************************************* 167 | ''' 168 | 169 | . Disable all fault tolerance annotations (except `@Fallback`). Useful for when running in a service mesh (e.g. Istio) environment. Commenting out any one of the timeout-disabling properties will disable the timeout. 170 | + 171 | -- 172 | .frontend microprofile-config.properties 173 | [source,properties] 174 | ---- 175 | # Disable fault tolerance globally 176 | MP_Fault_Tolerance_NonFallback_Enabled=false <1> 177 | 178 | # Disable group policy: 179 | #Timeout/enabled=false 180 | 181 | # Disable a specific fault tolerance policy. Ex: 182 | #org.acme.FrontendResource/listStudents/Timeout/enabled=false 183 | ---- 184 | <1> All fault tolerance annotations disabled because this annotation is not commented out 185 | -- 186 | + 187 | // ********************************************* 188 | ''' 189 | 190 | . Check that original list of students is returned 191 | + 192 | -- 193 | .Terminal 3 194 | [source,bash] 195 | ---- 196 | $ curl -i localhost:8080/frontend/list 197 | ---- 198 | 199 | .Terminal 3 Output 200 | .... 201 | HTTP/1.1 200 OK 202 | Content-Length: 41 203 | Content-Type: application/json 204 | 205 | ["Duke","John","Jane","Arun","Christina"] 206 | .... 207 | 208 | .Terminal 1 Output 209 | .... 210 | ** Waiting 2000ms ** 211 | .... 212 | -- 213 | + 214 | // ********************************************* 215 | ''' 216 | 217 | . Comment out `MP_Fault_Tolerance_NonFallback_Enabled=false` in `microprofile-config.properties` 218 | + 219 | -- 220 | .frontend microprofile-config.properties 221 | [source,properties] 222 | ---- 223 | # Disable fault tolerance globally 224 | #MP_Fault_Tolerance_NonFallback_Enabled=false <1> 225 | 226 | # Disable group policy: 227 | #Timeout/enabled=false 228 | 229 | # Disable a specific fault tolerance policy. Ex: 230 | #org.acme.FrontendResource/listStudents/Timeout/enabled=false 231 | ---- 232 | <1> Commented out 233 | 234 | NOTE: Feel free to uncomment the more specific approaches (all timeouts or just the timeout on `listStudents()`) and try them out. Just remember to comment them all out before continuing beyond this step. 235 | 236 | -- 237 | + 238 | // ********************************************* 239 | ''' 240 | 241 | . External configuration of fault tolerance parameters. 242 | + 243 | -- 244 | 245 | NOTE: MicroProfile Fault Tolerance allows any fault tolerance annotation parameter to be configured in microprofile-config.properties, overriding the value in source code. 246 | 247 | .frontend microprofile-config.properties 248 | [source,properties] 249 | ---- 250 | ... 251 | ... 252 | #org.acme.FrontendResource/listStudents/Timeout/enabled=false 253 | org.acme.FrontendResource/listStudents/Timeout/value=3000 <1> 254 | ---- 255 | <1> Add this, making the timeout longer than the wait time and preventing the fallback from being called. 256 | 257 | .Terminal 3 258 | [source,bash] 259 | ---- 260 | $ curl -i localhost:8080/frontend/list 261 | ---- 262 | .Terminal 3 Output 263 | .... 264 | HTTP/1.1 200 OK 265 | Content-Length: 41 266 | Content-Type: application/json 267 | 268 | ["Duke","John","Jane","Arun","Christina"] 269 | .... 270 | 271 | .Terminal 1 Output 272 | .... 273 | ** Waiting 2000ms ** 274 | .... 275 | -- 276 | // ********************************************* 277 | ''' 278 | 279 | . Comment out timeout value in `microprofile-config.properties` so annotation parameter values are used 280 | + 281 | -- 282 | .frontend microprofile-config.properties 283 | [source,properties] 284 | ---- 285 | ... 286 | ... 287 | #org.acme.FrontendResource/listStudents/Timeout/enabled=false 288 | #org.acme.FrontendResource/listStudents/Timeout/value=3000 <1> 289 | ---- 290 | <1> Comment this out 291 | 292 | -- 293 | 294 | + 295 | 296 | // ********************************************* 297 | ''' 298 | 299 | . Check endpoint. Verify fallback list of students is retrieved 300 | + 301 | -- 302 | .Terminal 3 303 | [source,bash] 304 | ---- 305 | $ curl -i localhost:8080/frontend/list 306 | ---- 307 | 308 | .Terminal 2 Output 309 | .... 310 | Exception Timeout[org.acme.FrontendResource#listStudents] timed out 311 | listStudentsFallbackCounter: 2 312 | .... 313 | 314 | .Terminal 3 Output 315 | .... 316 | HTTP/1.1 200 OK 317 | Content-Length: 66 318 | Content-Type: application/json 319 | 320 | ["Smart Sam","Genius Gabby","A-Student Angie","Intelligent Irene"] 321 | .... 322 | 323 | .Terminal 1 Output 324 | .... 325 | ** Waiting 2000ms ** 326 | .... 327 | -- 328 | + 329 | // ********************************************* 330 | ''' 331 | 332 | . Update doDelay() in StudentResource.java to return a random delay. 333 | + 334 | -- 335 | .StudentResource.java 336 | [source,java] 337 | ---- 338 | void doDelay() { 339 | int delayTime; 340 | try { 341 | delayTime=(int)(Math.random()*delay); <1> 342 | System.out.println("** Waiting " + delayTime + "ms **"); 343 | TimeUnit.MILLISECONDS.sleep(delayTime); 344 | } catch (InterruptedException e) { 345 | e.printStackTrace(); 346 | } 347 | } 348 | ---- 349 | <1> Updated code to print random number: `delayTime=(int)(Math.random()*delay);` 350 | -- 351 | + 352 | // ********************************************* 353 | ''' 354 | 355 | . Verify random sleep time. 356 | + 357 | -- 358 | .Terminal 3 359 | [source,bash] 360 | ---- 361 | $ curl -i localhost:8080/frontend/list 362 | ---- 363 | .Terminal 2 Output 364 | .... 365 | # Depending on random timeout, may show: 366 | Exception Timeout[org.acme.FrontendResource#listStudents] timed out 367 | listStudentsFallbackCounter: 3 368 | .... 369 | 370 | .Terminal 3 Output 371 | .... 372 | HTTP/1.1 200 OK 373 | Content-Length: 66 374 | Content-Type: application/json 375 | 376 | ["Smart Sam","Genius Gabby","A-Student Angie","Intelligent Irene"] 377 | or 378 | ["Duke","John","Jane","Arun","Christina"] 379 | .... 380 | 381 | NOTE: Because the delay is random, a longer delay will return the fallback student list, and a shorter delay will return the original student list. 382 | 383 | .Terminal 1 Output 384 | .... 385 | ** Waiting 1-1000ms ** <1> 386 | .... 387 | <1> This will be a random number between 1 and 1000 388 | 389 | NOTE: Retry a few times to see random sleep times. Keep retrying until Timeout threshold is reached and fallback method is called. 390 | -- 391 | + 392 | // ********************************************* 393 | ''' 394 | 395 | . Add a @Retry annotation, which by default will retry a request up to 3 times when exception is caught (e.g. TimeoutException) 396 | + 397 | -- 398 | .FrontendResource.java 399 | [source,java] 400 | ---- 401 | @Timeout 402 | @Retry <1> 403 | @Fallback(value = ListStudentsFallbackHandler.class) 404 | @GET 405 | @Path("/list") 406 | @Produces(MediaType.APPLICATION_JSON) 407 | public List listStudents() { 408 | List students; 409 | 410 | students = student.listStudents(); 411 | 412 | return students; 413 | } 414 | ---- 415 | <1> Add this 416 | -- 417 | 418 | + 419 | // ********************************************* 420 | ''' 421 | 422 | . Check retry logic 423 | + 424 | -- 425 | .Terminal 3 426 | [source,bash] 427 | ---- 428 | $ curl -i localhost:8080/frontend/list 429 | ---- 430 | .Terminal 2 Output 431 | .... 432 | # Depending on random timeout, may show: 433 | Exception Timeout[org.acme.FrontendResource#listStudents] timed out 434 | listStudentsFallbackCounter: 4 435 | .... 436 | 437 | .Terminal 3 Output 438 | .... 439 | HTTP/1.1 200 OK 440 | Content-Length: 66 441 | Content-Type: application/json 442 | 443 | ["Smart Sam","Genius Gabby","A-Student Angie","Intelligent Irene"] 444 | or 445 | ["Duke","John","Jane","Arun","Christina"] 446 | .... 447 | 448 | .Terminal 1 Output 449 | .... 450 | ** Waiting 1-1000ms ** <1> 451 | .... 452 | <1> One line will be displayed if less than 500ms, more than one line if more than 500ms due to retry 453 | 454 | NOTE: Re-run command until there are at least two output lines in Terminal 1 for a single `curl` command, at least one of which will be more than 500ms. 455 | -- 456 | + 457 | // ********************************************* 458 | ''' 459 | 460 | . Create file src/main/java/org/acme/CircuitBreakerTracker.java 461 | + 462 | -- 463 | 464 | This class will be used to track the state of our upcoming circuitbreaker. It will print the state of the circuitbreaker to stdout. 465 | 466 | .CircuitBreakerTracker.java 467 | [source,java] 468 | ---- 469 | package org.acme; 470 | 471 | import java.time.Duration; 472 | import java.util.HashMap; 473 | import java.util.Map; 474 | 475 | import javax.enterprise.context.Dependent; 476 | import javax.inject.Inject; 477 | 478 | import org.eclipse.microprofile.metrics.Gauge; 479 | import org.eclipse.microprofile.metrics.MetricID; 480 | import org.eclipse.microprofile.metrics.MetricRegistry; 481 | import org.eclipse.microprofile.metrics.MetricRegistry.Type; 482 | import org.eclipse.microprofile.metrics.annotation.RegistryType; 483 | 484 | @Dependent 485 | public class CircuitBreakerTracker { 486 | @Inject 487 | @RegistryType(type = Type.APPLICATION) 488 | MetricRegistry registry; 489 | 490 | public Map track() { 491 | HashMap map = new HashMap<>(); 492 | MetricID id = new MetricID("ft.org.acme.FrontendResource.listStudents.circuitbreaker.closed.total"); 493 | Gauge gauge = registry.getGauges().get(id); 494 | 495 | if (gauge != null) { 496 | map.put("CBClosedTime", "" + Duration.ofNanos((long) gauge.getValue()).toMillis() + "ms\n"); 497 | } 498 | 499 | id = new MetricID("ft.org.acme.FrontendResource.listStudents.circuitbreaker.halfOpen.total"); 500 | gauge = registry.getGauges().get(id); 501 | if (gauge != null) { 502 | map.put("CBHalfOpenTime", "" + Duration.ofNanos((long) gauge.getValue()).toMillis() + "ms\n"); 503 | } 504 | 505 | id = new MetricID("ft.org.acme.FrontendResource.listStudents.circuitbreaker.open.total"); 506 | gauge = registry.getGauges().get(id); 507 | if (gauge != null) { 508 | map.put("CBOpenTime", "" + Duration.ofNanos((long) gauge.getValue()).toMillis() + "ms\n"); 509 | } 510 | 511 | map.put("CBSucceededCount", 512 | "" + registry.counter("ft.org.acme.FrontendResource.listStudents.circuitbreaker.callsSucceeded.total") 513 | .getCount() + "\n"); 514 | map.put("CBPreventedCount", 515 | "" + registry.counter("ft.org.acme.FrontendResource.listStudents.circuitbreaker.callsPrevented.total") 516 | .getCount() + "\n"); 517 | 518 | map.forEach((key, value) -> System.out.print(key + ": " + value)); 519 | System.out.println(); 520 | 521 | return map; 522 | 523 | } 524 | } 525 | ---- 526 | -- 527 | + 528 | // ********************************************* 529 | ''' 530 | 531 | . Replace `@Timeout` logic with a `@CircuitBreaker` 532 | + 533 | -- 534 | .FrontendResource.java 535 | [source,java] 536 | ---- 537 | // @Timeout <1> 538 | // @Retry <2> 539 | @CircuitBreaker( <3> 540 | requestVolumeThreshold = 4, <4> 541 | failureRatio = 0.5, <5> 542 | delay = 10000, <6> 543 | successThreshold = 3 <7> 544 | ) 545 | @Fallback(value = ListStudentsFallbackHandler.class) 546 | @GET 547 | @Path("/list") 548 | @Produces(MediaType.APPLICATION_JSON) 549 | public List listStudents() { 550 | List students; 551 | 552 | students = student.listStudents(); 553 | 554 | return students; 555 | } 556 | ---- 557 | <1> Comment out @Timeout 558 | <2> Comment out @Retry 559 | <3> Add a circuit breaker. If circuit breaker throws a CircuitBreakerOpen exception, the @Retry annotation will retry the request. 560 | <4> Rolling window of 4 requests. 561 | <5> % of failures within the window that cause the circuit breaker to transition to "open"state 562 | <6> Wait 1000 milliseconds before allowing another request. Until then, each request will result in a CircuitBreakerOpen exception 563 | <7> Number of consecutive successful requests before circuit transitions from the half-open state to the closed state. The circuit breaker enters the half-open state upon the first successful request. 564 | -- 565 | + 566 | // ********************************************* 567 | ''' 568 | 569 | . Inject the CircuitbreakerTracker instance in FrontendResource.java 570 | + 571 | -- 572 | .FrontendResource.java 573 | [source,java] 574 | ---- 575 | @Path("/frontend") 576 | public class FrontendResource { 577 | @Inject <1> 578 | CircuitBreakerTracker tracker; 579 | 580 | @Inject 581 | @RestClient 582 | StudentRestClient student; 583 | ---- 584 | <1> Inject an instance of the circuitbreaker tracker 585 | -- 586 | + 587 | // ********************************************* 588 | ''' 589 | . Output circuit breaker stats 590 | + 591 | -- 592 | .FrontendResource.java 593 | [source,java] 594 | ---- 595 | List students; 596 | 597 | tracker.track(); <1> 598 | 599 | students = student.listStudents(); 600 | ---- 601 | <1> Display the circuitbreaker state 602 | -- 603 | + 604 | // ********************************************* 605 | ''' 606 | 607 | . Inject the CircuitbreakerTracker instance in ListStudentsFallbackHandler.java 608 | + 609 | -- 610 | .ListStudentsFallbackHandler.java 611 | [source,java] 612 | ---- 613 | @Inject 614 | @RegistryType(type = Type.APPLICATION) 615 | MetricRegistry registry; 616 | 617 | @Inject <1> 618 | CircuitBreakerTracker tracker; 619 | ---- 620 | <1> Inject the tracker 621 | -- 622 | + 623 | // ********************************************* 624 | ''' 625 | . Output circuit breaker stats in ListStudentsFallbackHandler.java 626 | + 627 | -- 628 | .ListStudentsFallbackHandler.java 629 | [source,java] 630 | ---- 631 | System.out.println("Exception " + failure); 632 | System.out.println("listStudentsFallbackCounter: " + registry.counter("ft.org.acme.FrontendResource.listStudents.fallback.calls.total").getCount()); 633 | tracker.track(); <1> 634 | ---- 635 | <1> Display the circuitbreaker state 636 | -- 637 | + 638 | 639 | // ********************************************* 640 | ''' 641 | 642 | . Check CircuitBreaker logic 643 | + 644 | -- 645 | .Terminal 3 646 | [source,bash] 647 | ---- 648 | $ curl -i localhost:8080/frontend/list 649 | ---- 650 | .Terminal 3 Output 651 | .... 652 | HTTP/1.1 200 OK 653 | Content-Length: 66 654 | Content-Type: application/json 655 | 656 | ["Duke","John","Jane","Arun","Christina"] 657 | .... 658 | 659 | 660 | .Terminal 2 661 | [source,bash] 662 | ---- 663 | CBClosedTime: 4ms <1> 664 | CBOpenTime: 0ms <2> 665 | CBSucceededCount: 0 <3> 666 | CBPreventedCount: 0 <4> 667 | CBHalfOpenTime: 0ms <5> 668 | ---- 669 | <1> Amount of time circuitbreaker is in the closed state 670 | <2> Amount of time circuitbreaker is in the open state 671 | <3> Number of times a call is successfully completed 672 | <4> Number of times a call was prevented due to circuit breaker being open 673 | <5> Amount of time circuitbreaker is in the half-open state 674 | 675 | .Terminal 1 Output 676 | .... 677 | ** Waiting 1-1000ms ** 678 | .... 679 | -- 680 | + 681 | 682 | // ********************************************* 683 | 684 | 685 | . Stop student service 686 | + 687 | -- 688 | .Terminal 1 689 | [source,bash] 690 | ---- 691 | CTRL-C 692 | ---- 693 | -- 694 | + 695 | // ********************************************* 696 | ''' 697 | 698 | . Check the circuit breaker 699 | + 700 | -- 701 | 702 | This will result in `java.net.ConnectException`. When circuit breaker delay is exceeded, then the circuit breaker throws a CircuitBreakerOpenException. Both exceptions are caught by fallback logic to invoke fallback method. Try running this a few times, waiting 10-15 seconds occasionally. 703 | 704 | 705 | .Terminal 3 706 | [source,bash] 707 | ---- 708 | $ curl -i localhost:8080/frontend/list 709 | ---- 710 | 711 | .Terminal 2 712 | [source,bash] 713 | ---- 714 | Exception RESTEASY004655: Unable to invoke request: java.net.ConnectException: Connection refused (Connection refused) 715 | listStudentsFallbackCounter: 1 716 | CBClosedTime: 6770ms 717 | CBOpenTime: 0ms 718 | CBSucceededCount: 1 719 | CBPreventedCount: 0 720 | CBHalfOpenTime: 0ms 721 | ---- 722 | 723 | .Terminal 1 724 | [source,bash] 725 | ---- 726 | ** Waiting 844ms ** 727 | ---- 728 | -- 729 | + 730 | // ********************************************* 731 | ''' 732 | 733 | . Re-run student service 734 | + 735 | -- 736 | .Terminal 1 737 | [source,bash] 738 | ---- 739 | mvn compile quarkus:dev -Ddebug=5006 740 | ---- 741 | -- 742 | + 743 | // ********************************************* 744 | ''' 745 | 746 | . Retry until circuit breaker closes and the normal student list is displayed. 747 | + 748 | -- 749 | .Terminal 3 750 | [source,bash] 751 | ---- 752 | $ curl -i localhost:8080/frontend/list 753 | ---- 754 | 755 | .Terminal 3 Output 756 | .... 757 | HTTP/1.1 200 OK 758 | Content-Length: 66 759 | Content-Type: application/json 760 | 761 | ["Smart Sam","Genius Gabby","A-Student Angie","Intelligent Irene"] 762 | .... 763 | -- 764 | Retry the command until the primary student list is displayed. During this time, you will see changes to the stats output to the CLI. 765 | 766 | Example: 767 | 768 | .Terminal 2 769 | .... 770 | CBClosedTime: 25202ms 771 | CBOpenTime: 28069ms 772 | CBSucceededCount: 4 773 | CBPreventedCount: 3 774 | CBHalfOpenTime: 19369ms 775 | .... 776 | -------------------------------------------------------------------------------- /docs/microprofile-health.adoc: -------------------------------------------------------------------------------- 1 | == MicroProfile Health 2 | 3 | This section will create an endpoint that exposes the health of the student service. The logic will result in the student service being healthy 50% of the time. This will be checked using a CLI, but in the packaging section will be checked using a docker-compose healthcheck. 4 | 5 | . Verify default health check endpoint 6 | + 7 | -- 8 | .Terminal 3 9 | .... 10 | $ curl -i localhost:8082/health 11 | .... 12 | 13 | .Terminal 3 Output 14 | .... 15 | HTTP/1.1 200 OK 16 | content-type: application/json; charset=UTF-8 17 | content-length: 46 18 | 19 | 20 | { 21 | "status": "UP", 22 | "checks": [ 23 | ] 24 | } 25 | .... 26 | -- 27 | + 28 | // ********************************************* 29 | ''' 30 | 31 | . Create a MicroProfile Health Endpoint 32 | + 33 | -- 34 | .student/src/main/java/org/acme/StudentHealth.java 35 | [source,java] 36 | ---- 37 | package org.acme; 38 | 39 | import org.eclipse.microprofile.health.HealthCheck; 40 | import org.eclipse.microprofile.health.HealthCheckResponse; 41 | import org.eclipse.microprofile.health.Liveness; 42 | import org.eclipse.microprofile.health.Readiness; 43 | 44 | @Liveness <1> 45 | @Readiness <2> 46 | public class StudentHealth implements HealthCheck { 47 | @Override 48 | public HealthCheckResponse call() { 49 | double random = Math.random(); 50 | return HealthCheckResponse 51 | .named("StudentLivenessReadiness") <3> 52 | .state(random < .50 ? true : false) <4> 53 | .withData("randomNumber", "" + random) <5> 54 | .build(); 55 | } 56 | } 57 | ---- 58 | <1> Restart unrecoverable service 59 | <2> Pause traffic until ready 60 | <3> A healthcheck can be named 61 | <4> State is UP (true) or DOWN (false) 62 | <5> Data can be added to provide state context 63 | 64 | NOTE: Retry a few times until both UP and DOWN have been displayed across subsequent requests. If there is more than one health check class in an application, then all must be UP for overall state to be UP. 65 | 66 | NOTE: Typically there would be a separate health check class for readiness and liveness, but shown here in a single class for "conciseness" under time constraints. 67 | -- 68 | + 69 | // ********************************************* 70 | ''' 71 | 72 | . Check health liveness endpoint specifically 73 | + 74 | -- 75 | .Terminal 3 76 | [source,bash] 77 | ---- 78 | $ curl -i localhost:8080/health/live 79 | ---- 80 | 81 | .Terminal 3 Output 82 | .... 83 | HTTP/1.1 503 Service Unavailable <1> 84 | content-type: application/json; charset=UTF-8 85 | content-length: 231 86 | 87 | 88 | { 89 | "status": "DOWN", 90 | "checks": [ 91 | { 92 | "name": "StudentLivenessReadiness", 93 | "status": "DOWN", 94 | "data": { 95 | "randomNumber": "0.60806403626233085" 96 | } 97 | } 98 | ] 99 | } 100 | .... 101 | <1> The HTTP Reponse code will be 503 when a service is down 102 | 103 | NOTE: There is a /health/ready endpoint as well 104 | -- 105 | -------------------------------------------------------------------------------- /docs/microprofile-jwt-rbac.adoc: -------------------------------------------------------------------------------- 1 | == MicroProfile JWT RBAC 2 | 3 | This section will secure the student service and frontend service endpoints, and propagate a bearer token across services. 4 | 5 | NOTE: A token has already been generated using a supplied build of Adam Bien's https://github.com/AdamBien/jwtenizr[jwtenizr]. In addition, to facilitate these instructions, the token will last until 2070 :-) 6 | 7 | Because tokens are base64 encoded, they can be easily decoded. jwt.io can display jwt tokens and verify them using the issuer's public key. https://jwt.io/#debugger-io?token=eyJraWQiOiJqd3Qua2V5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJ1c2VyXC80Mzk3MSIsInVwbiI6ImRlbW9AYWNtZS5vcmciLCJteWMiOiJNeSBDdXN0b20gQ2xhaW0iLCJhdXRoX3RpbWUiOjE1Nzg2NTEyODMsImlzcyI6ImFpcmhhY2tzIiwiZ3JvdXBzIjpbInVzZXIiLCJhZG1pbiJdLCJleHAiOjMxNTU4ODI4OTgsImlhdCI6MTU3ODY1MTI4MywianRpIjoiYWlyaGFja3Mtand0LXVuaXF1ZS1pZC0xMjM0MjE0MiJ9.Eaqe3sTH64doIVW3on25EA_uD9XrfppndiweUNLVbFK3KxaIfXaAdQ4N9IkQG6Iw0A7I7kngjeSHwb2DzH8rQE8yp7sCtey6kmC689eQC0j2k-YbyGZ68xnsMj5taOBVGH_ZSWC6E1L-Gk-GgcTvX6I3SaBC8pwZ267q6psknqlAtfD2JoE7ezEb7LrLVwP1vaGqKzC2X6pv5J-07DNBqe75uBWQyqX_WE856ug3uqWcHtNck8nqU6VhwXqxHZ6vkRlx9VoMgFUF851D-WuKMCUdfXJHekDyKmjYuyLiw7jtQSdliY3ONOXgFm_uzjKGuZ1VKPdQXyx7GQ9NsNTYfw&publicKey=-----BEGIN%20PUBLIC%20KEY-----%0AMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBR6TwVxolT5E2emnQEwqJztmeWRThU4ZA3V9%2B4vjOXoNmSKWrLfqLaKuMric9opYQi86yO1o0qChkAnlRY7ZytcaFqcehYOSAhcghYNn4Wzi70D2lJHj%2FYflFKdssySyNzqMIBMxNWZWx8kIVDRrVamsmF2Fo4Dg72ce8KiMSlqkWrHiSbfWpa2aQru9dEhErJPf05fGzQWwtvOvtLCp%2FtLXq7GmTE2XJJdiCk3CdE3OP%2FFQRWyeRtHk6Uq4hjzXTX6Wnrb7xDZCjQubfWYq9yoINet1eMFWFUXRsAJQbMJKIstcCvwmO35iPjFrftWTADOh3pzIARVqWwupDN7fwIDAQAB%0A-----END%20PUBLIC%20KEY-----[(Click here)] to see the token used in this course. 8 | 9 | // ********************************************* 10 | 11 | . Add the MicroProfile dependency to *both the student service and frontend service* `pom.xml` files. 12 | + 13 | -- 14 | .pom.xml 15 | [source,xml] 16 | ---- 17 | <1> 18 | io.quarkus 19 | quarkus-smallrye-jwt 20 | 21 | ---- 22 | <1> Add this 23 | -- 24 | + 25 | 26 | // ********************************************* 27 | ''' 28 | 29 | . Add required MicroProfile JWT RBAC properties to *both the student service and frontend service* `microprofile-config.properties` files. 30 | + 31 | -- 32 | .microprofile-config.properties 33 | [source,property] 34 | ---- 35 | mp.jwt.verify.issuer=airhacks 36 | mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBR6TwVxolT5E2emnQEwqJztmeWRThU4ZA3V9+4vjOXoNmSKWrLfqLaKuMric9opYQi86yO1o0qChkAnlRY7ZytcaFqcehYOSAhcghYNn4Wzi70D2lJHj/YflFKdssySyNzqMIBMxNWZWx8kIVDRrVamsmF2Fo4Dg72ce8KiMSlqkWrHiSbfWpa2aQru9dEhErJPf05fGzQWwtvOvtLCp/tLXq7GmTE2XJJdiCk3CdE3OP/FQRWyeRtHk6Uq4hjzXTX6Wnrb7xDZCjQubfWYq9yoINet1eMFWFUXRsAJQbMJKIstcCvwmO35iPjFrftWTADOh3pzIARVqWwupDN7fwIDAQAB 37 | ---- 38 | -- 39 | 40 | <<< 41 | 42 | === Securing Frontend Service 43 | 44 | . Create a new endpoint in `FrontendResource.java` to display the Principal 45 | + 46 | -- 47 | .FrontendResource.java 48 | [source,java] 49 | ---- 50 | @Inject 51 | Principal principal; 52 | 53 | @GET 54 | @Path("/tokeninfo") 55 | @Produces(MediaType.TEXT_PLAIN) 56 | public String tokeninfo() { 57 | String string = "Principal: " + principal.getName(); 58 | return string; 59 | } 60 | ---- 61 | -- 62 | // ********************************************* 63 | ''' 64 | . Check endpoint, which should return 'null' since no principal has been supplied 65 | + 66 | -- 67 | .Terminal 3 68 | [source,bash] 69 | ---- 70 | $ curl -i localhost:8080/frontend/tokeninfo 71 | ---- 72 | 73 | .Terminal 3 Output 74 | .... 75 | HTTP/1.1 200 OK 76 | Content-Length: 15 77 | Content-Type: text/plain;charset=UTF-8 78 | 79 | Principal: null 80 | .... 81 | -- 82 | + 83 | // ********************************************* 84 | ''' 85 | 86 | . Assign token to a `TOKEN` environmental variable 87 | + 88 | -- 89 | 90 | .Terminal 3 91 | [source,bash] 92 | ---- 93 | $ export TOKEN="eyJraWQiOiJqd3Qua2V5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJ1c2VyXC80Mzk3MSIsInVwbiI6ImRlbW9AYWNtZS5vcmciLCJteWMiOiJNeSBDdXN0b20gQ2xhaW0iLCJhdXRoX3RpbWUiOjE1Nzg2NTEyODMsImlzcyI6ImFpcmhhY2tzIiwiZ3JvdXBzIjpbInVzZXIiLCJhZG1pbiJdLCJleHAiOjMxNTU4ODI4OTgsImlhdCI6MTU3ODY1MTI4MywianRpIjoiYWlyaGFja3Mtand0LXVuaXF1ZS1pZC0xMjM0MjE0MiJ9.Eaqe3sTH64doIVW3on25EA_uD9XrfppndiweUNLVbFK3KxaIfXaAdQ4N9IkQG6Iw0A7I7kngjeSHwb2DzH8rQE8yp7sCtey6kmC689eQC0j2k-YbyGZ68xnsMj5taOBVGH_ZSWC6E1L-Gk-GgcTvX6I3SaBC8pwZ267q6psknqlAtfD2JoE7ezEb7LrLVwP1vaGqKzC2X6pv5J-07DNBqe75uBWQyqX_WE856ug3uqWcHtNck8nqU6VhwXqxHZ6vkRlx9VoMgFUF851D-WuKMCUdfXJHekDyKmjYuyLiw7jtQSdliY3ONOXgFm_uzjKGuZ1VKPdQXyx7GQ9NsNTYfw" 94 | ---- 95 | -- 96 | + 97 | // ********************************************* 98 | ''' 99 | 100 | . Re-run the command, this time supplying the token: 101 | + 102 | -- 103 | 104 | .Terminal 3 105 | [source,bash] 106 | ---- 107 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/tokeninfo 108 | ---- 109 | 110 | .Terminal 3 Output 111 | .... 112 | HTTP/1.1 200 OK 113 | Content-Length: 15 114 | Content-Type: text/plain;charset=UTF-8 115 | 116 | Principal: demo@acme.org 117 | .... 118 | -- 119 | + 120 | // ********************************************* 121 | ''' 122 | 123 | . Update "/tokeninfo" endpoint to display all claims 124 | + 125 | -- 126 | .FrontendResource.java 127 | [source,java] 128 | ---- 129 | @Inject 130 | JsonWebToken token; <1> 131 | 132 | @GET 133 | @Path("/tokeninfo") 134 | @Produces(MediaType.TEXT_PLAIN) 135 | public String tokenInfo() { <2> 136 | String string = "Principal: " + principal.getName(); 137 | 138 | string += ",\n"; 139 | 140 | string += token.getClaimNames() 141 | .stream() 142 | .map(claim -> "\n " + claim + ": " + token.getClaim(claim)) 143 | .collect(Collectors.toList()) 144 | .toString(); 145 | 146 | return string; 147 | } 148 | ---- 149 | <1> Inject the token 150 | <2> Replace the contents of tokenInfo 151 | -- 152 | + 153 | // ********************************************* 154 | ''' 155 | 156 | . Check the token output 157 | 158 | + 159 | -- 160 | .Terminal 3 161 | [source,bash] 162 | ---- 163 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/tokeninfo 164 | ---- 165 | 166 | .Terminal 3 Output 167 | .... 168 | HTTP/1.1 200 OK 169 | Content-Length: 929 170 | Content-Type: text/plain;charset=UTF-8 171 | 172 | Principal: demo@acme.org, 173 | [ 174 | sub: user/43971, 175 | upn: demo@acme.org, 176 | myc: My Custom Claim, 177 | raw_token: eyJraWQiOiJqd3Qua2V5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJ1c2VyXC80Mzk3MSIsInVwbiI6ImRlbW9AYWNtZS5vcmciLCJteWMiOiJNeSBDdXN0b20gQ2xhaW0iLCJhdXRoX3RpbWUiOjE1Nzg2NTEyODMsImlzcyI6ImFpcmhhY2tzIiwiZ3JvdXBzIjpbInVzZXIiLCJhZG1pbiJdLCJleHAiOjMxNTU4ODI4OTgsImlhdCI6MTU3ODY1MTI4MywianRpIjoiYWlyaGFja3Mtand0LXVuaXF1ZS1pZC0xMjM0MjE0MiJ9.Eaqe3sTH64doIVW3on25EA_uD9XrfppndiweUNLVbFK3KxaIfXaAdQ4N9IkQG6Iw0A7I7kngjeSHwb2DzH8rQE8yp7sCtey6kmC689eQC0j2k-YbyGZ68xnsMj5taOBVGH_ZSWC6E1L-Gk-GgcTvX6I3SaBC8pwZ267q6psknqlAtfD2JoE7ezEb7LrLVwP1vaGqKzC2X6pv5J-07DNBqe75uBWQyqX_WE856ug3uqWcHtNck8nqU6VhwXqxHZ6vkRlx9VoMgFUF851D-WuKMCUdfXJHekDyKmjYuyLiw7jtQSdliY3ONOXgFm_uzjKGuZ1VKPdQXyx7GQ9NsNTYfw, 178 | auth_time: 1578651283, 179 | iss: airhacks, 180 | groups: [admin, user], 181 | exp: 3155882898, 182 | iat: 1578651283, 183 | jti: airhacks-jwt-unique-id-12342142] 184 | .... 185 | -- 186 | + 187 | // ********************************************* 188 | ''' 189 | 190 | . Secure endpoints by limiting access to specified roles 191 | + 192 | -- 193 | .FrontendResource.java 194 | [source,java] 195 | ---- 196 | @RolesAllowed("user") <1> 197 | @GET 198 | @Path("/tokeninfo") 199 | @Produces(MediaType.TEXT_PLAIN) 200 | public String tokeninfo() { 201 | String string = "Principal: " + principal.getName(); 202 | string += ",\n"; 203 | 204 | string += token.getClaimNames().stream().map(tok -> "\n " + tok + ": " + token.getClaim(tok)) 205 | .collect(Collectors.toList()).toString(); 206 | 207 | return string; 208 | } 209 | 210 | @RolesAllowed("superuser") <2> 211 | @SimplyTimed( 212 | absolute = true, 213 | name = "listStudentsTime", 214 | displayName = "FrontendResource.listStudents()") 215 | // @Fallback(value = ListStudentsFallbackHandler.class) 216 | @Fallback(fallbackMethod = "listStudentsFallback") 217 | // @Timeout 218 | // @Retry 219 | @CircuitBreaker( 220 | requestVolumeThreshold = 4, 221 | failureRatio = .5, 222 | delay = 10000, 223 | successThreshold = 3) 224 | @GET 225 | @Produces(MediaType.APPLICATION_JSON) 226 | @Path("/list") 227 | public List listStudents() { 228 | return student.listStudents(); 229 | } 230 | ---- 231 | <1> Apply `@RolesAllowed("user")` to the getToken() method 232 | <2> Apply `@RolesAllowed("superuser")` to the listStudents() method 233 | -- 234 | // ********************************************* 235 | ''' 236 | 237 | . Check the endpoints to validate access 238 | + 239 | -- 240 | .Terminal 3 241 | [source,bash] 242 | ---- 243 | $ curl -i http://localhost:8080/frontend/list 244 | ---- 245 | 246 | .Output 247 | .... 248 | HTTP/1.1 401 Unauthorized 249 | www-authenticate: Bearer {token} 250 | Content-Length: 0 251 | .... 252 | 253 | 254 | NOTE: Access is denied because the user is anonymous and there are no roles tied to the anonymous user. Note the HTTP response code is `401 Unauthorized` 255 | -- 256 | 257 | . Retry the request using a token. 258 | + 259 | -- 260 | .Terminal 3 261 | [source,bash] 262 | ---- 263 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 264 | ---- 265 | 266 | .Terminal 3 Output 267 | .... 268 | HTTP/1.1 403 Forbidden 269 | Content-Length: 9 270 | Content-Type: application/json 271 | 272 | Forbidden 273 | .... 274 | NOTE: This time access is denied because the demo user does not belong to the "superuser" group. The demo user belongs to the "user" and "admin" groups. Note the HTTP response code is `403 Forbidden` 275 | 276 | -- 277 | + 278 | // ********************************************* 279 | ''' 280 | 281 | . Change the "superuser" role to the "admin" role, which the "demo" user belongs to 282 | + 283 | -- 284 | .FrontendResource.java 285 | [source,java] 286 | ---- 287 | @RolesAllowed("admin") <1> 288 | @SimplyTimed( 289 | absolute = true, 290 | name = "listStudentsTime", 291 | displayName = "FrontendResource.listStudents()") 292 | // @Fallback(value = ListStudentsFallbackHandler.class) 293 | @Fallback(fallbackMethod = "listStudentsFallback") 294 | // @Timeout 295 | // @Retry 296 | @CircuitBreaker( 297 | requestVolumeThreshold = 4, 298 | failureRatio = .5, 299 | delay = 10000, 300 | successThreshold = 3) 301 | @GET 302 | @Produces(MediaType.APPLICATION_JSON) 303 | @Path("/list") 304 | public List listStudents() { 305 | return student.listStudents(); 306 | } 307 | ---- 308 | <1> Change "superuser" to "admin" 309 | -- 310 | // ********************************************* 311 | ''' 312 | 313 | . Check access with newly supplied "admin" role 314 | + 315 | -- 316 | .Terminal 3 317 | [source,bash] 318 | ---- 319 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 320 | ---- 321 | .Terminal 3 Output 322 | .... 323 | HTTP/1.1 200 OK 324 | Content-Length: 41 325 | Content-Type: application/json 326 | 327 | ["Duke","John","Jane","Arun","Christina"] 328 | .... 329 | -- 330 | 331 | <<< 332 | 333 | === Securing Student Service 334 | 335 | . Secure `StudentResource.listStudents()`, requiring the admin role 336 | + 337 | -- 338 | .StudentResource.java 339 | [source,java] 340 | ---- 341 | @RolesAllowed("admin") <1> 342 | @GET 343 | @Path("/list") 344 | @Produces(MediaType.APPLICATION_JSON) 345 | public List listStudents() { 346 | doDelay(); 347 | return students; 348 | } 349 | ---- 350 | <1> Secure with "admin" role 351 | 352 | .Terminal 3 353 | [source,bash] 354 | ---- 355 | $ curl -i -H"Authorization: Bearer ${TOKEN}" http://localhost:8080/frontend/list 356 | ---- 357 | 358 | .Terminal 3 Output 359 | .... 360 | HTTP/1.1 200 OK 361 | Content-Length: 66 362 | Content-Type: application/json 363 | 364 | ["Smart Sam","Genius Gabby","A-Student Angie","Intelligent Irene"] 365 | .... 366 | This implies that the request to the student service is not being managed properly because the fallback output is returned. 367 | -- 368 | // ********************************************* 369 | ''' 370 | 371 | . The token needs to be forwarded to the student service. This requires annotating StudentRestClient with `@RegisterClientHeaders` and defining the headers to propagate (Authorization header) using the `org.eclipse.microprofile.rest.client.propagateHeaders` property. 372 | + 373 | -- 374 | .StudentRestClient.java 375 | [source,java] 376 | ---- 377 | @RegisterClientHeaders <1> 378 | @RegisterRestClient(configKey = "StudentService") 379 | @Path("/student") 380 | public interface StudentRestClient { 381 | ---- 382 | <1> Add `@RegisterClientHeaders` to frontend microprofile-config.properties 383 | 384 | .frontend/src/main/resources/META-INF/microprofile-config.properties 385 | [source,properties] 386 | ---- 387 | org.eclipse.microprofile.rest.client.propagateHeaders=Authorization<1> 388 | 389 | mp.jwt.verify.issuer=airhacks 390 | mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBR6TwVxolT5E2emnQEwqJztmeWRThU4ZA3V9+4vjOXoNmSKWrLfqLaKuMric9opYQi86yO1o0qChkAnlRY7ZytcaFqcehYOSAhcghYNn4Wzi70D2lJHj/YflFKdssySyNzqMIBMxNWZWx8kIVDRrVamsmF2Fo4Dg72ce8KiMSlqkWrHiSbfWpa2aQru9dEhErJPf05fGzQWwtvOvtLCp/tLXq7GmTE2XJJdiCk3CdE3OP/FQRWyeRtHk6Uq4hjzXTX6Wnrb7xDZCjQubfWYq9yoINet1eMFWFUXRsAJQbMJKIstcCvwmO35iPjFrftWTADOh3pzIARVqWwupDN7fwIDAQAB 391 | ---- 392 | // Note that it is important to not have spaces before property text <> labels!!!!! 393 | <1> Add this line to propagate the Authorization header. Additional headers can be propagated as well, separated by commas. 394 | 395 | .Terminal 3 Output 396 | .... 397 | HTTP/1.1 200 OK 398 | Content-Length: 41 399 | Content-Type: application/json 400 | 401 | ["Duke"," John"," Jane"," Arun"," Christina"] 402 | .... 403 | 404 | The token (Authorization header) has been successfully propagated. 405 | -- 406 | -------------------------------------------------------------------------------- /docs/microprofile-metrics.adoc: -------------------------------------------------------------------------------- 1 | == MicroProfile Metrics 2 | 3 | This section will cover business and performance metrics that will be graphed in Prometheus and Grafana in the packaging and deployment section. 4 | 5 | . Enable MicroProfile 2.3 REST metrics 6 | + 7 | -- 8 | While REST metrics in MicroProfile Metrics are optional to support, Quarkus does support them. However, in Quarkus, metrics must be enabled on an extension-by-extension basis. See https://quarkus.io/guides/all-config . 9 | 10 | .FrontendService microprofile-config.properties 11 | [source,properties] 12 | ---- 13 | # ... 14 | # org.acme.FrontendResource/listStudents/Timeout/value=3000 15 | quarkus.resteasy.metrics.enabled=true <1> 16 | ---- 17 | <1> Enable rest metrics 18 | -- 19 | // ********************************************* 20 | ''' 21 | . View all default metrics (in Prometheus/OpenMetrics format) 22 | + 23 | -- 24 | .Terminal 3 25 | [source, bash] 26 | ---- 27 | $ curl -i http://localhost:8080/metrics 28 | ---- 29 | .Terminal 3 Example Output 30 | .... 31 | ... 32 | ... 33 | # TYPE application_ft_org_acme_FrontendResource_listStudents_circuitbreaker_callsSucceeded_total counter 34 | application_ft_org_acme_FrontendResource_listStudents_circuitbreaker_callsSucceeded_total 2.0 35 | # HELP base_gc_time_total Displays the approximate accumulated collection elapsed time in milliseconds. This attribute displays -1 if the collection elapsed time is undefined for this collector. The Java virtual machine implementation may use a high resolution timer to measure the elapsed time. This attribute may display the same value even if the collection count has been incremented if the collection elapsed time is very short. 36 | # TYPE base_gc_time_total counter 37 | base_gc_time_total_seconds{name="PS MarkSweep"} 0.156 38 | ... 39 | ... 40 | .... 41 | 42 | NOTE: OpenMetrics format provides metadata like metrics type (ex: gauge) and description 43 | 44 | -- 45 | // ********************************************* 46 | ''' 47 | 48 | . View base metrics (in JSON this time) 49 | + 50 | -- 51 | .Terminal 3 52 | [source,bash] 53 | ---- 54 | $ curl -i -H "Accept: application/json" \ 55 | http://localhost:8080/metrics/base 56 | ---- 57 | .Terminal 3 Example Output 58 | .... 59 | HTTP/1.1 200 OK 60 | Access-Control-Allow-Origin: * 61 | Access-Control-Allow-Credentials: true 62 | Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, HEAD 63 | Access-Control-Max-Age: 1209600 64 | Access-Control-Allow-Headers: origin, content-type, accept, authorization 65 | Content-Type: application/json 66 | content-length: 630 67 | 68 | 69 | { 70 | "gc.total;name=PS MarkSweep": 2, 71 | "cpu.systemLoadAverage": 2.1572265625, 72 | "thread.count": 78, 73 | "classloader.loadedClasses.count": 8145, 74 | "classloader.unloadedClasses.total": 26, 75 | "gc.total;name=PS Scavenge": 7, 76 | "gc.time;name=PS MarkSweep": 75, 77 | "jvm.uptime": 6725918, 78 | "thread.max.count": 158, 79 | "memory.committedHeap": 879230976, 80 | "classloader.loadedClasses.total": 8171, 81 | "cpu.availableProcessors": 12, 82 | "gc.time;name=PS Scavenge": 72, 83 | "thread.daemon.count": 12, 84 | "memory.maxHeap": 7635730432, 85 | "cpu.processCpuLoad": 0.00015370844246171116, 86 | "memory.usedHeap": 102588008 87 | } 88 | .... 89 | -- 90 | + 91 | // ********************************************* 92 | ''' 93 | 94 | . View vendor-specific (Quarkus) metrics (in JSON) 95 | + 96 | -- 97 | .Terminal 3 98 | [source,bash] 99 | ---- 100 | $ curl -i -H "Accept: application/json" \ 101 | http://localhost:8080/metrics/vendor 102 | ---- 103 | .Terminal 3 Example Output 104 | .... 105 | HTTP/1.1 200 OK 106 | Access-Control-Allow-Origin: * 107 | Access-Control-Allow-Credentials: true 108 | Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, HEAD 109 | Access-Control-Max-Age: 1209600 110 | Access-Control-Allow-Headers: origin, content-type, accept, authorization 111 | Content-Type: application/json 112 | content-length: 933 113 | 114 | 115 | { 116 | "memory.freePhysicalSize": 185147392, 117 | "memoryPool.usage;name=Metaspace": 41917128, 118 | "memoryPool.usage.max;name=PS Eden Space": 534773760, 119 | "memoryPool.usage;name=PS Eden Space": 0, 120 | "memoryPool.usage.max;name=PS Old Gen": 26178520, 121 | "memoryPool.usage;name=PS Old Gen": 26162136, 122 | "cpu.processCpuTime": 23883246000, 123 | "memory.committedNonHeap": 62717952, 124 | "memoryPool.usage.max;name=PS Survivor Space": 22014064, 125 | "memoryPool.usage.max;name=Compressed Class Space": 5191952, 126 | "memoryPool.usage;name=Code Cache": 12367808, 127 | "memory.freeSwapSize": 185192448, 128 | "memoryPool.usage.max;name=Metaspace": 41909544, 129 | "cpu.systemCpuLoad": 0.059001660401582626, 130 | "memoryPool.usage.max;name=Code Cache": 12367808, 131 | "memory.usedNonHeap": 59479424, 132 | "memoryPool.usage;name=PS Survivor Space": 20868400, 133 | "memoryPool.usage;name=Compressed Class Space": 5193208, 134 | "memory.maxNonHeap": -1 135 | } 136 | .... 137 | -- 138 | + 139 | // ********************************************* 140 | ''' 141 | 142 | . View application metrics (in JSON) 143 | + 144 | 145 | -- 146 | .Terminal 3 147 | [source,bash] 148 | ---- 149 | $ curl -i -H "Accept: application/json" \ 150 | http://localhost:8080/metrics/application 151 | ---- 152 | .Terminal 3 Example Output 153 | .... 154 | HTTP/1.1 200 OK 155 | Access-Control-Allow-Origin: * 156 | Access-Control-Allow-Credentials: true 157 | Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, HEAD 158 | Access-Control-Max-Age: 1209600 159 | Access-Control-Allow-Headers: origin, content-type, accept, authorization 160 | Content-Type: application/json 161 | content-length: 1162 162 | 163 | 164 | { 165 | "ft.org.acme.FrontendResource.listStudents.circuitbreaker.closed.total": 229004662188, 166 | "ft.org.acme.FrontendResource.listStudents.circuitbreaker.callsFailed.total": 4, 167 | "ft.org.acme.FrontendResource.listStudents.retry.callsSucceededNotRetried.total": 5, 168 | "ft.org.acme.FrontendResource.listStudents.invocations.total": 9, 169 | "ft.org.acme.FrontendResource.listStudents.circuitbreaker.open.total": 138015497877, 170 | "ft.org.acme.FrontendResource.listStudents.retry.callsFailed.total": 4, 171 | "ft.org.acme.FrontendResource.listStudents.retry.retries.total": 16, 172 | ... 173 | ... 174 | ... 175 | .... 176 | -- 177 | + 178 | // ********************************************* 179 | ''' 180 | 181 | . Add `@Counted` to `FrontendResource`, counting invocations for each method 182 | + 183 | -- 184 | .FrontendResource.java 185 | [source,java] 186 | ---- 187 | @Counted( <1> 188 | absolute = true, 189 | name = "FrontendCounter") 190 | @Path("/frontend") 191 | public class FrontendResource { 192 | 193 | @Inject 194 | @RestClient 195 | StudentRestClient student; 196 | // ... 197 | ---- 198 | <1> Add `@Counted` annotation 199 | -- 200 | + 201 | // ********************************************* 202 | ''' 203 | 204 | . Time `listStudents()` method duration 205 | + 206 | -- 207 | .FrontendResource.java 208 | [source,java] 209 | ---- 210 | @SimplyTimed(absolute = true, <1> 211 | name = "listStudentsTime", <2> 212 | displayName = "FrontendResource.listStudents()") <3> 213 | @Retry(maxRetries = 4,delay = 1000) 214 | @CircuitBreaker( 215 | requestVolumeThreshold = 4, 216 | failureRatio = 0.5, 217 | delay = 10000, 218 | successThreshold = 2) 219 | @Fallback(value = ListStudentsFallbackHandler.class) 220 | @GET 221 | @Path("/list") 222 | @Produces(MediaType.APPLICATION_JSON) 223 | public List listStudents() { 224 | List students = student.listStudents(); 225 | 226 | return students; 227 | } 228 | ---- 229 | <1> *absolute* Remove package name. Metric uses name parameter if it exists, if not it uses the name of the class or method. 230 | <2> *name* Metric name (custom name) 231 | <3> *displayName* Human-readable name 232 | -- 233 | // ********************************************* 234 | ''' 235 | 236 | . View `@SimplyTimed` metrics 237 | + 238 | -- 239 | .Terminal 3 240 | [source,bash] 241 | ---- 242 | $ curl -i -s localhost:8080/metrics | grep -i "elapsedTime" | grep -v TYPE 243 | ---- 244 | .Terminal 3 Example Output 245 | .... 246 | application_listStudentsTime_elapsedTime_seconds 1.009317591 247 | base_REST_request_elapsedTime_seconds{class="org.acme.FrontendResource",method="listStudents"} 1.011809823 248 | .... 249 | 250 | 251 | NOTE: Notice some metrics have curly braces around them "{}". These are metric tags that subset a metric. See the https://github.com/jclingan/mp-metrics-tags[metrics-tags example] to see metric tags in action. 252 | 253 | -- 254 | + 255 | // ********************************************* 256 | ''' 257 | 258 | . View `@Counted` metrics 259 | + 260 | -- 261 | .Terminal 3 262 | [source,bash] 263 | ---- 264 | $ curl -i localhost:8080/metrics/application | grep -i count | grep -v TYPE 265 | ---- 266 | .Terminal 3 Example Output 267 | .... 268 | application_FrontendCounter_listStudentsFallback_total 0.0 269 | application_FrontendCounter_hello_total 0.0 270 | application_FrontendCounter_FrontendResource_total 1.0 271 | application_FrontendCounter_listStudents_total 2.0 272 | .... 273 | -- 274 | + 275 | // ********************************************* 276 | ''' 277 | 278 | . Add a field to hold the size of the student list 279 | + 280 | -- 281 | .FrontendResource.java 282 | [source,java] 283 | ---- 284 | public class FrontendResource { 285 | int numStudents; <1> 286 | ---- 287 | <1> Add this 288 | -- 289 | + 290 | // ********************************************* 291 | ''' 292 | 293 | . Assign list size to `numStudents` in `listStudents()` 294 | + 295 | -- 296 | .FrontendResource.java 297 | [source,java] 298 | ---- 299 | public List listStudents() { 300 | List students = student.listStudents(); 301 | numStudents = students.size(); <1> 302 | 303 | return students; 304 | } 305 | ---- 306 | <1> Add this 307 | -- 308 | + 309 | // ********************************************* 310 | ''' 311 | 312 | . Replace Fallback class with Fallback method 313 | + 314 | -- 315 | .FrontendResource.java 316 | [source,java] 317 | ---- 318 | @CircuitBreaker( 319 | requestVolumeThreshold = 4, 320 | failureRatio = 0.5, 321 | delay = 10000, 322 | successThreshold = 2) 323 | // @Fallback(value = ListStudentsFallbackHandler.class) <1> 324 | @Fallback(fallbackMethod = "listStudentsFallback") <2> 325 | ---- 326 | <1> Comment out @Fallback that falls back to a handler class 327 | <2> Create a new @Fallback that falls back to method 328 | -- 329 | + 330 | // ********************************************* 331 | ''' 332 | 333 | . Create a Fallback method 334 | + 335 | -- 336 | .FrontendResource.java 337 | [source,java] 338 | ---- 339 | public List listStudentsFallback() { 340 | List students = Arrays.asList( 341 | "Smart Sam", 342 | "Genius Gabby", 343 | "A-Student Angie", 344 | "Intelligent Irene"); 345 | 346 | numStudents = students.size();<1> 347 | 348 | return students; 349 | } 350 | ---- 351 | <1> This method also stores the student size 352 | -- 353 | + 354 | // ********************************************* 355 | ''' 356 | 357 | . Create a gauge to display number of students 358 | + 359 | -- 360 | .FrontendResource.java 361 | [source,java] 362 | ---- 363 | @Gauge(unit = MetricUnits.NONE, name = "numberOfStudents", 364 | absolute = true) 365 | public int getNumberOfStudents() { 366 | return numStudents; 367 | } 368 | ---- 369 | -- 370 | + 371 | // ********************************************* 372 | ''' 373 | 374 | . "Prime" numStudents by calling listStudents() 375 | + 376 | -- 377 | .Terminal 3 378 | [source,bash] 379 | ---- 380 | $ curl -i localhost:8080/frontend/list 381 | ---- 382 | .Terminal 3 Output 383 | .... 384 | HTTP/1.1 200 OK 385 | Content-Length: 41 386 | Content-Type: application/json 387 | 388 | ["Duke","John","Jane","Arun","Christina"] 389 | .... 390 | -- 391 | + 392 | // ********************************************* 393 | ''' 394 | 395 | . Get number of students using the gauge 396 | + 397 | -- 398 | .Terminal 3 399 | [source,bash] 400 | ---- 401 | $ curl -i -s localhost:8080/metrics/application | \ 402 | grep -i application_numberOfStudents 403 | ---- 404 | .Terminal 3 Output 405 | .... 406 | # TYPE application_numberOfStudents gauge 407 | application_numberOfStudents 5.0 408 | .... 409 | -- 410 | 411 | Another useful example, beyond this tutorial, is the https://github.com/jclingan/mp-health-metrics[health-metrics example], which demonstrates the use of the metrics repository API. 412 | -------------------------------------------------------------------------------- /docs/microprofile-rest-client.adoc: -------------------------------------------------------------------------------- 1 | == MicroProfile Rest Client 2 | 3 | This section creates a "frontend" service that utilizes the type-safe MicroProfile Rest Client API to invoke the student service. Additional Quarkus extensions (aka maven dependencies) are also added to support upcoming sections well. 4 | 5 | . Create frontend project using mvn command line 6 | + 7 | -- 8 | .Terminal 2 9 | [source,bash] 10 | ---- 11 | $ cd tutorial/working 12 | $ mvn io.quarkus:quarkus-maven-plugin:1.3.0.Final:create \ 13 | -DprojectGroupId=org.acme \ 14 | -DprojectArtifactId=frontend \ 15 | -DclassName="org.acme.FrontendResource" \ 16 | -Dpath="/frontend" \ 17 | -Dextensions="resteasy-jsonb,metrics,rest-client,fault-tolerance" 18 | ---- 19 | -- 20 | + 21 | // ********************************************* 22 | ''' 23 | 24 | . Open frontend project in your IDE 25 | + 26 | // ********************************************* 27 | ''' 28 | 29 | . Start frontend in Quarkus dev mode 30 | + 31 | -- 32 | .Terminal 2 33 | [source,bash] 34 | ---- 35 | $ mvn compile quarkus:dev 36 | ---- 37 | -- 38 | + 39 | // ********************************************* 40 | ''' 41 | 42 | 43 | . Create `src/main/java/org/acme/StudentRestClient.java` and paste in the following content 44 | + 45 | -- 46 | .frontend/src/main/java/org/acme/StudentRestClient.java 47 | [source,java] 48 | ---- 49 | package org.acme; 50 | 51 | import java.util.List; 52 | 53 | import javax.ws.rs.GET; 54 | import javax.ws.rs.Path; 55 | import javax.ws.rs.Produces; 56 | import javax.ws.rs.core.MediaType; 57 | 58 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 59 | 60 | @RegisterRestClient(baseUri = "http://localhost:8082") 61 | @Path("/student") 62 | public interface StudentRestClient { 63 | @GET 64 | @Produces(MediaType.TEXT_PLAIN) 65 | public String hello(); 66 | 67 | @GET 68 | @Path("/list") 69 | @Produces(MediaType.APPLICATION_JSON) 70 | public List listStudents(); 71 | } 72 | ---- 73 | -- 74 | + 75 | // ********************************************* 76 | ''' 77 | 78 | . Inject `StudentRestClient` into `FrontendResource.java` 79 | + 80 | -- 81 | .FrontendResource.java 82 | [source,java] 83 | ---- 84 | @Inject 85 | @RestClient 86 | StudentRestClient student; 87 | ---- 88 | -- 89 | + 90 | // ********************************************* 91 | ''' 92 | 93 | . Change `hello()` method to invoke student service student service `hello` endpoint 94 | + 95 | -- 96 | .FrontendResource.java 97 | [source,java] 98 | ---- 99 | @GET 100 | @Produces(MediaType.TEXT_PLAIN) 101 | public String hello() { 102 | return student.hello(); // <1> 103 | } 104 | ---- 105 | <1> Replace `"hello"` with `student.hello()`, as shown 106 | -- 107 | 108 | . Check endpoint works properly 109 | + 110 | -- 111 | .Terminal 3 112 | [source,bash] 113 | ---- 114 | $ curl -i localhost:8080/frontend 115 | ---- 116 | 117 | .Terminal 3 Output 118 | .... 119 | HTTP/1.1 200 OK 120 | Content-Length: 5 121 | Content-Type: text/plain;charset=UTF-8 122 | 123 | Howdy 124 | .... 125 | -- 126 | + 127 | // ********************************************* 128 | ''' 129 | 130 | . Remove `baseURI` parameter from `@RegisterRestClient` so it can be configured using a property 131 | + 132 | -- 133 | .StudentRestClient.java 134 | [source,java] 135 | ---- 136 | @RegisterRestClient <1> 137 | ---- 138 | <1> Removed `(baseUri = "http://localhost:8082")` 139 | -- 140 | + 141 | // ********************************************* 142 | ''' 143 | 144 | . Configure rest client `baseUri` in `microprofile-config.properties` 145 | + 146 | -- 147 | .frontend microprofile-config.properties 148 | [source,properties] 149 | ---- 150 | org.acme.StudentRestClient/mp-rest/uri=http://localhost:8082 151 | ---- 152 | -- 153 | + 154 | // ********************************************* 155 | ''' 156 | 157 | . Check endpoint 158 | + 159 | -- 160 | .Terminal 3 161 | [source,bash] 162 | ---- 163 | $ curl -i localhost:8080/frontend 164 | ---- 165 | 166 | .Terminal 3 Output 167 | .... 168 | HTTP/1.1 200 OK 169 | Content-Length: 5 170 | Content-Type: text/plain;charset=UTF-8 171 | 172 | Howdy 173 | .... 174 | -- 175 | + 176 | // ********************************************* 177 | ''' 178 | 179 | . Update `@RegisterRestClient` annotation to specify `configKey` in `StudentRestClient.java` 180 | + 181 | -- 182 | .StudentRestClient.java 183 | [source,java] 184 | ---- 185 | @RegisterRestClient(configKey = "StudentService") 186 | ---- 187 | -- 188 | + 189 | // ********************************************* 190 | ''' 191 | 192 | . Update the frontend `src/main/resources/META-INF/microprofile-config.properties` to utilize the `configKey` 193 | + 194 | -- 195 | .frontend microprofile-config.properties 196 | [source,properties] 197 | ---- 198 | StudentService/mp-rest/uri=http://localhost:8082 199 | ---- 200 | -- 201 | + 202 | // ********************************************* 203 | ''' 204 | 205 | . Check endpoint 206 | + 207 | -- 208 | .Terminal 3 209 | [source,bash] 210 | ---- 211 | $ curl -i localhost:8080/frontend 212 | ---- 213 | .Terminal 3 Output 214 | .... 215 | HTTP/1.1 200 OK 216 | Content-Length: 5 217 | Content-Type: text/plain;charset=UTF-8 218 | 219 | Howdy 220 | .... 221 | -- 222 | + 223 | // ********************************************* 224 | ''' 225 | 226 | . Add `listStudents()` method to `FrontendResource.java`. 227 | + 228 | -- 229 | .FrontendResource.java 230 | [source,java] 231 | ---- 232 | @GET 233 | @Produces(MediaType.APPLICATION_JSON) 234 | @Path("/list") 235 | public List listStudents() { 236 | List students = student.listStudents(); 237 | 238 | return students; 239 | } 240 | ---- 241 | -- 242 | + 243 | // ********************************************* 244 | ''' 245 | 246 | . Specify a `StudentRestClient readTimeout` in frontend `microprofile-config.properties` that will throw an exception if read time threshold is exceeded 247 | + 248 | -- 249 | .frontend microprofile-config.properties 250 | [source,properties] 251 | ---- 252 | StudentService/mp-rest/readTimeout = 1000 <1> 253 | ---- 254 | <1> Add this 255 | -- 256 | + 257 | // ********************************************* 258 | ''' 259 | 260 | . Check endpoint, which should result in a "java.net.SocketTimeoutException: Read timed out" because Student doDelay() method is set at a 2000ms delay. 261 | + 262 | -- 263 | .Terminal 3 264 | [source,bash] 265 | ---- 266 | $ curl -i localhost:8080/frontend/list 267 | ---- 268 | .Terminal 3 Output 269 | .... 270 | # Stack trace ... 271 | Unable to invoke request: java.net.SocketTimeoutException: Read timed out 272 | # Stack trace ... 273 | .... 274 | .Terminal 2 Output 275 | .... 276 | # Stack trace ... 277 | Unable to invoke request: java.net.SocketTimeoutException: Read timed out 278 | # Stack trace ... 279 | .... 280 | 281 | .Terminal 1 Output 282 | .... 283 | ** Waiting 2000ms ** 284 | .... 285 | -- 286 | + 287 | // ********************************************* 288 | ''' 289 | 290 | . Comment out the `readTimeout` property in `microprofile-config.properties` to avoid exception 291 | + 292 | -- 293 | .frontend microprofile-config.properties 294 | [source,properties] 295 | ---- 296 | #StudentService/mp-rest/readTimeout = 1000 <1> 297 | ---- 298 | <1> Comment this out 299 | -- 300 | + 301 | // ********************************************* 302 | ''' 303 | 304 | . Check endpoint 305 | + 306 | -- 307 | .Terminal 3 308 | [source,bash] 309 | ---- 310 | $ curl -i localhost:8080/frontend/list 311 | ---- 312 | .Terminal 3 Output 313 | .... 314 | HTTP/1.1 200 OK 315 | Content-Length: 41 316 | Content-Type: application/json 317 | 318 | ["Duke","John","Jane","Arun","Christina"] 319 | .... 320 | 321 | .Terminal 1 Output 322 | .... 323 | ** Waiting 2000ms ** 324 | .... 325 | -- 326 | -------------------------------------------------------------------------------- /jwt/jwt-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss":"airhacks", 3 | "jti":"airhacks-jwt-unique-id-12342142", 4 | "sub":"user/43971", 5 | "upn":"demo@acme.org", 6 | "exp":3155882898, 7 | "myc":"My Custom Claim", 8 | "groups":[ 9 | "user", 10 | "admin" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /jwt/jwtenizr-config.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "mpConfigIssuer": "airhacks", 4 | "mpConfigurationFolder": ".", 5 | "privateKey": "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0FHpPBXGiVPkTZ6adATConO2Z5ZFOFThkDdX37i+M5eg2ZIpast+otoq4yuJz2ilhCLzrI7WjSoKGQCeVFjtnK1xoWpx6Fg5ICFyCFg2fhbOLvQPaUkeP9h+UUp2yzJLI3OowgEzE1ZlbHyQhUNGtVqayYXYWjgODvZx7wqIxKWqRaseJJt9alrZpCu710SESsk9/Tl8bNBbC286+0sKn+0tersaZMTZckl2IKTcJ0Tc4/8VBFbJ5G0eTpSriGPNdNfpaetvvENkKNC5t9Zir3Kgg163V4wVYVRdGwAlBswkoiy1wK/CY7fmI+MWt+1ZMAM6HenMgBFWpbC6kM3t/AgMBAAECggEAEig+bNlPq96Ffr9kvU/xaA7qpinPjN/235/Fg0ow4TQNRe69RIfkpU5Cjn9mdef5ZB9NbSgcmGpa3s55Lz8enjhxCNSaEnVG21x8d2Fh3kPM5roYOVsyfAGFDZ+G5xBZfIdJ1boaA37GB7oMGo/blG64QCQzk/KdyBrGPHRThkFLFKQ4/FPiItoMbuNsn1LkzoHsysE/zTlINcU6aR6AC1lGd6XnKTZ3oNmsEE90c3Ka2lRNKkBez0tF2vhSqNto3G9baRQCYc+s1hObSOkD63xbdSmqW3r92dROiFLFSW0bLeSV+Vfl8mdcIDAYhxB6z8Ag+NamxxUgN8msGnZDAQKBgQD26tDRlkdDETapo+2HhgOHIcFNZjJRgYcxk2BAl8dneEjrWDFXVe925ayRJ1n5H65+fXudH6HGuBqeS5epH6inpz2yhu4A2VW6xDLVr3RYNCqQduYWSU3HrITVAyrCAeoE3XY/zYaCkrxhCEzHrl+jtc2mx4Mwv5Gxacd8b/sZKQKBgQC6tEPkkkwlA3q7OQiAvosd6ntq+260Zn7QxFjWHqQa9bBx9yJcs4rbsbvEjZzZYrvkPbfvKPyOyejoQ8jLef2OWem4UCna3AE22EYZCLiKrP4QyVubEaT+4QmU1dF/LvTZGOHzS1RBnYrpaUOE2XK26rIyj1JkjnnMCfPjAyj8ZwKBgHEadrTKpbHu/J8QXAmsNYl8yNLvcTUspATtgTWVUN3wl1ZWe6OJ1bsc0cNbxzb6cC+4wxriFnS0eFzyO3JY8iR50yUn8XWqGD8JCFAhoqWUn7q0/AhRY7OHLwF0Be4AenXoC/aF6ot9M5Uu/Byrn1uaN62hzlJSLiaaUaqKkPahAoGBAIe63VCh+rE94k+RmtjdrQ1GlgqkV0rW6IKk+2BOTkc7LdbsLxLtg5lLkJHH0atH2AiJxdocFd9IcgXIoYUrXmXKf9r6jDMLleZlPDCe4moOcXWKrQoCvcx9lRWai/7GCEJ7kGfq74n0GjStzeQaoQbRgFK5VtC7s1feNGtotLcpAoGBALOvdaGXfj2KCZFk45yykY3rfl6992Nh6GYFkCyN82VmFQfGZLgEH8hBi2YzMJiVmuOXn0jZqi0MZ2L2OTF056FCrWNgakoLK9VI40COiPS5GZkAAx/RU18chTpds5YW67C/gFZyXVfYoIhR2ImbPb/u1zwjGOADP0ZAZMm9RdXZ", 6 | "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBR6TwVxolT5E2emnQEwqJztmeWRThU4ZA3V9+4vjOXoNmSKWrLfqLaKuMric9opYQi86yO1o0qChkAnlRY7ZytcaFqcehYOSAhcghYNn4Wzi70D2lJHj/YflFKdssySyNzqMIBMxNWZWx8kIVDRrVamsmF2Fo4Dg72ce8KiMSlqkWrHiSbfWpa2aQru9dEhErJPf05fGzQWwtvOvtLCp/tLXq7GmTE2XJJdiCk3CdE3OP/FQRWyeRtHk6Uq4hjzXTX6Wnrb7xDZCjQubfWYq9yoINet1eMFWFUXRsAJQbMJKIstcCvwmO35iPjFrftWTADOh3pzIARVqWwupDN7fwIDAQAB" 7 | } -------------------------------------------------------------------------------- /jwt/jwtenizr.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jclingan/oreilly-microprofile-tutorial/2fe47b3e298e20e0d5a490aa2792739e3bebfb90/jwt/jwtenizr.jar -------------------------------------------------------------------------------- /jwt/jwtenizr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASEDIR=$(dirname $0) 3 | java -Dverbose -jar ${BASEDIR}/jwtenizr.jar "$@"% 4 | -------------------------------------------------------------------------------- /jwt/microprofile-config.properties: -------------------------------------------------------------------------------- 1 | #generated by jwtenizr 2 | #Fri Jan 10 02:14:43 PST 2020 3 | mp.jwt.verify.issuer=airhacks 4 | mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBR6TwVxolT5E2emnQEwqJztmeWRThU4ZA3V9+4vjOXoNmSKWrLfqLaKuMric9opYQi86yO1o0qChkAnlRY7ZytcaFqcehYOSAhcghYNn4Wzi70D2lJHj/YflFKdssySyNzqMIBMxNWZWx8kIVDRrVamsmF2Fo4Dg72ce8KiMSlqkWrHiSbfWpa2aQru9dEhErJPf05fGzQWwtvOvtLCp/tLXq7GmTE2XJJdiCk3CdE3OP/FQRWyeRtHk6Uq4hjzXTX6Wnrb7xDZCjQubfWYq9yoINet1eMFWFUXRsAJQbMJKIstcCvwmO35iPjFrftWTADOh3pzIARVqWwupDN7fwIDAQAB 5 | -------------------------------------------------------------------------------- /jwt/token.jwt: -------------------------------------------------------------------------------- 1 | eyJraWQiOiJqd3Qua2V5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJ1c2VyXC80Mzk3MSIsInVwbiI6ImRlbW9AYWNtZS5vcmciLCJteWMiOiJNeSBDdXN0b20gQ2xhaW0iLCJhdXRoX3RpbWUiOjE1Nzg2NTEyODMsImlzcyI6ImFpcmhhY2tzIiwiZ3JvdXBzIjpbInVzZXIiLCJhZG1pbiJdLCJleHAiOjMxNTU4ODI4OTgsImlhdCI6MTU3ODY1MTI4MywianRpIjoiYWlyaGFja3Mtand0LXVuaXF1ZS1pZC0xMjM0MjE0MiJ9.Eaqe3sTH64doIVW3on25EA_uD9XrfppndiweUNLVbFK3KxaIfXaAdQ4N9IkQG6Iw0A7I7kngjeSHwb2DzH8rQE8yp7sCtey6kmC689eQC0j2k-YbyGZ68xnsMj5taOBVGH_ZSWC6E1L-Gk-GgcTvX6I3SaBC8pwZ267q6psknqlAtfD2JoE7ezEb7LrLVwP1vaGqKzC2X6pv5J-07DNBqe75uBWQyqX_WE856ug3uqWcHtNck8nqU6VhwXqxHZ6vkRlx9VoMgFUF851D-WuKMCUdfXJHekDyKmjYuyLiw7jtQSdliY3ONOXgFm_uzjKGuZ1VKPdQXyx7GQ9NsNTYfw -------------------------------------------------------------------------------- /working/README.adoc: -------------------------------------------------------------------------------- 1 | This directory is a convenience location for implementing the tutorial steps. 2 | There is nothing in the tutorial that requires using this directory. 3 | --------------------------------------------------------------------------------