├── .gitignore ├── Makefile ├── README.md ├── REQUIREMENTS.md ├── SOLUTION.md ├── delivered ├── cloudconf2019 │ └── README.md └── cloudconf2020 │ └── README.md ├── draw ├── general-monitoring-infra.xml ├── influxdb-monitoring-infra.xml ├── prometheus-monitoring-infra.xml ├── tracing-infra-general.drawio └── tracing-infra-otel-jaeger-influxdb.drawio ├── latex-tpl └── listings-setup.tex ├── lesson01-getting-started ├── README.md └── SOLUTIONS.md ├── lesson02-logging ├── README.md └── SOLUTIONS.md ├── lesson03-influxdb ├── README.md ├── SOLUTIONS.md ├── docker-compose.yaml ├── jaeger │ ├── Dockerfile │ └── influxdb-plugin │ │ └── config.yaml └── telegraf │ └── telegraf.conf ├── lesson04-tracing ├── README.md └── SOLUTIONS.md ├── lesson0x-justforpro └── README.md └── patches ├── 0001-feat-discount-Added-healthcheck.patch ├── 0001-feat-discount-Added-logging-support.patch ├── 0001-feat-discount-added-tracing.patch ├── 0001-feat-frontend-Added-healthcheck-endpoint.patch ├── 0001-feat-frontend-Added-logging.patch ├── 0001-feat-frontend-Instrument-http-handlers.patch ├── 0001-feat-items-Added-healtcheck-endpoint.patch ├── 0001-feat-items-Injected-logger.patch ├── 0001-feat-items-tracing-instrumentation-with-b3-and-openc.patch ├── 0001-feat-pay-Added-healthcheck.patch ├── 0001-feat-pay-add-log4j2.patch └── 0001-fix-pay-Trace-with-B3-and-opentelemetry.patch /.gitignore: -------------------------------------------------------------------------------- 1 | workshop.pdf 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | pandoc --highlight-style kate \ 3 | --listings \ 4 | -H ./latex-tpl/listings-setup.tex \ 5 | -V pagestyle=empty \ 6 | -s README.md REQUIREMENTS.md \ 7 | ./lesson01-getting-started/README.md \ 8 | ./lesson02-logging/README.md \ 9 | ./lesson03-influxdb/README.md \ 10 | ./lesson04-tracing/README.md \ 11 | ./lesson0x-justforpro/README.md \ 12 | ./SOLUTION.md \ 13 | ./lesson01-getting-started/SOLUTIONS.md \ 14 | ./lesson02-logging/SOLUTIONS.md \ 15 | ./lesson03-influxdb/SOLUTIONS.md \ 16 | ./lesson04-tracing/SOLUTIONS.md \ 17 | --toc \ 18 | -o workshop.pdf 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Application Monitoring" 3 | author: Gianluca Arbezzano 4 | geometry: "left=3cm,right=3cm,top=2cm,bottom=2cm" 5 | fontfamily: helvet 6 | output: pdf_document 7 | --- 8 | 9 | \newpage 10 | 11 | Microservices, cloud computing, DevOps, containers changed the way to write applications. 12 | Nowadays we have smaller, much more distributed and replicated applications. 13 | Sometime even across different languages. 14 | 15 | A steam of logs is not enough to understand what it is going on. We need 16 | correlation between requests across the board. We need more context to be able 17 | to tell the story of our distributed system. 18 | 19 | Now that developers are in the loop of running their application they need 20 | different tools compared with what sysadmin are used to have. Because they need 21 | to understand what it is happening inside their application. 22 | 23 | That's why I developed this course. Because I think **application 24 | instrumentation** will splay an important part in our journey to understand and 25 | troubleshoot what is going now in our applications. 26 | 27 | ## Target 28 | 29 | * Developers 30 | * Solution Architect 31 | * DevOps 32 | 33 | ## Practical 34 | The practical part of the course is based on the code located at 35 | [gianarb/shopmany](https://github.com/gianarb/shopmany) 36 | 37 | ## Material 38 | 39 | This PDF is long, only because it contains all the code changes in form of git 40 | patches. Otherwise it will be just a couple of page long. 41 | 42 | This morning we learned in theory what tracing, logging, reliability means. This 43 | afternoon we are gonna see it in practice. The exercise are divided in 4 44 | lessons. The same areas we spoke about this morning 45 | 46 | 1. Lesson 1 - Health check 47 | 2. Lesson 2 - Logging 48 | 3. Lesson 3 - Infrastructure monitoring with InfluxDB & Jeager 49 | 3. Lesson 4 - Tracing 50 | 51 | The PDF as I said contains the solution for those exercise, you can use them as 52 | inspiration in case your blocked or to move forward with the servicesa that are 53 | written in language you do not know about. 54 | 55 | It is not easy to copy/paste from a PDF, that's why you still have the raw 56 | patches in the [gianarb/workshop-observability](https://github.com/gianarb/workshop-observability) repository 57 | under the `./patches` directory. 58 | 59 | Or as a branch to [gianarb/shopmany](https://github.com/gianarb/shopmany). 60 | 61 | ## Timeline 62 | This is an example of timeline that I used at the CloudConf 2019 in Italy. 63 | 64 | 09.00 Registration and presentation 65 | 09.30 - 13.00 Theory 66 | 67 | * Observability vs monitoring 68 | * Logs, events and traces 69 | * How a monitoring infrastructure looks like: InfluxDB, Prometheus, Jaeger, 70 | Zipkin, Kapacitor, Telegraf... 71 | * Deep dive on InfluxDB and the TICK Stack 72 | * Deep dive on Distributed Tracing 73 | 74 | 13.00 - 14.00 Launch 75 | 14.00 - 17.00 Let's make our hands dirty 76 | 17.30 - 18.00 Recap, questions and so on 77 | 78 | ## Credits 79 | This is probably one of the most important section! Instrumenting application is 80 | hard because you need to build agreement. We know as a developer how 81 | "complaining oriented" we are as a category. There are big communities, people 82 | that are working to make all of this easy and possible. You will find a chapter 83 | "Link" in every lesson with some of the blog posts I wrote and read about this 84 | topic. Here I would like to share with you some of the people you should follow 85 | if you are looking for inspiration around these topics: 86 | 87 | * [Yuri Shkuro](https://github.com/yurishkuro) Opentracing and Jaeger Contributor 88 | * [Charity Major](https://twitter.com/mipsytipsy) CTO of HoneyComb and pioneer of "Observability". 89 | * [JDB](https://twitter.com/rakyll) Engineer at Google. 90 | * [Brendan Gregg](http://www.brendangregg.com/) Performance Engineer at Netflix 91 | * [InfluxData](https://influxdata.com) the company behind InfluxDB and its 92 | founder [Paul Dix](https://twitter.com/pauldix). 93 | 94 | \newpage 95 | -------------------------------------------------------------------------------- /REQUIREMENTS.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | Along the course the attendees will modify one or more applications (based on 4 | their skill with the languages) to achieve a better visibility of the system. 5 | The applications are written using: 6 | 7 | 1. Golang (no framework required and we use `gomod` to manage dependencies` 8 | 2. Java (SpringBoot and gradle) 9 | 3. PHP (Zend Expressive and composer to manage external libraries) 10 | 4. JS/Node (expressive as framework and npm to manage external libraries) 11 | 12 | You do not need to know all the languages, but to execute the practical session 13 | of the course you will to know at least one of those languages. 14 | 15 | The applications, even the one that uses framework are very easy to approach. 16 | Just a couple of lines of code and classes. The course if not about how good you 17 | are at coding. 18 | 19 | We will use `git` to checkout and follow the course. Nothing crazy just command 20 | like: `checkout`, `clone`, `cherry-pick`. 21 | 22 | We will `docker` and `docker-compose` to spin up an down applications. Even here 23 | nothing advanced but if can build a bit of confidence with these tools it will 24 | be way easier for you to follow the course. 25 | 26 | # Prerequisiti (italian) 27 | 28 | Verra' richiesto ai partecipanti di modificare ed evolvere una o piu' 29 | applicazioni a loro scelta. Le applicazioni sono scritte in: 30 | 31 | 1. Golang (nessun framework utilizzato ma useremo gomod per gestire alcune 32 | librerie esterne) 33 | 2. Java (SpringBoot e gradle) 34 | 3. PHP (Zend Expressive e composer per gestire alcune librerie esterne) 35 | 4. JS/Node (expressive e npm per gestire alcune librerie esterne) 36 | 37 | Non e' necessario conoscere tutti i linguaggi, ma per poter eseguire la parte 38 | pratica e' necessario poter modificare almeno una di queste applicazioni 39 | 40 | Tutte sono a livello di codice e framework utilizzati molto semplici. Poche 41 | classi, poche linee di codice. Quindi non serve una conoscenza approfondita. 42 | 43 | Utilizzeremo git per spostarci da una versione all'altra del codice. Comandi 44 | base: `clone`, `checkout`, `cherry-pick`. Niente di avanzato. 45 | 46 | Utilizzeremo intensamente `docker` e `docker-compose` per poter fare il setup 47 | delle varie applicazioni. Anche qui nulla di avanzato ma arrivare con entrambi i 48 | tools installati e con un minimo di consapevolezza dei comandi di base rendera' 49 | il corso molto piu' facile da seguire. 50 | 51 | \newpage 52 | -------------------------------------------------------------------------------- /SOLUTION.md: -------------------------------------------------------------------------------- 1 | # SOLUTION 2 | 3 | \newpage 4 | -------------------------------------------------------------------------------- /delivered/cloudconf2019/README.md: -------------------------------------------------------------------------------- 1 | Applicazione: https://github.com/gianarb/shopmany 2 | 3 | Materiale e lezioni: https://github.com/gianarb/workshop-observability 4 | 5 | Form post workshop: https://goo.gl/forms/rEX4KJly0rw5YmN93 6 | 7 | Slides: https://goo.gl/cL4bhi 8 | 9 | Partecipanti: 20 10 | -------------------------------------------------------------------------------- /delivered/cloudconf2020/README.md: -------------------------------------------------------------------------------- 1 | Applicazione: https://github.com/gianarb/shopmany 2 | 3 | Materiale e lezioni: https://github.com/gianarb/workshop-observability 4 | 5 | Form post workshop: https://forms.gle/wQPU5dXvnSm6178C8 6 | 7 | Slides: http://bit.ly/reliability-workshop-slide 8 | 9 | Numbero Partecipanti: 10 | -------------------------------------------------------------------------------- /draw/general-monitoring-infra.xml: -------------------------------------------------------------------------------- 1 | 7VnbcpswEP0aP6aDEcjkMXEufWg77bgzTR5lWC6tQFTIMeTrK4xkgcmtrW3CJE/WHq2W1a7OsWwmaJ6W15zk8WcWAJ3YVlBO0MXEtqcYOfKjRqoG8VwFRDwJlJMBFsk9KNBS6CoJoOg4CsaoSPIu6LMsA190MMI5W3fdQka7T81JBD1g4RPaR38kgYj1LiyDf4QkivWTp5aaSYl2VkARk4CtWxC6nKA5Z0w0o7ScA62Lp+vSrLt6ZHabGIdMvGRBMQ8p/hLR7P46zctvc/cqDU48lZuo9IYhkPtXJuMiZhHLCL006DlnqyyAOqolLePzibFcglMJ/gQhKtVMshJMQrFIqZqFMhE3rfFtHeqDq6yLUkXeGJUymjzr5B7dvoIKtuI+PLFnfYwIj0A84edumyRPN7AUBK/kOg6UiOSumwdRxyza+plOyIFqxl80RsW9I3SlnvQdKKjYmMq8z5dcjqJ6FEEGnAjGe93s9modJwIWOdmUZy0J2+1LmFA6Z1SGqdeiMAxt35d4ITj7Ba2ZAC+xi7dduQMuoHy6L/066gWa6UogbE2gtaHbVHMoblENWwcq/elYOCErzKubttGssl1tm3Ub6wBksl9IJjwkmewemTSHipxknV7j36takBuqnBQNV86kS8Z4Sqhx0Nzr0VJm2ERt5tts/Z/n7C/jllo8kvH4ReR0aBHRCY1XRY4oImgMIoLeReRtiQjyBheRaa+Mr15EjG7c6hjHERFnDCLivIvI2xIRZza4iDjjE5Hhfs+4L1SR2ZAq4vZURJ5mCv6+/wEIXPAC5yHKePYS4T1RBjmv7vLujp0yR2QMPhJjNkvPOCdVyyFnSSaKVuSvNWDOlrsrx9OdP0mf8Ucza+c0NRmYs7Xdyr8fN3zAS0FLGA7/FTuMXgx/T5/1GriQFa/fLOy1ugS88MELDPY9WIb7qa6Ld6rrHK660jTvPxo2mbdI6PIP -------------------------------------------------------------------------------- /draw/influxdb-monitoring-infra.xml: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /draw/prometheus-monitoring-infra.xml: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /draw/tracing-infra-general.drawio: -------------------------------------------------------------------------------- 1 | 7VhNj5swEP01OTYCE0hybLLZ9tBqK6VSu6eVgQHcGoyMyUd/fQdiApRslmqzSRP1knieZ/wx4/dGYmDN480HSdPos/CBD4jhbwbW3YAQ07FG+Fcg2x0ysTUQSuZrpxpYsl+gQUOjOfMhazkqIbhiaRv0RJKAp1oYlVKs226B4O1dUxpCB1h6lHfRb8xXUXULo8Y/AgujamfT0DMxrZw1kEXUF+sGZC0G1lwKoXajeDMHXiSvyssu7v6Z2f3BJCSqT8DI9cjTdj1ZPbmGnT2w9EHcvxvrs6ltdWHw8f7aFFJFIhQJ5YsanUmRJz4Uqxpo1T6fhEgRNBH8AUptdTFprgRCkYq5noUNU9+L8KGtrcfGzN1Gr1wa28pIlNw2ggrzsTlXh5VWFbe7X3GpZ9OmoUzk0oMjuaqeH5UhqCN+1r64yAoQMeB5ME4Cp4qt2ueg+nmGe7+6gjjQRfyLgup1V5TneqcBcWiMhZklbpaWGTHepyn+mp3atyu7jpiCZUrLpKyR3u0qUs7CBMccAszGLGCczwUXslzKCoKAeB7imZLiJzRmfMd1bGdfmhVIBZvjxekmswqo2KbVhVT2uuaqOdVY1ORp5Xj6AhidtJ6RUmaDUDW9XqLUhQhFehJqeklCkQ6hWIJvOo/xuri9SJBfvCCAK3EUFiMPu+DruHUCapAe1DjEDOetiDH532p6M8PqyQz7ksyw+rcacnutxrIv32q6Hfy6Wk3Nr7en1Ogams3oRprNIXKctdnY/bXJuj1tssnltakr+f+8Ng1NYjb1yRwa0/ELClVaX0AyTBvI08uWcw2y5dyIbB3izVlla9pJ5FeJ95evy9QfmuRTmAQHNcnxJuAGp8ntiNjtljA+oEnkNMlFs/6OVs41vkZai98= -------------------------------------------------------------------------------- /draw/tracing-infra-otel-jaeger-influxdb.drawio: -------------------------------------------------------------------------------- 1 | 7VlNc5swEP01PiYD4iNwbGwnbaedZMaHNqeMDAsolREjZBv311fGwkAgMZk4Bmd6Mvu0K9Bbv10JRsZ4kd1ynEQ/mQ90hDQ/GxmTEUK6bZjyZ4tsdohjKSDkxFdOJTAjf0GBmkKXxIe05igYo4IkddBjcQyeqGGYc7auuwWM1u+a4BAawMzDtIn+Ir6IilVoJf4VSBgVd9Y1NbLAhbMC0gj7bF2BjOnIGHPGxO5qkY2BbskreNnF3bwwun8wDrHoEmDOPfS4WTurx7lmpXckuWM3F1fq2cSmWDD4cv3KZFxELGQxptMSveZsGfuwnVWTVunzg7FEgroEn0CIjUomXgomoUgsqBqFjIjf2/BLS1kPlZFJpmbOjU1hxIJvKkFb86E6VoblVhG3W992US/SpqCULbkHr3BV/P0wD0G84mfskytVAWwB8nlkHAeKBVnVnwOrv2e49yszKC9UEt+QUDXvCtOlutMtk/aX+/tGput5XEdEwCzBOQVrKeZ6zjAlYSyvKQRy7dcBoXTMKOP5VEYQBMjzJJ4Kzv5AZcS357Zl7xOxAi4gez0VTeqKgEJbqpagwl6XytRdhUVVVRaOx6dba9B6QgHpFfmUYjokoJ7kgzrKx+1TPqghH5bIlQKFfP6LkL1PRkdQAeqggjYR2B+lAed/D+ksAqOjCKw+RWA0RPAdr/Bn7SKG1X8X0Ru0nlcXKfX08RIyz6GPmAf6yFMuqIF1kjYlnLSTWM3CM/usZcdC/ZcddH5l51JHerX06Jeae3Wg+OTWPXAiaQN+/Ipkn0NFsg9VpHRw9ahNIietR7o9HIGgwTdmt6MM9H5fkPT6yuvM9lqdU2r2mlJ3KCntev4cfkJ7PYC6zX0g4FC27nd1qGfbPh+DE7Ru+2zPgXlwnJ5mIqu+x75q2fahkza15vH+WxzQZTa5Piq/geNB+7Z67limpR2HX1t/dobRe+e3efabUpwK4s0Acy/6BCSbbS/eT0ty86g4xmmKY5+/81w9DILdjyNYmuVHx3ys8unWmP4D7Vxdl5o4GP41Xk6PJCTg5Yzaz53unE57duZqD0JUutFYjFX76zdoEEmy6lQhI+yV5IUE8rzfbxJbsDtZvUuC2fieRYS2QDtatWCvBYCDoSt+Usp6S/GRJIySOJIP5YTH+BeRxLakLuKIzAsPcsYoj2dFYsimUxLyAi1IErYsPjZktPjWWTAiGuExDKhO/SuO+DibRTunvyfxaJy92WnLO5Mge1gS5uMgYss9Euy3YDdhjG+vJqsuoSl4GS7zL1+/TJ8+fYbPf+JF7+P9319R92Y72NuXdNlNISFT/ttD+z/AYDZ/aq+/LdeLfj+OP9H7G+htx/4Z0IUETE6WrzMEE7aYRiQdpd2Cd8txzMnjLAjTu0shM4I25hMqWo64HMaUdhllyaYvjBDxI1fQ5zxh/5C9Oz4YQIzFHfkBJOFkpbDsyHydHROE9BI2ITxZi35ylIzDUm47srnMhQB6kjbeFwBXEgMpeKPdyDm44kLi+wKsHVfD+iMR4pdspJ9SIf0CnIuiHxB/GJrQx6FPBsNS0S/C7wIdfwcY8MelwY80+D9Mh3Sx6t1dFPShH5LQCPpA2E7ULhF0D6Ii6I510P0mybyjyDy0Dn+nSfADBX7XNvzZy5oBP1TgR9bhh/W3+K5i8a27WaBHObUDHSmgW7fzoAGxDVZAt2/dcZOsu6tYd2wdfj11rTH8Sjbrerbh9xwNWxKNyKNssoSP2YhNA9rPqXdF9PNn/mBsJjH/TjhfyzJSsOCsyBGyivnT3vVzOtQbJFu9lRx501hnjamY79N+Y69X2sy7bVpZv+380kn9BicFMGyRhOSQAMvciAfJiPBDUTwyy0ZCaMDjn8XPu7yeNSqFw4qe+bb1DPvXp2dvsAsKunZE0zatB5LEAjKSVKV+nVPVz7WqfnoK/z1TP/EjoKiJ6kGlYuhYj6qhDv07Jtq3Dw/ngR7QeDQV15QMuSHIHg6BOciO8ACjMmvmvsICUzbZMbBgt2hycR5kYec1mb9imAFsxxnuf/D8ZPsluz6wOLU2O31Vqp1OlhVnQ2wNq+yliMHuM86QDEfTTjYTiBFKNkPfjNh5WlqWkjknaJlJyUqzc8JZq0he1qm8mkVAaIC64lVA3Ll2g2bdnuETA7e9VXoLgZurl4eaEbhB66UhVy8NNStwg6a1l4oDtyvMW19b4NapJHCDfsWBm55WXWfgZtKySgO37P17SH5mEREUfdlHYMHNxixMsReG6i5FLA4DeitvTOIo2qolmce/gsFmqBT/WSoam8mguxbqpWMJTZxvlbJUDmRrLrt4DmocwAYOgNI4oCchkgO3NeWAYjwM2+pQpQxoTO6CDNam4twFwf99+mGffjQnQecWk80+HSjrFliN67bBRmk+PZPDa5KMPJF9zsawGOxl2+qPZ7X+mRJ0ngnQt/c0I6tFBk9XbVaL9E0+zcpqkWkxttqsFuHrs3OvLKtFXikeUM1qsVNtVov0mtN1ZrUmLas2q9U3nMicqqshWI+cSslqEbKd1R44NvBjQeQbauDkPVDcVAqs7znIBq4/9MpZJWB9Py/Wazk1hV45NACsn9TAehWnptArRweA9bUyT6+XVRjR5lHs896do5m7V0jdj20vbJUc0WLprl/5Rl6sH4eqqZIpR0WA9c3y+MDR73pBr3h160UTrBdNaoq84tR3NRB70B84H1Uv6BWn7lg/du9ZXah5iVMv3Td7J/pmdO5mi/N05cBhtm8faqMoimM27i+/kKKIZv7XPNsiX/4HR7D/Lw== -------------------------------------------------------------------------------- /latex-tpl/listings-setup.tex: -------------------------------------------------------------------------------- 1 | % Contents of listings-setup.tex 2 | \usepackage{xcolor} 3 | 4 | \lstset{ 5 | basicstyle=\ttfamily, 6 | keywordstyle=\color[rgb]{0.13,0.29,0.53}\bfseries, 7 | stringstyle=\color[rgb]{0.31,0.60,0.02}, 8 | commentstyle=\color[rgb]{0.56,0.35,0.01}\itshape, 9 | showspaces=false, 10 | showstringspaces=false, 11 | showtabs=false, 12 | tabsize=2, 13 | captionpos=b, 14 | breaklines=true, 15 | breakatwhitespace=true, 16 | breakautoindent=true, 17 | escapeinside={\%*}{*)}, 18 | linewidth=\textwidth, 19 | basewidth=0.5em, 20 | } 21 | -------------------------------------------------------------------------------- /lesson01-getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | ## Lesson 1 3 | 4 | First of all we need to get in touch with the applications we will instrument 5 | and we will improve along this course. 6 | 7 | I built a shiny, great, new e-commerce. Check it out from 8 | [gianarb/shopmany](https://github.com/gianarb/shopmany) and read the README.md 9 | in order to run it. 10 | 11 | In short: 12 | 13 | ```bash 14 | $ git clone git@github.com:gianarb/shopmany.git 15 | $ cd shopmany 16 | $ git pull --all 17 | $ docker-compose up frontend 18 | ``` 19 | 20 | You can open your browser and visit the page: `3000`. ShopMany is a modern 21 | e-commerce for super nerds. 22 | 23 | ## Services 24 | 25 | As you can see from the shopmany's README.md there are different services in different 26 | languages. I am not expecting you to know all of them. I had a couple of friends 27 | that helped me to write them down too. 28 | 29 | Pick one, two or even all if you feel confident, but the goal is to instrument 30 | and code only what you know. 31 | 32 | Along the course we will get over all of them. I set it up in this way to tell 33 | you that observability and application instrumentation are practices that are 34 | cross languages. Because we have to understand what is going on overall. With 35 | the ability to zoom in specific applications if needed. 36 | 37 | At the end of the PDF you can find the solutions. You can look at them all 38 | together or later as you prefer. Some of them area easy as `git diff` divided by 39 | application. 40 | 41 | The diff contains the commit sha in the header. You can 42 | [cherry-pick](https://git-scm.com/docs/git-cherry-pick) the code 43 | for the applications that you are not developing or if you are blocked from the 44 | [gianarb/shopmany](https://github.com/gianarb/shopmany) repository. 45 | 46 | For example if you are not working in Java with the `pay` application you can 47 | cherry pick the java code via: 48 | 49 | ```bash 50 | git cherry-pick 51 | ``` 52 | 53 | ## Exercise: Health endpoint 54 | 55 | **Time: 20minutes** 56 | 57 | It is time to make our hands dirty. From now you need to have selected the set 58 | of applications or the application that you are gonna use along the course. 59 | Leave the others behind. 60 | 61 | The first exercise is to create an healthcheck endpoint. 62 | 63 | The goal for every `/health` endpoint is to give you information about the 64 | status of the running process. I saw a lot of bad implementation where the 65 | endpoint was just returning a printed JSON as response without doing any check. 66 | 67 | I would like you to create a new endpoint: 68 | 69 | ```bash 70 | PATH: /health 71 | METHOD: GET 72 | BODY: 73 | { 74 | "status": "healthy|unhelathy", 75 | "checks: [ 76 | { 77 | "name": "mysql", 78 | "status": "healthy", 79 | "error": "" 80 | } 81 | ] 82 | } 83 | ``` 84 | 85 | Based on the application you are modifying you need to check if the required 86 | dependencies are working. 87 | 88 | * `items` needs to check if `mysql` is working. 89 | * `frontend` needs to check if `item` is up and running. 90 | * `discount` needs to check if `mongodb` is up and running. 91 | * `pay` needs to check if `mysql` is up and running. 92 | 93 | If all the checks are `healthy` you return `200` as status code and the general 94 | status is `healthy`. If one of them is not you populate the `error` for that 95 | check, you mark it as unhealthy and the general status will become `unhealthy` 96 | too. 97 | 98 | All the checks needs to be `healthy` to mark the general status as `healthy`. 99 | 100 | ## Motivation 101 | 102 | A strong healthcheck is important to troubleshoot applications where you didn't 103 | write them. Because if across the company you agree on the same format the first 104 | things you can do is to check for that endpoint. 105 | 106 | Moving forward we will see how to use it for automation and monitoring. 107 | 108 | ## Tips and Tricks 109 | 110 | You do not need to add dependencies here. The exercise just requires to code 111 | a new endpoint. 112 | 113 | ## Link 114 | 115 | * [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/) 116 | * [Kubernetes Liveness and Readiness Probes: Looking for More Feet](https://blog.colinbreck.com/kubernetes-liveness-and-readiness-probes-looking-for-more-feet/) 117 | * [NGINX HTTP Health Checks](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-health-check/) 118 | 119 | \newpage 120 | -------------------------------------------------------------------------------- /lesson01-getting-started/SOLUTIONS.md: -------------------------------------------------------------------------------- 1 | # Solution Lesson 1 - Healtcheck 2 | 3 | ## Item 4 | 5 | ```diff 6 | From 378cd70c0eac5bf0ab97903e09e4b319d8e5f2eb Mon Sep 17 00:00:00 2001 7 | From: Gianluca Arbezzano 8 | Date: Thu, 14 Mar 2019 09:40:16 +0100 9 | Subject: [PATCH] feat(items): Added healtcheck endpoint 10 | 11 | Signed-off-by: Gianluca Arbezzano 12 | --- 13 | items/config/autoload/containers.global.php | 2 +- 14 | items/config/routes.php | 2 + 15 | items/src/App/src/Handler/Health.php | 49 +++++++++++++++++++++ 16 | items/src/App/src/Handler/HealthFactory.php | 14 ++++++ 17 | 4 files changed, 66 insertions(+), 1 deletion(-) 18 | create mode 100644 items/src/App/src/Handler/Health.php 19 | create mode 100644 items/src/App/src/Handler/HealthFactory.php 20 | 21 | diff --git a/items/config/autoload/containers.global.php b/items/config/autoload/containers.global.php 22 | index 3166620..511480b 100644 23 | --- a/items/config/autoload/containers.global.php 24 | +++ b/items/config/autoload/containers.global.php 25 | @@ -14,12 +14,12 @@ return [ 26 | // not require arguments to the constructor. Map a service name to the 27 | // class name. 28 | 'invokables' => [ 29 | - // Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class, 30 | ], 31 | // Use 'factories' for services provided by callbacks/factory classes. 32 | 'factories' => [ 33 | App\Service\ItemService::class => App\Service\ItemServiceFactory::class, 34 | App\Handler\Item::class => App\Handler\ItemFactory::class, 35 | + App\Handler\Health::class => App\Handler\HealthFactory::class, 36 | ], 37 | ], 38 | ]; 39 | diff --git a/items/config/routes.php b/items/config/routes.php 40 | index fc0abb7..e37ed12 100644 41 | --- a/items/config/routes.php 42 | +++ b/items/config/routes.php 43 | @@ -6,6 +6,7 @@ use Psr\Container\ContainerInterface; 44 | use Zend\Expressive\Application; 45 | use Zend\Expressive\MiddlewareFactory; 46 | use App\Handler\Item; 47 | +use App\Handler\Health; 48 | 49 | /** 50 | * Setup routes with a single request method: 51 | @@ -22,4 +23,5 @@ use App\Handler\Item; 52 | */ 53 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { 54 | $app->get('/item', Item::class); 55 | + $app->get('/health', Health::class); 56 | }; 57 | diff --git a/items/src/App/src/Handler/Health.php b/items/src/App/src/Handler/Health.php 58 | new file mode 100644 59 | index 0000000..47c210e 60 | --- /dev/null 61 | +++ b/items/src/App/src/Handler/Health.php 62 | @@ -0,0 +1,49 @@ 63 | +username = $username; 78 | + $this->hostname = $hostname; 79 | + $this->password = $password; 80 | + $this->dbname = $dbname; 81 | + } 82 | + 83 | + public function handle(ServerRequestInterface $request) : ResponseInterface 84 | + { 85 | + $statusCode = 500; 86 | + $body = new \stdClass(); 87 | + $body->status = "unhealthy"; 88 | + $mySqlCheck = new \stdClass(); 89 | + $mySqlCheck->name = "mysql"; 90 | + $mySqlCheck->status = "unhealthy"; 91 | + 92 | + try { 93 | + $this->pdo = new PDO("mysql:host=$this->hostname;port=3306;dbname=$this->dbname", $this->username, $this->password); 94 | + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 95 | + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 96 | + 97 | + $statusCode = 200; 98 | + $body->status = "healthy"; 99 | + $mySqlCheck->status = "healthy"; 100 | + 101 | + } catch(\PDOException $ex){ 102 | + $mySqlCheck->error = $ex->getMessage(); 103 | + } 104 | + $body->checks = [$mySqlCheck]; 105 | + 106 | + $response = new JsonResponse($body); 107 | + $response = $response->withStatus($statusCode); 108 | + 109 | + return $response; 110 | + } 111 | +} 112 | diff --git a/items/src/App/src/Handler/HealthFactory.php b/items/src/App/src/Handler/HealthFactory.php 113 | new file mode 100644 114 | index 0000000..e974128 115 | --- /dev/null 116 | +++ b/items/src/App/src/Handler/HealthFactory.php 117 | @@ -0,0 +1,14 @@ 118 | +get('config')['mysql']; 129 | + return new Health($mysqlConfig['hostname'], $mysqlConfig['user'], $mysqlConfig['pass'], $mysqlConfig['dbname']); 130 | + } 131 | +} 132 | -- 133 | 2.23.0 134 | ``` 135 | 136 | ## Discount 137 | 138 | ```diff 139 | From 64be9f3b6237f2851291bbd2187d951007530761 Mon Sep 17 00:00:00 2001 140 | From: Gianluca Arbezzano 141 | Date: Sun, 17 Mar 2019 11:27:17 +0100 142 | Subject: [PATCH] feat(discount): Added healthcheck 143 | 144 | Now the discount service has its own healthcheck endpoint. 145 | 146 | ``` 147 | METHOD: GET 148 | PATH: /health 149 | ``` 150 | 151 | It checks if th mongodb is reachable or not. 152 | 153 | Signed-off-by: Gianluca Arbezzano 154 | --- 155 | discount/server.js | 22 ++++++++++++++++++++++ 156 | 1 file changed, 22 insertions(+) 157 | 158 | diff --git a/discount/server.js b/discount/server.js 159 | index a7cb17b..cedde93 100644 160 | --- a/discount/server.js 161 | +++ b/discount/server.js 162 | @@ -8,6 +8,28 @@ const dbName = 'shopmany'; 163 | const client = new MongoClient(url, { useNewUrlParser: true }); 164 | app.use(errorHandler) 165 | 166 | +app.get("/health", function(req, res, next) { 167 | + var resbody = { 168 | + "status": "healthy", 169 | + checks: [], 170 | + }; 171 | + var resCode = 200; 172 | + 173 | + client.connect(function(err) { 174 | + var mongoCheck = { 175 | + "name": "mongo", 176 | + "status": "healthy", 177 | + }; 178 | + if (err != null) { 179 | + mongoCheck.error = err.toString(); 180 | + mongoCheck.status = "unhealthy"; 181 | + resbody.status = "unhealthy" 182 | + resCode = 500; 183 | + } 184 | + resbody.checks.push(mongoCheck); 185 | + res.status(resCode).json(resbody) 186 | + }); 187 | +}); 188 | 189 | app.get("/discount", function(req, res, next) { 190 | client.connect(function(err) { 191 | -- 192 | 2.23.0 193 | ``` 194 | 195 | ## Pay 196 | 197 | ```diff 198 | From 360c2265f77163da6c30f1aaa662c2d26ee43ff3 Mon Sep 17 00:00:00 2001 199 | From: Gianluca Arbezzano 200 | Date: Sat, 23 Mar 2019 15:48:10 +0100 201 | Subject: [PATCH] feat(pay): Added healthcheck 202 | 203 | Signed-off-by: Gianluca Arbezzano 204 | --- 205 | pay/src/main/java/pay/Application.java | 23 ++++++++++++++++- 206 | pay/src/main/java/pay/HealthCheck.java | 31 +++++++++++++++++++++++ 207 | pay/src/main/java/pay/HealthResponse.java | 29 +++++++++++++++++++++ 208 | 3 files changed, 82 insertions(+), 1 deletion(-) 209 | create mode 100644 pay/src/main/java/pay/HealthCheck.java 210 | create mode 100644 pay/src/main/java/pay/HealthResponse.java 211 | 212 | diff --git a/pay/src/main/java/pay/Application.java b/pay/src/main/java/pay/Application.java 213 | index c66e0c0..ef1194a 100644 214 | --- a/pay/src/main/java/pay/Application.java 215 | +++ b/pay/src/main/java/pay/Application.java 216 | @@ -4,11 +4,11 @@ import org.springframework.boot.SpringApplication; 217 | import org.springframework.boot.autoconfigure.SpringBootApplication; 218 | import org.springframework.http.ResponseEntity; 219 | import org.springframework.web.bind.annotation.*; 220 | +import javax.servlet.http.HttpServletResponse; 221 | 222 | @SpringBootApplication 223 | @RestController 224 | public class Application { 225 | - 226 | private PayRepository payRepository; 227 | 228 | public Application(PayRepository payRepository) { 229 | @@ -27,6 +27,27 @@ public class Application { 230 | return ResponseEntity.ok("Success"); 231 | } 232 | 233 | + @GetMapping("/health") 234 | + @ResponseBody 235 | + public HealthResponse health(HttpServletResponse response) { 236 | + HealthResponse h = new HealthResponse(); 237 | + String status = "unhealthy"; 238 | + 239 | + HealthCheck mysqlC = new HealthCheck(); 240 | + mysqlC.setName("mysql"); 241 | + try { 242 | + payRepository.count(); 243 | + status = "healthy"; 244 | + mysqlC.setStatus("healthy"); 245 | + } catch (Exception e) { 246 | + mysqlC.setStatus("unhealthy"); 247 | + mysqlC.setError(e.getMessage()); 248 | + response.setStatus(500); 249 | + } 250 | + h.setStatus(status); 251 | + h.addHealthCheck(mysqlC); 252 | + return h; 253 | + } 254 | 255 | public static void main(String[] args) { 256 | SpringApplication.run(Application.class, args); 257 | diff --git a/pay/src/main/java/pay/HealthCheck.java b/pay/src/main/java/pay/HealthCheck.java 258 | new file mode 100644 259 | index 0000000..b3b7723 260 | --- /dev/null 261 | +++ b/pay/src/main/java/pay/HealthCheck.java 262 | @@ -0,0 +1,31 @@ 263 | +package pay; 264 | + 265 | +public class HealthCheck { 266 | + private String status; 267 | + private String name; 268 | + private String error; 269 | + 270 | + public String getStatus() { 271 | + return status; 272 | + } 273 | + 274 | + public void setStatus(String status) { 275 | + this.status = status; 276 | + } 277 | + 278 | + public String getName() { 279 | + return name; 280 | + } 281 | + 282 | + public void setName(String name) { 283 | + this.name = name; 284 | + } 285 | + 286 | + public String getError() { 287 | + return error; 288 | + } 289 | + 290 | + public void setError(String error) { 291 | + this.error = error; 292 | + } 293 | +} 294 | diff --git a/pay/src/main/java/pay/HealthResponse.java b/pay/src/main/java/pay/HealthResponse.java 295 | new file mode 100644 296 | index 0000000..8431f53 297 | --- /dev/null 298 | +++ b/pay/src/main/java/pay/HealthResponse.java 299 | @@ -0,0 +1,29 @@ 300 | +package pay; 301 | + 302 | +import java.util.*; 303 | + 304 | +public class HealthResponse { 305 | + private String status; 306 | + 307 | + private List checks; 308 | + 309 | + public HealthResponse () { 310 | + this.checks = new ArrayList(); 311 | + } 312 | + 313 | + public String getStatus() { 314 | + return status; 315 | + } 316 | + 317 | + public void setStatus(String status) { 318 | + this.status = status; 319 | + } 320 | + 321 | + public void addHealthCheck(HealthCheck h) { 322 | + this.checks.add(h); 323 | + } 324 | + 325 | + public List getChecks() { 326 | + return checks; 327 | + } 328 | +} 329 | -- 330 | 2.23.0 331 | ``` 332 | 333 | # Frontend 334 | 335 | ```diff 336 | From 07879e69ff65853685e7711420103f7f7e093c25 Mon Sep 17 00:00:00 2001 337 | From: Gianluca Arbezzano 338 | Date: Thu, 14 Mar 2019 18:34:17 +0100 339 | Subject: [PATCH] feat(frontend): Added healthcheck endpoint 340 | 341 | Now the frontend service has its healthcheck to validate if service that 342 | returns the list of items is working. 343 | 344 | Signed-off-by: Gianluca Arbezzano 345 | --- 346 | frontend/handler/health.go | 87 ++++++++++++++++++++++++++++++++++++++ 347 | frontend/main.go | 1 + 348 | 2 files changed, 88 insertions(+) 349 | create mode 100644 frontend/handler/health.go 350 | 351 | diff --git a/frontend/handler/health.go b/frontend/handler/health.go 352 | new file mode 100644 353 | index 0000000..733d28f 354 | --- /dev/null 355 | +++ b/frontend/handler/health.go 356 | @@ -0,0 +1,87 @@ 357 | +package handler 358 | + 359 | +import ( 360 | + "encoding/json" 361 | + "fmt" 362 | + "io/ioutil" 363 | + "net/http" 364 | + 365 | + "github.com/gianarb/shopmany/frontend/config" 366 | +) 367 | + 368 | +const unhealthy = "unhealty" 369 | +const healthy = "healthy" 370 | + 371 | +type healthResponse struct { 372 | + Status string 373 | + Checks []check 374 | +} 375 | + 376 | +type check struct { 377 | + Error string 378 | + Status string 379 | + Name string 380 | +} 381 | + 382 | +func NewHealthHandler(config config.Config, hclient *http.Client) *healthHandler { 383 | + return &healthHandler{ 384 | + config: config, 385 | + hclient: hclient, 386 | + } 387 | +} 388 | + 389 | +type healthHandler struct { 390 | + config config.Config 391 | + hclient *http.Client 392 | +} 393 | + 394 | +func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 395 | + b := healthResponse{ 396 | + Status: unhealthy, 397 | + Checks: []check{}, 398 | + } 399 | + w.Header().Add("Content-Type", "application/json") 400 | + 401 | + itemCheck := checkItem(h.config.ItemHost, h.hclient) 402 | + if itemCheck.Status == healthy { 403 | + b.Status = healthy 404 | + } 405 | + 406 | + b.Checks = append(b.Checks, itemCheck) 407 | + 408 | + body, err := json.Marshal(b) 409 | + if err != nil { 410 | + w.WriteHeader(500) 411 | + } 412 | + if b.Status == unhealthy { 413 | + w.WriteHeader(500) 414 | + } 415 | + fmt.Fprintf(w, string(body)) 416 | +} 417 | + 418 | +func checkItem(host string, hclient *http.Client) check { 419 | + c := check{ 420 | + Name: "item", 421 | + Error: "", 422 | + Status: unhealthy, 423 | + } 424 | + req, _ := http.NewRequest("GET", fmt.Sprintf("%s/health", host), nil) 425 | + resp, err := hclient.Do(req) 426 | + if err != nil { 427 | + c.Error = err.Error() 428 | + return c 429 | + } 430 | + defer resp.Body.Close() 431 | + if resp.StatusCode >= 200 && resp.StatusCode < 300 { 432 | + c.Status = healthy 433 | + return c 434 | + } 435 | + b, err := ioutil.ReadAll(resp.Body) 436 | + if err != nil { 437 | + c.Error = err.Error() 438 | + return c 439 | + } 440 | + c.Error = string(b) 441 | + 442 | + return c 443 | +} 444 | diff --git a/frontend/main.go b/frontend/main.go 445 | index f78d524..ee16adc 100644 446 | --- a/frontend/main.go 447 | +++ b/frontend/main.go 448 | @@ -28,6 +28,7 @@ func main() { 449 | http.Handle("/", fs) 450 | http.Handle("/api/items", handler.NewGetItemsHandler(config, httpClient)) 451 | http.Handle("/api/pay", handler.NewPayHandler(config, httpClient)) 452 | + http.Handle("/health", handler.NewHealthHandler(config, httpClient)) 453 | 454 | log.Println("Listening on port 3000...") 455 | http.ListenAndServe(":3000", nil) 456 | -- 457 | 2.23.0 458 | ``` 459 | 460 | \newpage 461 | -------------------------------------------------------------------------------- /lesson02-logging/README.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | ## Lessson 2 3 | 4 | The ERA where `printf` and `console.log` around your code was enough to identify 5 | issues is over. A powerful logging library is what we need to troubleshoot and 6 | outage or it is a good way to understand what is going on. 7 | 8 | During this exercise we will make our application to speak. I selected a couple 9 | of libraries that I think are good for the following capabilities: 10 | 11 | 1. They are well known and used 12 | 2. They have the capability to pass external information from a message 13 | 3. You can have JSON as output format 14 | 4. Flexible but easy to use 15 | 16 | Those are the libraries we are gonna use: 17 | 18 | * JAVA: https://logging.apache.org/log4j/2.x 19 | * Golang: https://github.com/uber-go/zap 20 | * PHP: https://github.com/Seldaek/monolog 21 | * Node: https://github.com/pinojs/pino 22 | 23 | ## Exercise: Log all the http request 24 | 25 | **Time: 30minutes** 26 | 27 | Based on the application you picked you should integrate the suggested library 28 | and log every request. 29 | 30 | The format of the logger should be JSON. 31 | 32 | You should write middlewares, interceptor or what ever will help you to decouple 33 | the logging part from the endpoint. 34 | 35 | Find a way to inject the logger in your classes in order to use always the same 36 | instance (or a child) of the main logger. 37 | 38 | ## Tips and tricks 39 | 40 | * Dependency manager: based on the language that you are using is a bit 41 | different. 42 | * For pay (java) and frontend (go) the dependencies are managed as part of the image build. 43 | You can use the command `docker-compose up --build pay` or `docker-compose 44 | up --build frontend` to compile the applications. It will download the 45 | dependencies. 46 | * discount (nodejs) uses npm. If you have npm installed you can 47 | do `npm i` inside the application directory (`./discount`). 48 | * PHP uses composer at this stage you can use it from the app directory and use 49 | `composer install` normally. Moving forward it will be a bit tricker 50 | because we will have php extensions installed. But you will get some tips 51 | when it will be the right time. 52 | * When you inject a logger in a class keep a keywork like `service=mysql` and 53 | configure the logger to add that k/v to every message. This will help you 54 | to understand where logs come from. 55 | * Use in the right way the log level. It is an important information. 56 | * if it is an error that blocks the execution of the program the log level is `error`. 57 | * if it an error that you can manage in the application you should use 58 | `warn`. 59 | * if it's an informative log uses `info`. 60 | 61 | ## Link 62 | 63 | * [Structured Logging and Your Team](https://www.honeycomb.io/blog/structured-logging-and-your-team/) 64 | * [How Are Structured Logs Different From Events?](https://www.honeycomb.io/blog/how-are-structured-logs-different-from-events/) 65 | * [From Logs to Metrics](https://medium.com/@leodido/from-logs-to-metrics-f38854e3441a) 66 | * [Analyzing logs with Chronograf](https://docs.influxdata.com/chronograf/v1.7/guides/analyzing-logs/) 67 | * [Journald logging driver](https://docs.docker.com/config/containers/logging/journald/) 68 | 69 | \newpage 70 | -------------------------------------------------------------------------------- /lesson02-logging/SOLUTIONS.md: -------------------------------------------------------------------------------- 1 | ## Solution Lesson 2 - Logging 2 | 3 | ## Item 4 | 5 | ```diff 6 | From f412a291861afc30784da7dcdf54defcfb7d9476 Mon Sep 17 00:00:00 2001 7 | From: Gianluca Arbezzano 8 | Date: Thu, 14 Mar 2019 10:27:38 +0100 9 | Subject: [PATCH] feat(items): Injected logger 10 | 11 | The item service is not logging using Monolog 12 | 13 | Signed-off-by: Gianluca Arbezzano 14 | --- 15 | items/Dockerfile | 5 ++ 16 | items/composer.json | 3 +- 17 | items/config/autoload/containers.global.php | 2 + 18 | items/config/pipeline.php | 2 + 19 | items/src/App/src/Handler/Item.php | 10 ++++ 20 | items/src/App/src/Handler/ItemFactory.php | 3 +- 21 | .../App/src/Middleware/LoggerMiddleware.php | 54 +++++++++++++++++++ 22 | .../Middleware/LoggerMiddlewareFactory.php | 20 +++++++ 23 | items/src/App/src/Service/LoggerFactory.php | 20 +++++++ 24 | 9 files changed, 117 insertions(+), 2 deletions(-) 25 | create mode 100644 items/src/App/src/Middleware/LoggerMiddleware.php 26 | create mode 100644 items/src/App/src/Middleware/LoggerMiddlewareFactory.php 27 | create mode 100644 items/src/App/src/Service/LoggerFactory.php 28 | 29 | diff --git a/items/Dockerfile b/items/Dockerfile 30 | index 58a1e86..2184cb1 100644 31 | --- a/items/Dockerfile 32 | +++ b/items/Dockerfile 33 | @@ -2,3 +2,8 @@ FROM php:7.2-apache 34 | 35 | RUN a2enmod rewrite 36 | RUN docker-php-ext-install pdo_mysql 37 | + 38 | +RUN find /etc/apache2/sites-enabled/* -exec sed -i 's/#*[Cc]ustom[Ll]og/#CustomLog/g' {} \; 39 | +RUN find /etc/apache2/sites-enabled/* -exec sed -i 's/#*[Ee]rror[Ll]og/#ErrorLog/g' {} \; 40 | +RUN a2disconf other-vhosts-access-log 41 | + 42 | diff --git a/items/composer.json b/items/composer.json 43 | index 50bea51..c0badf9 100644 44 | --- a/items/composer.json 45 | +++ b/items/composer.json 46 | @@ -46,7 +46,8 @@ 47 | "zendframework/zend-expressive-fastroute": "^3.0", 48 | "zendframework/zend-expressive-helpers": "^5.0", 49 | "zendframework/zend-servicemanager": "^3.3", 50 | - "zendframework/zend-stdlib": "^3.1" 51 | + "zendframework/zend-stdlib": "^3.1", 52 | + "monolog/monolog": "1.24.0" 53 | }, 54 | "require-dev": { 55 | "phpunit/phpunit": "^7.0.1", 56 | diff --git a/items/config/autoload/containers.global.php b/items/config/autoload/containers.global.php 57 | index 511480b..12a5b18 100644 58 | --- a/items/config/autoload/containers.global.php 59 | +++ b/items/config/autoload/containers.global.php 60 | @@ -20,6 +20,8 @@ return [ 61 | App\Service\ItemService::class => App\Service\ItemServiceFactory::class, 62 | App\Handler\Item::class => App\Handler\ItemFactory::class, 63 | App\Handler\Health::class => App\Handler\HealthFactory::class, 64 | + "Logger" => App\Service\LoggerFactory::class, 65 | + App\Middleware\LoggerMiddleware::class => App\Middleware\LoggerMiddlewareFactory::class, 66 | ], 67 | ], 68 | ]; 69 | diff --git a/items/config/pipeline.php b/items/config/pipeline.php 70 | index cfe8f0b..e9287fd 100644 71 | --- a/items/config/pipeline.php 72 | +++ b/items/config/pipeline.php 73 | @@ -14,11 +14,13 @@ use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware; 74 | use Zend\Expressive\Router\Middleware\MethodNotAllowedMiddleware; 75 | use Zend\Expressive\Router\Middleware\RouteMiddleware; 76 | use Zend\Stratigility\Middleware\ErrorHandler; 77 | +use App\Middleware\LoggerMiddleware; 78 | 79 | /** 80 | * Setup middleware pipeline: 81 | */ 82 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { 83 | + $app->pipe($container->get(LoggerMiddleware::class)); 84 | // The error handler should be the first (most outer) middleware to catch 85 | // all Exceptions. 86 | $app->pipe(ErrorHandler::class); 87 | diff --git a/items/src/App/src/Handler/Item.php b/items/src/App/src/Handler/Item.php 88 | index 2ea3d66..f1d9a64 100644 89 | --- a/items/src/App/src/Handler/Item.php 90 | +++ b/items/src/App/src/Handler/Item.php 91 | @@ -6,18 +6,28 @@ use Psr\Http\Message\ServerRequestInterface; 92 | use Psr\Http\Server\RequestHandlerInterface; 93 | use Zend\Diactoros\Response\JsonResponse; 94 | use App\Service\ItemService; 95 | +use Monolog\Logger; 96 | +use Monolog\Processor\TagProcessor; 97 | 98 | class Item implements RequestHandlerInterface 99 | { 100 | private $itemService; 101 | + private $logger; 102 | 103 | function __construct(ItemService $itemService) { 104 | $this->itemService = $itemService; 105 | + $this->logger = new Logger('item_service'); 106 | } 107 | 108 | public function handle(ServerRequestInterface $request) : ResponseInterface 109 | { 110 | + $this->logger->info("Get list of items"); 111 | $items = $this->itemService->list(); 112 | + $this->logger->info("Retrived list of items", ["num_items" => count($items)]); 113 | return new JsonResponse(['items' => $items]); 114 | } 115 | + 116 | + public function withLogger($logger) { 117 | + $this->logger = $logger; 118 | + } 119 | } 120 | diff --git a/items/src/App/src/Handler/ItemFactory.php b/items/src/App/src/Handler/ItemFactory.php 121 | index a1db1df..7de3a2d 100644 122 | --- a/items/src/App/src/Handler/ItemFactory.php 123 | +++ b/items/src/App/src/Handler/ItemFactory.php 124 | @@ -9,6 +9,7 @@ class ItemFactory 125 | { 126 | public function __invoke(ContainerInterface $container) 127 | { 128 | - return new Item($container->get(ItemService::class)); 129 | + $h = new Item($container->get(ItemService::class)); 130 | + return $h; 131 | } 132 | } 133 | diff --git a/items/src/App/src/Middleware/LoggerMiddleware.php b/items/src/App/src/Middleware/LoggerMiddleware.php 134 | new file mode 100644 135 | index 0000000..64538c1 136 | --- /dev/null 137 | +++ b/items/src/App/src/Middleware/LoggerMiddleware.php 138 | @@ -0,0 +1,54 @@ 139 | +logger = $logger; 156 | + $this->logger->pushProcessor(new TagProcessor([ 157 | + "service" => "logger_middleware", 158 | + ])); 159 | + } 160 | + 161 | + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 162 | + { 163 | + $isGood = true; 164 | + try { 165 | + $response = $handler->handle($request); 166 | + } catch (Throwable $e) { 167 | + $this->logger->panic("HTTP Server", [ 168 | + "path", $request->getUri()->getPath(), 169 | + "method", $request->getMethod(), 170 | + "status_code" => $response->getStatusCode(), 171 | + "error" => $e->getMessage(), 172 | + ]); 173 | + $isGood=false; 174 | + } 175 | + if ($isGood) { 176 | + if ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 299) { 177 | + $this->logger->info("HTTP Server", [ 178 | + "path", $request->getUri()->getPath(), 179 | + "method", $request->getMethod(), 180 | + "status_code" => $response->getStatusCode(), 181 | + ]); 182 | + } else { 183 | + $this->logger->warn("HTTP Server", [ 184 | + "path", $request->getUri()->getPath(), 185 | + "method", $request->getMethod(), 186 | + "status_code" => $response->getStatusCode(), 187 | + ]); 188 | + } 189 | + } 190 | + return $response; 191 | + } 192 | +} 193 | diff --git a/items/src/App/src/Middleware/LoggerMiddlewareFactory.php b/items/src/App/src/Middleware/LoggerMiddlewareFactory.php 194 | new file mode 100644 195 | index 0000000..bd4fba9 196 | --- /dev/null 197 | +++ b/items/src/App/src/Middleware/LoggerMiddlewareFactory.php 198 | @@ -0,0 +1,20 @@ 199 | +get("Logger"); 216 | + return new LoggerMiddleware($logger); 217 | + } 218 | +} 219 | diff --git a/items/src/App/src/Service/LoggerFactory.php b/items/src/App/src/Service/LoggerFactory.php 220 | new file mode 100644 221 | index 0000000..cc60ae0 222 | --- /dev/null 223 | +++ b/items/src/App/src/Service/LoggerFactory.php 224 | @@ -0,0 +1,20 @@ 225 | +setFormatter(new JsonFormatter()); 240 | + $logger->pushHandler($handler); 241 | + return $logger; 242 | + } 243 | +} 244 | + 245 | -- 246 | 2.23.0 247 | ``` 248 | 249 | ## Discount 250 | 251 | ```diff 252 | From ed6ce9b8dfcf1e396d979c09cf91b980b93a789d Mon Sep 17 00:00:00 2001 253 | From: Gianluca Arbezzano 254 | Date: Sun, 17 Mar 2019 19:20:10 +0100 255 | Subject: [PATCH] feat(discount): Added logging support 256 | 257 | Signed-off-by: Gianluca Arbezzano 258 | --- 259 | discount/package.json | 1 + 260 | discount/server.js | 15 ++++++++++++++- 261 | 2 files changed, 15 insertions(+), 1 deletion(-) 262 | 263 | diff --git a/discount/package.json b/discount/package.json 264 | index 0647009..1640ae1 100644 265 | --- a/discount/package.json 266 | +++ b/discount/package.json 267 | @@ -11,6 +11,7 @@ 268 | "license": "ISC", 269 | "dependencies": { 270 | "express": "^4.16.4", 271 | + "express-pino-logger": "^4.0.0", 272 | "mongodb": "^3.1.13" 273 | } 274 | } 275 | diff --git a/discount/server.js b/discount/server.js 276 | index cedde93..50a32a9 100644 277 | --- a/discount/server.js 278 | +++ b/discount/server.js 279 | @@ -8,6 +8,12 @@ const dbName = 'shopmany'; 280 | const client = new MongoClient(url, { useNewUrlParser: true }); 281 | app.use(errorHandler) 282 | 283 | +const logger = require('pino')() 284 | +const expressPino = require('express-pino-logger')({ 285 | + logger: logger.child({"service": "httpd"}) 286 | +}) 287 | +app.use(expressPino) 288 | + 289 | app.get("/health", function(req, res, next) { 290 | var resbody = { 291 | "status": "healthy", 292 | @@ -21,6 +27,7 @@ app.get("/health", function(req, res, next) { 293 | "status": "healthy", 294 | }; 295 | if (err != null) { 296 | + req.log.warn(err.toString()); 297 | mongoCheck.error = err.toString(); 298 | mongoCheck.status = "unhealthy"; 299 | resbody.status = "unhealthy" 300 | @@ -36,6 +43,7 @@ app.get("/discount", function(req, res, next) { 301 | db = client.db(dbName); 302 | db.collection('discount').find({}).toArray(function(err, discounts) { 303 | if (err != null) { 304 | + req.log.error(err.toString()); 305 | return next(err) 306 | } 307 | var goodDiscount = null 308 | @@ -47,6 +55,7 @@ app.get("/discount", function(req, res, next) { 309 | if (goodDiscount != null) { 310 | res.json({"discount": goodDiscount}) 311 | } else { 312 | + req.log.warn("discount not found"); 313 | res.status(404).json({ error: 'Discount not found' }); 314 | } 315 | return 316 | @@ -55,10 +64,14 @@ app.get("/discount", function(req, res, next) { 317 | }); 318 | 319 | app.use(function(req, res, next) { 320 | + req.log.warn("route not found"); 321 | return res.status(404).json({error: "route not found"}); 322 | }); 323 | 324 | function errorHandler(err, req, res, next) { 325 | + req.log.error(err.toString(), { 326 | + error_status: err.status 327 | + }); 328 | var st = err.status 329 | if (st == 0 || st == null) { 330 | st = 500; 331 | @@ -68,5 +81,5 @@ function errorHandler(err, req, res, next) { 332 | } 333 | 334 | app.listen(3000, () => { 335 | - console.log("Server running on port 3000"); 336 | + logger.info("Server running on port 3000"); 337 | }); 338 | -- 339 | 2.23.0 340 | ``` 341 | ## Pay 342 | 343 | ```diff 344 | From 8e21fd8af39ccb5225edf25fe5d03486fd155534 Mon Sep 17 00:00:00 2001 345 | From: Gianluca Arbezzano 346 | Date: Sat, 23 Mar 2019 16:09:13 +0100 347 | Subject: [PATCH] feat(pay): add log4j2 348 | 349 | Co-Authored-by: Walter Dal Mut 350 | Signed-off-by: Gianluca Arbezzano 351 | --- 352 | pay/build.gradle | 11 +++++- 353 | pay/src/main/java/pay/AppConfig.java | 14 +++++++ 354 | pay/src/main/java/pay/Application.java | 4 ++ 355 | pay/src/main/java/pay/LoggerInterceptor.java | 39 ++++++++++++++++++++ 356 | pay/src/main/resources/log4j2.xml | 16 ++++++++ 357 | 5 files changed, 83 insertions(+), 1 deletion(-) 358 | create mode 100644 pay/src/main/java/pay/AppConfig.java 359 | create mode 100644 pay/src/main/java/pay/LoggerInterceptor.java 360 | create mode 100644 pay/src/main/resources/log4j2.xml 361 | 362 | diff --git a/pay/build.gradle b/pay/build.gradle 363 | index a8e253c..50bb905 100644 364 | --- a/pay/build.gradle 365 | +++ b/pay/build.gradle 366 | @@ -26,9 +26,18 @@ sourceCompatibility = 1.8 367 | targetCompatibility = 1.8 368 | 369 | dependencies { 370 | - compile("org.springframework.boot:spring-boot-starter-web") 371 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 372 | //compile("com.h2database:h2") 373 | compile 'mysql:mysql-connector-java' 374 | + compile("com.fasterxml.jackson.core:jackson-databind") 375 | + compile("org.springframework.boot:spring-boot-starter-web"){ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'} 376 | + compile('org.springframework.boot:spring-boot-starter-log4j2') 377 | testCompile('org.springframework.boot:spring-boot-starter-test') 378 | } 379 | + 380 | + 381 | +configurations { 382 | + all { 383 | + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' 384 | + } 385 | +} 386 | diff --git a/pay/src/main/java/pay/AppConfig.java b/pay/src/main/java/pay/AppConfig.java 387 | new file mode 100644 388 | index 0000000..bb788cb 389 | --- /dev/null 390 | +++ b/pay/src/main/java/pay/AppConfig.java 391 | @@ -0,0 +1,14 @@ 392 | +package pay; 393 | + 394 | +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 395 | +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 396 | +import org.springframework.stereotype.Component; 397 | + 398 | +@Component 399 | +public class AppConfig extends WebMvcConfigurerAdapter { 400 | + 401 | + @Override 402 | + public void addInterceptors(InterceptorRegistry registry) { 403 | + registry.addInterceptor(new LoggerInterceptor()); 404 | + } 405 | +} 406 | diff --git a/pay/src/main/java/pay/Application.java b/pay/src/main/java/pay/Application.java 407 | index ef1194a..1d8d39d 100644 408 | --- a/pay/src/main/java/pay/Application.java 409 | +++ b/pay/src/main/java/pay/Application.java 410 | @@ -5,10 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; 411 | import org.springframework.http.ResponseEntity; 412 | import org.springframework.web.bind.annotation.*; 413 | import javax.servlet.http.HttpServletResponse; 414 | +import org.slf4j.Logger; 415 | +import org.slf4j.LoggerFactory; 416 | 417 | @SpringBootApplication 418 | @RestController 419 | public class Application { 420 | + private static final Logger logger = LoggerFactory.getLogger(Application.class); 421 | private PayRepository payRepository; 422 | 423 | public Application(PayRepository payRepository) { 424 | @@ -40,6 +43,7 @@ public class Application { 425 | status = "healthy"; 426 | mysqlC.setStatus("healthy"); 427 | } catch (Exception e) { 428 | + logger.error("Mysql healthcheck failed", e.getMessage()); 429 | mysqlC.setStatus("unhealthy"); 430 | mysqlC.setError(e.getMessage()); 431 | response.setStatus(500); 432 | diff --git a/pay/src/main/java/pay/LoggerInterceptor.java b/pay/src/main/java/pay/LoggerInterceptor.java 433 | new file mode 100644 434 | index 0000000..654229f 435 | --- /dev/null 436 | +++ b/pay/src/main/java/pay/LoggerInterceptor.java 437 | @@ -0,0 +1,39 @@ 438 | +package pay; 439 | + 440 | +import org.springframework.stereotype.Component; 441 | +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 442 | +import javax.servlet.http.HttpServletRequest; 443 | +import javax.servlet.http.HttpServletResponse; 444 | +import org.slf4j.Logger; 445 | +import org.slf4j.LoggerFactory; 446 | + 447 | +@Component 448 | +public class LoggerInterceptor 449 | + extends HandlerInterceptorAdapter { 450 | + private static final Logger logger = LoggerFactory.getLogger(Application.class); 451 | + 452 | + @Override 453 | + public boolean preHandle( 454 | + HttpServletRequest request, 455 | + HttpServletResponse response, 456 | + Object handler) { 457 | + long startTime = System.currentTimeMillis(); 458 | + logger.info("[Start HTTP Request]: Path" + request.getRequestURL().toString() 459 | + + " StartTime=" + startTime); 460 | + request.setAttribute("startTime", startTime); 461 | + 462 | + return true; 463 | + } 464 | + 465 | + @Override 466 | + public void afterCompletion( 467 | + HttpServletRequest request, 468 | + HttpServletResponse response, 469 | + Object handler, 470 | + Exception ex) { 471 | + long startTime = (Long) request.getAttribute("startTime"); 472 | + logger.info("[End HTTP Request]: Path" + request.getRequestURL().toString() 473 | + + " EndTime=" + System.currentTimeMillis() 474 | + + " TimeTaken="+ (System.currentTimeMillis() - startTime)); 475 | + } 476 | +} 477 | diff --git a/pay/src/main/resources/log4j2.xml b/pay/src/main/resources/log4j2.xml 478 | new file mode 100644 479 | index 0000000..7403409 480 | --- /dev/null 481 | +++ b/pay/src/main/resources/log4j2.xml 482 | @@ -0,0 +1,16 @@ 483 | + 484 | + 485 | + 486 | + 487 | + 488 | + 489 | + 490 | + 491 | + 492 | + 493 | + 494 | + 495 | + 496 | + 497 | + 498 | + 499 | -- 500 | 2.23.0 501 | ``` 502 | 503 | ## Frontend 504 | 505 | ```diff 506 | From a305d0c7fcd110767c6ef696eb927d79b896e017 Mon Sep 17 00:00:00 2001 507 | From: Gianluca Arbezzano 508 | Date: Thu, 14 Mar 2019 19:17:55 +0100 509 | Subject: [PATCH] feat(frontend): Added logging 510 | 511 | Signed-off-by: Gianluca Arbezzano 512 | --- 513 | frontend/go.mod | 7 ++++++- 514 | frontend/go.sum | 6 ++++++ 515 | frontend/handler/getitems.go | 18 ++++++++++++++++++ 516 | frontend/handler/health.go | 9 +++++++++ 517 | frontend/handler/pay.go | 8 ++++++++ 518 | frontend/main.go | 34 +++++++++++++++++++++++++++++----- 519 | 6 files changed, 76 insertions(+), 6 deletions(-) 520 | 521 | diff --git a/frontend/go.mod b/frontend/go.mod 522 | index c9f9ab6..d86b3cb 100644 523 | --- a/frontend/go.mod 524 | +++ b/frontend/go.mod 525 | @@ -2,4 +2,9 @@ module github.com/gianarb/shopmany/frontend 526 | 527 | go 1.12 528 | 529 | -require github.com/jessevdk/go-flags v1.4.0 530 | +require ( 531 | + github.com/jessevdk/go-flags v1.4.0 532 | + go.uber.org/atomic v1.3.2 // indirect 533 | + go.uber.org/multierr v1.1.0 // indirect 534 | + go.uber.org/zap v1.9.1 535 | +) 536 | diff --git a/frontend/go.sum b/frontend/go.sum 537 | index bc46dae..ab7c346 100644 538 | --- a/frontend/go.sum 539 | +++ b/frontend/go.sum 540 | @@ -1,3 +1,9 @@ 541 | github.com/gianarb/shopmany v0.0.0-20190313091614-ac1c2f0595da h1:DxIHt5N7dhhxgDsk9pFvl4DAoggKEtNvQTOA7ZmC2eU= 542 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 543 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 544 | +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= 545 | +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 546 | +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 547 | +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 548 | +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= 549 | +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 550 | diff --git a/frontend/handler/getitems.go b/frontend/handler/getitems.go 551 | index 5019d55..54a3d32 100644 552 | --- a/frontend/handler/getitems.go 553 | +++ b/frontend/handler/getitems.go 554 | @@ -9,6 +9,7 @@ import ( 555 | "strconv" 556 | 557 | "github.com/gianarb/shopmany/frontend/config" 558 | + "go.uber.org/zap" 559 | ) 560 | 561 | type ItemsResponse struct { 562 | @@ -62,27 +63,41 @@ func getDiscountPerItem(ctx context.Context, hclient *http.Client, itemID int, d 563 | type getItemsHandler struct { 564 | config config.Config 565 | hclient *http.Client 566 | + logger *zap.Logger 567 | } 568 | 569 | func NewGetItemsHandler(config config.Config, hclient *http.Client) *getItemsHandler { 570 | + logger, _ := zap.NewProduction() 571 | return &getItemsHandler{ 572 | config: config, 573 | hclient: hclient, 574 | + logger: logger, 575 | } 576 | } 577 | 578 | +func (h *getItemsHandler) WithLogger(logger *zap.Logger) { 579 | + h.logger = logger 580 | +} 581 | + 582 | func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 583 | ctx := r.Context() 584 | w.Header().Add("Content-Type", "application/json") 585 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/item", h.config.ItemHost), nil) 586 | if err != nil { 587 | + h.logger.Error(err.Error()) 588 | http.Error(w, err.Error(), 500) 589 | return 590 | } 591 | resp, err := h.hclient.Do(req) 592 | + if err != nil { 593 | + h.logger.Error(err.Error()) 594 | + http.Error(w, err.Error(), 500) 595 | + return 596 | + } 597 | defer resp.Body.Close() 598 | body, err := ioutil.ReadAll(resp.Body) 599 | if err != nil { 600 | + h.logger.Error(err.Error()) 601 | http.Error(w, err.Error(), 500) 602 | return 603 | } 604 | @@ -91,6 +106,7 @@ func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 605 | } 606 | err = json.Unmarshal(body, &items) 607 | if err != nil { 608 | + h.logger.Error(err.Error()) 609 | http.Error(w, err.Error(), 500) 610 | return 611 | } 612 | @@ -98,6 +114,7 @@ func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 613 | for k, item := range items.Items { 614 | d, err := getDiscountPerItem(ctx, h.hclient, item.ID, h.config.DiscountHost) 615 | if err != nil { 616 | + h.logger.Error(err.Error()) 617 | http.Error(w, err.Error(), 500) 618 | continue 619 | } 620 | @@ -106,6 +123,7 @@ func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 621 | 622 | b, err := json.Marshal(items) 623 | if err != nil { 624 | + h.logger.Error(err.Error()) 625 | http.Error(w, err.Error(), 500) 626 | return 627 | } 628 | diff --git a/frontend/handler/health.go b/frontend/handler/health.go 629 | index 733d28f..fa9e52f 100644 630 | --- a/frontend/handler/health.go 631 | +++ b/frontend/handler/health.go 632 | @@ -7,6 +7,7 @@ import ( 633 | "net/http" 634 | 635 | "github.com/gianarb/shopmany/frontend/config" 636 | + "go.uber.org/zap" 637 | ) 638 | 639 | const unhealthy = "unhealty" 640 | @@ -24,15 +25,22 @@ type check struct { 641 | } 642 | 643 | func NewHealthHandler(config config.Config, hclient *http.Client) *healthHandler { 644 | + logger, _ := zap.NewProduction() 645 | return &healthHandler{ 646 | config: config, 647 | hclient: hclient, 648 | + logger: logger, 649 | } 650 | } 651 | 652 | type healthHandler struct { 653 | config config.Config 654 | hclient *http.Client 655 | + logger *zap.Logger 656 | +} 657 | + 658 | +func (h *healthHandler) WithLogger(logger *zap.Logger) { 659 | + h.logger = logger 660 | } 661 | 662 | func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 663 | @@ -51,6 +59,7 @@ func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 664 | 665 | body, err := json.Marshal(b) 666 | if err != nil { 667 | + h.logger.Error(err.Error()) 668 | w.WriteHeader(500) 669 | } 670 | if b.Status == unhealthy { 671 | diff --git a/frontend/handler/pay.go b/frontend/handler/pay.go 672 | index b3a8a24..f3e5434 100644 673 | --- a/frontend/handler/pay.go 674 | +++ b/frontend/handler/pay.go 675 | @@ -5,20 +5,28 @@ import ( 676 | "net/http" 677 | 678 | "github.com/gianarb/shopmany/frontend/config" 679 | + "go.uber.org/zap" 680 | ) 681 | 682 | type payHandler struct { 683 | config config.Config 684 | hclient *http.Client 685 | + logger *zap.Logger 686 | } 687 | 688 | func NewPayHandler(config config.Config, hclient *http.Client) *payHandler { 689 | + logger, _ := zap.NewProduction() 690 | return &payHandler{ 691 | config: config, 692 | hclient: hclient, 693 | + logger: logger, 694 | } 695 | } 696 | 697 | +func (h *payHandler) WithLogger(logger *zap.Logger) { 698 | + h.logger = logger 699 | +} 700 | + 701 | func (h *payHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 702 | w.Header().Add("Content-Type", "application/json") 703 | if r.Method != "POST" { 704 | diff --git a/frontend/main.go b/frontend/main.go 705 | index ee16adc..35a084c 100644 706 | --- a/frontend/main.go 707 | +++ b/frontend/main.go 708 | @@ -8,9 +8,12 @@ import ( 709 | "github.com/gianarb/shopmany/frontend/config" 710 | "github.com/gianarb/shopmany/frontend/handler" 711 | flags "github.com/jessevdk/go-flags" 712 | + "go.uber.org/zap" 713 | ) 714 | 715 | func main() { 716 | + logger, _ := zap.NewProduction() 717 | + defer logger.Sync() 718 | config := config.Config{} 719 | _, err := flags.Parse(&config) 720 | 721 | @@ -22,14 +25,35 @@ func main() { 722 | fmt.Printf("Pay Host: %v\n", config.PayHost) 723 | fmt.Printf("Discount Host: %v\n", config.DiscountHost) 724 | 725 | + mux := http.NewServeMux() 726 | + 727 | httpClient := &http.Client{} 728 | fs := http.FileServer(http.Dir("static")) 729 | 730 | - http.Handle("/", fs) 731 | - http.Handle("/api/items", handler.NewGetItemsHandler(config, httpClient)) 732 | - http.Handle("/api/pay", handler.NewPayHandler(config, httpClient)) 733 | - http.Handle("/health", handler.NewHealthHandler(config, httpClient)) 734 | + httpdLogger := logger.With(zap.String("service", "httpd")) 735 | + getItemsHandler := handler.NewGetItemsHandler(config, httpClient) 736 | + getItemsHandler.WithLogger(logger) 737 | + payHandler := handler.NewPayHandler(config, httpClient) 738 | + payHandler.WithLogger(logger) 739 | + healthHandler := handler.NewHealthHandler(config, httpClient) 740 | + healthHandler.WithLogger(logger) 741 | + 742 | + mux.Handle("/", fs) 743 | + mux.Handle("/api/items", getItemsHandler) 744 | + mux.Handle("/api/pay", payHandler) 745 | + mux.Handle("/health", healthHandler) 746 | 747 | log.Println("Listening on port 3000...") 748 | - http.ListenAndServe(":3000", nil) 749 | + http.ListenAndServe(":3000", loggingMiddleware(httpdLogger.With(zap.String("from", "middleware")), mux)) 750 | +} 751 | + 752 | +func loggingMiddleware(logger *zap.Logger, h http.Handler) http.Handler { 753 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 754 | + logger.Info( 755 | + "HTTP Request", 756 | + zap.String("Path", r.URL.Path), 757 | + zap.String("Method", r.Method), 758 | + zap.String("RemoteAddr", r.RemoteAddr)) 759 | + h.ServeHTTP(w, r) 760 | + }) 761 | } 762 | -- 763 | 2.23.0 764 | 765 | ``` 766 | 767 | \newpage 768 | -------------------------------------------------------------------------------- /lesson03-influxdb/README.md: -------------------------------------------------------------------------------- 1 | # Monitoring stack with InfluxDB 2 | 3 | ## Lesson 3 4 | 5 | As you saw along the course today there are a lot of tools that you can use to 6 | build a monitoring infrastructure. You also know that it has to stay up when the 7 | system is down. So it is not an easy job and that's why there are vendors and as 8 | a service platform outside or inside cloud provider. 9 | 10 | There are a good amount of open source tools that you can use. This is a select 11 | pipeline that uses what provided by InfluxData the startup behind a popular time 12 | series database called InfluxDB. 13 | 14 | We are using InfluxDB v2, it is currently in Beta but well tested in its as a 15 | service distribution called InfluxCloud. 16 | 17 | With this lesson will familiarize with InfluxDB and its capabilities, precisely: 18 | 19 | 20 | ## Exercise: Familiarize with InfluxDB v2 21 | 22 | **Time: 30minutes** 23 | 24 | 1. We will spin it up with the command: 25 | 26 | ``` 27 | $ docker-compose up -d influxdb 28 | ``` 29 | 30 | At this point you can follow [Getting Started with 31 | InfluxDB](https://v2.docs.influxdata.com/v2.0/get-started/#set-up-influxdb) 32 | 33 | When following the steps be sure to use the right informations: 34 | 35 | * Enter a Username for your initial user. 36 | * Enter a Password and Confirm Password for your user. 37 | * Enter `workshop` as Organization Name. 38 | * Enter `workshop` as your initial Bucket Name. Click Continue. 39 | 40 | 2. [Create a token in the 41 | UI](https://v2.docs.influxdata.com/v2.0/security/tokens/create-token/) 42 | 43 | ### Start the Telegraf collector 44 | 45 | Copy the token and paste it in: `./telegraf/telegraf.conf` 46 | 47 | ``` 48 | [[outputs.influxdb_v2]] 49 | urls = ["http://influxdb:9999"] 50 | token = "mOtZOovg_o7CNpB68pex5O5NheWSjsLEDWPXUFlJXqqYnycJMKJxJnmFAbfmwRnOJ2bRPAgY-VdFWhPeqH8hCg==" 51 | organization = "workshop" 52 | bucket = "workshop" 53 | ``` 54 | 55 | Start Telegraf via: 56 | 57 | ```bash 58 | $ docker-compose up -d telegraf 59 | ``` 60 | 61 | Get back to the UI and you can [create a dashboard from the system 62 | template](https://v2.docs.influxdata.com/v2.0/visualize-data/dashboards/create-dashboard/#create-dashboards-with-templates) 63 | to visualize your data. 64 | 65 | 66 | ## Exercise: Configure Telegraf to use the healthcheck from our apps 67 | 68 | **Time: 20minutes** 69 | 70 | We coded an healthcheck for our application. This is a first useful signal to 71 | understand if the applications are running or not. Telegraf has a plugin called 72 | `inputs.http_response` that can be used to ping and validate an HTTP endpoint. 73 | 74 | Create a dashboard that uses these new metrics to tell you the status code 75 | returned by the health check. 76 | 77 | 78 | ## Exercise: Import a dashboard that shows service availability 79 | 80 | **Time: 10minutes** 81 | 82 | This is the code to import: 83 | 84 | ``` 85 | { 86 | "meta": { 87 | "version": "1", 88 | "type": "dashboard", 89 | "name": "Service control room-Template", 90 | "description": "template created from dashboard: Service control room" 91 | }, 92 | "content": { 93 | "data": { 94 | "type": "dashboard", 95 | "attributes": { 96 | "name": "Service control room", 97 | "description": "" 98 | }, 99 | "relationships": { 100 | "label": { 101 | "data": [] 102 | }, 103 | "cell": { 104 | "data": [ 105 | { 106 | "type": "cell", 107 | "id": "05663f2fd829f000" 108 | }, 109 | { 110 | "type": "cell", 111 | "id": "0566404c0b69f000" 112 | }, 113 | { 114 | "type": "cell", 115 | "id": "056640729b29f000" 116 | }, 117 | { 118 | "type": "cell", 119 | "id": "0566408f8829f000" 120 | }, 121 | { 122 | "type": "cell", 123 | "id": "056641b970a9f000" 124 | }, 125 | { 126 | "type": "cell", 127 | "id": "0566472d9869f000" 128 | }, 129 | { 130 | "type": "cell", 131 | "id": "0566473b7369f000" 132 | }, 133 | { 134 | "type": "cell", 135 | "id": "05664807e4e9f000" 136 | }, 137 | { 138 | "type": "cell", 139 | "id": "05664821c6e9f000" 140 | } 141 | ] 142 | }, 143 | "variable": { 144 | "data": [ 145 | { 146 | "type": "variable", 147 | "id": "05663a9e2be9f000" 148 | } 149 | ] 150 | } 151 | } 152 | }, 153 | "included": [ 154 | { 155 | "id": "05663f2fd829f000", 156 | "type": "cell", 157 | "attributes": { 158 | "x": 0, 159 | "y": 1, 160 | "w": 2, 161 | "h": 2 162 | }, 163 | "relationships": { 164 | "view": { 165 | "data": { 166 | "type": "view", 167 | "id": "05663f2fd829f000" 168 | } 169 | } 170 | } 171 | }, 172 | { 173 | "id": "0566404c0b69f000", 174 | "type": "cell", 175 | "attributes": { 176 | "x": 0, 177 | "y": 5, 178 | "w": 2, 179 | "h": 2 180 | }, 181 | "relationships": { 182 | "view": { 183 | "data": { 184 | "type": "view", 185 | "id": "0566404c0b69f000" 186 | } 187 | } 188 | } 189 | }, 190 | { 191 | "id": "056640729b29f000", 192 | "type": "cell", 193 | "attributes": { 194 | "x": 0, 195 | "y": 7, 196 | "w": 2, 197 | "h": 2 198 | }, 199 | "relationships": { 200 | "view": { 201 | "data": { 202 | "type": "view", 203 | "id": "056640729b29f000" 204 | } 205 | } 206 | } 207 | }, 208 | { 209 | "id": "0566408f8829f000", 210 | "type": "cell", 211 | "attributes": { 212 | "x": 0, 213 | "y": 3, 214 | "w": 2, 215 | "h": 2 216 | }, 217 | "relationships": { 218 | "view": { 219 | "data": { 220 | "type": "view", 221 | "id": "0566408f8829f000" 222 | } 223 | } 224 | } 225 | }, 226 | { 227 | "id": "056641b970a9f000", 228 | "type": "cell", 229 | "attributes": { 230 | "x": 0, 231 | "y": 0, 232 | "w": 5, 233 | "h": 1 234 | }, 235 | "relationships": { 236 | "view": { 237 | "data": { 238 | "type": "view", 239 | "id": "056641b970a9f000" 240 | } 241 | } 242 | } 243 | }, 244 | { 245 | "id": "0566472d9869f000", 246 | "type": "cell", 247 | "attributes": { 248 | "x": 2, 249 | "y": 1, 250 | "w": 6, 251 | "h": 2 252 | }, 253 | "relationships": { 254 | "view": { 255 | "data": { 256 | "type": "view", 257 | "id": "0566472d9869f000" 258 | } 259 | } 260 | } 261 | }, 262 | { 263 | "id": "0566473b7369f000", 264 | "type": "cell", 265 | "attributes": { 266 | "x": 2, 267 | "y": 3, 268 | "w": 6, 269 | "h": 2 270 | }, 271 | "relationships": { 272 | "view": { 273 | "data": { 274 | "type": "view", 275 | "id": "0566473b7369f000" 276 | } 277 | } 278 | } 279 | }, 280 | { 281 | "id": "05664807e4e9f000", 282 | "type": "cell", 283 | "attributes": { 284 | "x": 2, 285 | "y": 7, 286 | "w": 6, 287 | "h": 2 288 | }, 289 | "relationships": { 290 | "view": { 291 | "data": { 292 | "type": "view", 293 | "id": "05664807e4e9f000" 294 | } 295 | } 296 | } 297 | }, 298 | { 299 | "id": "05664821c6e9f000", 300 | "type": "cell", 301 | "attributes": { 302 | "x": 2, 303 | "y": 5, 304 | "w": 6, 305 | "h": 2 306 | }, 307 | "relationships": { 308 | "view": { 309 | "data": { 310 | "type": "view", 311 | "id": "05664821c6e9f000" 312 | } 313 | } 314 | } 315 | }, 316 | { 317 | "type": "view", 318 | "id": "05663f2fd829f000", 319 | "attributes": { 320 | "name": "Discont healthcheck status code", 321 | "properties": { 322 | "shape": "chronograf-v2", 323 | "type": "single-stat", 324 | "queries": [ 325 | { 326 | "text": "from(bucket: \"workshop\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._field == \"http_response_code\")\n |> filter(fn: (r) => r.server == \"http://discount:3000/health\")\n |> last()\n |> yield(name: \"last\")", 327 | "editMode": "advanced", 328 | "name": "", 329 | "builderConfig": { 330 | "buckets": [], 331 | "tags": [ 332 | { 333 | "key": "_measurement", 334 | "values": [], 335 | "aggregateFunctionType": "filter" 336 | } 337 | ], 338 | "functions": [], 339 | "aggregateWindow": { 340 | "period": "auto" 341 | } 342 | } 343 | } 344 | ], 345 | "prefix": "", 346 | "tickPrefix": "", 347 | "suffix": "", 348 | "tickSuffix": "", 349 | "colors": [ 350 | { 351 | "id": "base", 352 | "type": "text", 353 | "hex": "#00C9FF", 354 | "name": "laser", 355 | "value": 0 356 | }, 357 | { 358 | "id": "816c5320-9c48-4c09-b6f8-b7eff4368fd0", 359 | "type": "text", 360 | "hex": "#DC4E58", 361 | "name": "fire", 362 | "value": 400 363 | } 364 | ], 365 | "decimalPlaces": { 366 | "isEnforced": true, 367 | "digits": 2 368 | }, 369 | "note": "This cell returns the last status code for the service. if no data are returned it means that the service is not well scraped by the telegraf plugin. Lickely it means that it down", 370 | "showNoteWhenEmpty": true 371 | } 372 | } 373 | }, 374 | { 375 | "type": "view", 376 | "id": "0566404c0b69f000", 377 | "attributes": { 378 | "name": "Pay healthcheck status code", 379 | "properties": { 380 | "shape": "chronograf-v2", 381 | "type": "single-stat", 382 | "queries": [ 383 | { 384 | "text": "from(bucket: v.bucket)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._field == \"http_response_code\")\n |> filter(fn: (r) => r.server == \"http://pay:8080/health\")\n |> last()\n |> yield(name: \"last\")", 385 | "editMode": "advanced", 386 | "name": "", 387 | "builderConfig": { 388 | "buckets": [], 389 | "tags": [ 390 | { 391 | "key": "_measurement", 392 | "values": [], 393 | "aggregateFunctionType": "filter" 394 | } 395 | ], 396 | "functions": [], 397 | "aggregateWindow": { 398 | "period": "auto" 399 | } 400 | } 401 | } 402 | ], 403 | "prefix": "", 404 | "tickPrefix": "", 405 | "suffix": "", 406 | "tickSuffix": "", 407 | "colors": [ 408 | { 409 | "id": "base", 410 | "type": "text", 411 | "hex": "#00C9FF", 412 | "name": "laser", 413 | "value": 0 414 | }, 415 | { 416 | "id": "816c5320-9c48-4c09-b6f8-b7eff4368fd0", 417 | "type": "text", 418 | "hex": "#DC4E58", 419 | "name": "fire", 420 | "value": 400 421 | } 422 | ], 423 | "decimalPlaces": { 424 | "isEnforced": true, 425 | "digits": 2 426 | }, 427 | "note": "This cell returns the last status code for the service. if no data are returned it means that the service is not well scraped by the telegraf plugin. Lickely it means that it down", 428 | "showNoteWhenEmpty": true 429 | } 430 | } 431 | }, 432 | { 433 | "type": "view", 434 | "id": "056640729b29f000", 435 | "attributes": { 436 | "name": "Frontend healthcheck status code", 437 | "properties": { 438 | "shape": "chronograf-v2", 439 | "type": "single-stat", 440 | "queries": [ 441 | { 442 | "text": "from(bucket: v.bucket)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._field == \"http_response_code\")\n |> filter(fn: (r) => r.server == \"http://frontend:3000/health\")\n |> last()\n |> yield(name: \"last\")", 443 | "editMode": "advanced", 444 | "name": "", 445 | "builderConfig": { 446 | "buckets": [], 447 | "tags": [ 448 | { 449 | "key": "_measurement", 450 | "values": [], 451 | "aggregateFunctionType": "filter" 452 | } 453 | ], 454 | "functions": [], 455 | "aggregateWindow": { 456 | "period": "auto" 457 | } 458 | } 459 | } 460 | ], 461 | "prefix": "", 462 | "tickPrefix": "", 463 | "suffix": "", 464 | "tickSuffix": "", 465 | "colors": [ 466 | { 467 | "id": "base", 468 | "type": "text", 469 | "hex": "#00C9FF", 470 | "name": "laser", 471 | "value": 0 472 | }, 473 | { 474 | "id": "816c5320-9c48-4c09-b6f8-b7eff4368fd0", 475 | "type": "text", 476 | "hex": "#DC4E58", 477 | "name": "fire", 478 | "value": 400 479 | } 480 | ], 481 | "decimalPlaces": { 482 | "isEnforced": true, 483 | "digits": 2 484 | }, 485 | "note": "This cell returns the last status code for the service. if no data are returned it means that the service is not well scraped by the telegraf plugin. Lickely it means that it down", 486 | "showNoteWhenEmpty": true 487 | } 488 | } 489 | }, 490 | { 491 | "type": "view", 492 | "id": "0566408f8829f000", 493 | "attributes": { 494 | "name": "Item healthcheck status code", 495 | "properties": { 496 | "shape": "chronograf-v2", 497 | "type": "single-stat", 498 | "queries": [ 499 | { 500 | "text": "from(bucket: v.bucket)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._field == \"http_response_code\")\n |> filter(fn: (r) => r.server == \"http://item/health\")\n |> last()\n |> yield(name: \"last\")", 501 | "editMode": "advanced", 502 | "name": "", 503 | "builderConfig": { 504 | "buckets": [], 505 | "tags": [ 506 | { 507 | "key": "_measurement", 508 | "values": [], 509 | "aggregateFunctionType": "filter" 510 | } 511 | ], 512 | "functions": [], 513 | "aggregateWindow": { 514 | "period": "auto" 515 | } 516 | } 517 | } 518 | ], 519 | "prefix": "", 520 | "tickPrefix": "", 521 | "suffix": "", 522 | "tickSuffix": "", 523 | "colors": [ 524 | { 525 | "id": "base", 526 | "type": "text", 527 | "hex": "#00C9FF", 528 | "name": "laser", 529 | "value": 0 530 | }, 531 | { 532 | "id": "816c5320-9c48-4c09-b6f8-b7eff4368fd0", 533 | "type": "text", 534 | "hex": "#DC4E58", 535 | "name": "fire", 536 | "value": 400 537 | } 538 | ], 539 | "decimalPlaces": { 540 | "isEnforced": true, 541 | "digits": 2 542 | }, 543 | "note": "This cell returns the last status code for the service. if no data are returned it means that the service is not well scraped by the telegraf plugin. Lickely it means that it down", 544 | "showNoteWhenEmpty": true 545 | } 546 | } 547 | }, 548 | { 549 | "type": "view", 550 | "id": "056641b970a9f000", 551 | "attributes": { 552 | "name": "Name this Cell", 553 | "properties": { 554 | "shape": "chronograf-v2", 555 | "type": "markdown", 556 | "note": "This dashboard is the control room for our set of services" 557 | } 558 | } 559 | }, 560 | { 561 | "type": "view", 562 | "id": "0566472d9869f000", 563 | "attributes": { 564 | "name": "Discount status code distribution", 565 | "properties": { 566 | "shape": "chronograf-v2", 567 | "type": "histogram", 568 | "queries": [ 569 | { 570 | "text": "from(bucket: \"workshop\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"http_response\")\n |> filter(fn: (r) => r.server == \"http://discount:3000/health\")", 571 | "editMode": "advanced", 572 | "name": "", 573 | "builderConfig": { 574 | "buckets": [], 575 | "tags": [ 576 | { 577 | "key": "_measurement", 578 | "values": [], 579 | "aggregateFunctionType": "filter" 580 | } 581 | ], 582 | "functions": [], 583 | "aggregateWindow": { 584 | "period": "auto" 585 | } 586 | } 587 | } 588 | ], 589 | "colors": [ 590 | { 591 | "id": "9d5cb0aa-18e4-4c81-a223-bec447bba26a", 592 | "type": "scale", 593 | "hex": "#31C0F6", 594 | "name": "Nineteen Eighty Four", 595 | "value": 0 596 | }, 597 | { 598 | "id": "b19b236a-95e7-430f-a176-13df6b2557f7", 599 | "type": "scale", 600 | "hex": "#A500A5", 601 | "name": "Nineteen Eighty Four", 602 | "value": 0 603 | }, 604 | { 605 | "id": "fb6915cf-61c1-47b6-bdd9-8a42061dc9a6", 606 | "type": "scale", 607 | "hex": "#FF7E27", 608 | "name": "Nineteen Eighty Four", 609 | "value": 0 610 | } 611 | ], 612 | "xColumn": "_time", 613 | "fillColumns": [ 614 | "result" 615 | ], 616 | "xAxisLabel": "", 617 | "position": "stacked", 618 | "binCount": 0, 619 | "note": "", 620 | "showNoteWhenEmpty": false 621 | } 622 | } 623 | }, 624 | { 625 | "type": "view", 626 | "id": "0566473b7369f000", 627 | "attributes": { 628 | "name": "Item status code distribution", 629 | "properties": { 630 | "shape": "chronograf-v2", 631 | "type": "histogram", 632 | "queries": [ 633 | { 634 | "text": "from(bucket: \"workshop\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"http_response\")\n |> filter(fn: (r) => r.server == \"http://item/health\")", 635 | "editMode": "advanced", 636 | "name": "", 637 | "builderConfig": { 638 | "buckets": [], 639 | "tags": [ 640 | { 641 | "key": "_measurement", 642 | "values": [], 643 | "aggregateFunctionType": "filter" 644 | } 645 | ], 646 | "functions": [], 647 | "aggregateWindow": { 648 | "period": "auto" 649 | } 650 | } 651 | } 652 | ], 653 | "colors": [ 654 | { 655 | "id": "9d5cb0aa-18e4-4c81-a223-bec447bba26a", 656 | "type": "scale", 657 | "hex": "#31C0F6", 658 | "name": "Nineteen Eighty Four", 659 | "value": 0 660 | }, 661 | { 662 | "id": "b19b236a-95e7-430f-a176-13df6b2557f7", 663 | "type": "scale", 664 | "hex": "#A500A5", 665 | "name": "Nineteen Eighty Four", 666 | "value": 0 667 | }, 668 | { 669 | "id": "fb6915cf-61c1-47b6-bdd9-8a42061dc9a6", 670 | "type": "scale", 671 | "hex": "#FF7E27", 672 | "name": "Nineteen Eighty Four", 673 | "value": 0 674 | } 675 | ], 676 | "xColumn": "_time", 677 | "fillColumns": [ 678 | "result" 679 | ], 680 | "xAxisLabel": "", 681 | "position": "stacked", 682 | "binCount": 0, 683 | "note": "", 684 | "showNoteWhenEmpty": false 685 | } 686 | } 687 | }, 688 | { 689 | "type": "view", 690 | "id": "05664807e4e9f000", 691 | "attributes": { 692 | "name": "Frontend status code distribution", 693 | "properties": { 694 | "shape": "chronograf-v2", 695 | "type": "histogram", 696 | "queries": [ 697 | { 698 | "text": "from(bucket: \"workshop\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"http_response\")\n |> filter(fn: (r) => r.server == \"http://frontend:3000/health\")", 699 | "editMode": "advanced", 700 | "name": "", 701 | "builderConfig": { 702 | "buckets": [], 703 | "tags": [ 704 | { 705 | "key": "_measurement", 706 | "values": [], 707 | "aggregateFunctionType": "filter" 708 | } 709 | ], 710 | "functions": [], 711 | "aggregateWindow": { 712 | "period": "auto" 713 | } 714 | } 715 | } 716 | ], 717 | "colors": [ 718 | { 719 | "id": "9d5cb0aa-18e4-4c81-a223-bec447bba26a", 720 | "type": "scale", 721 | "hex": "#31C0F6", 722 | "name": "Nineteen Eighty Four", 723 | "value": 0 724 | }, 725 | { 726 | "id": "b19b236a-95e7-430f-a176-13df6b2557f7", 727 | "type": "scale", 728 | "hex": "#A500A5", 729 | "name": "Nineteen Eighty Four", 730 | "value": 0 731 | }, 732 | { 733 | "id": "fb6915cf-61c1-47b6-bdd9-8a42061dc9a6", 734 | "type": "scale", 735 | "hex": "#FF7E27", 736 | "name": "Nineteen Eighty Four", 737 | "value": 0 738 | } 739 | ], 740 | "xColumn": "_time", 741 | "fillColumns": [ 742 | "result" 743 | ], 744 | "xAxisLabel": "", 745 | "position": "stacked", 746 | "binCount": 0, 747 | "note": "", 748 | "showNoteWhenEmpty": false 749 | } 750 | } 751 | }, 752 | { 753 | "type": "view", 754 | "id": "05664821c6e9f000", 755 | "attributes": { 756 | "name": "Pay status code distribution", 757 | "properties": { 758 | "shape": "chronograf-v2", 759 | "type": "histogram", 760 | "queries": [ 761 | { 762 | "text": "from(bucket: \"workshop\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"http_response\")\n |> filter(fn: (r) => r.server == \"http://pay:8080/health\")", 763 | "editMode": "advanced", 764 | "name": "", 765 | "builderConfig": { 766 | "buckets": [], 767 | "tags": [ 768 | { 769 | "key": "_measurement", 770 | "values": [], 771 | "aggregateFunctionType": "filter" 772 | } 773 | ], 774 | "functions": [], 775 | "aggregateWindow": { 776 | "period": "auto" 777 | } 778 | } 779 | } 780 | ], 781 | "colors": [ 782 | { 783 | "id": "9d5cb0aa-18e4-4c81-a223-bec447bba26a", 784 | "type": "scale", 785 | "hex": "#31C0F6", 786 | "name": "Nineteen Eighty Four", 787 | "value": 0 788 | }, 789 | { 790 | "id": "b19b236a-95e7-430f-a176-13df6b2557f7", 791 | "type": "scale", 792 | "hex": "#A500A5", 793 | "name": "Nineteen Eighty Four", 794 | "value": 0 795 | }, 796 | { 797 | "id": "fb6915cf-61c1-47b6-bdd9-8a42061dc9a6", 798 | "type": "scale", 799 | "hex": "#FF7E27", 800 | "name": "Nineteen Eighty Four", 801 | "value": 0 802 | } 803 | ], 804 | "xColumn": "_time", 805 | "fillColumns": [ 806 | "result" 807 | ], 808 | "xAxisLabel": "", 809 | "position": "stacked", 810 | "binCount": 0, 811 | "note": "", 812 | "showNoteWhenEmpty": false 813 | } 814 | } 815 | }, 816 | { 817 | "id": "05663a9e2be9f000", 818 | "type": "variable", 819 | "attributes": { 820 | "name": "bucket", 821 | "arguments": { 822 | "type": "query", 823 | "values": { 824 | "query": "buckets()\n |> filter(fn: (r) => r.name !~ /^_/)\n |> rename(columns: {name: \"_value\"})\n |> keep(columns: [\"_value\"])\n", 825 | "language": "flux" 826 | } 827 | }, 828 | "selected": null 829 | }, 830 | "relationships": { 831 | "label": { 832 | "data": [] 833 | } 834 | } 835 | } 836 | ] 837 | }, 838 | "labels": [] 839 | } 840 | ``` 841 | 842 | **PRO:** You can do another set of cells related to `latency`. It is an 843 | important signal because if it grows too much it means that for some reason your 844 | application is slower. 845 | 846 | ## Tips and Tricks 847 | 848 | The `inputs.http_response` documentation is 849 | [here](http://docs.influxdata.com/telegraf/v1.10/plugins/inputs/#http-response) 850 | 851 | The telegraf configuration is under `./telegraf/telegraf.conf` and in order to 852 | reload the configuration you can use `docker-compose restart telegraf`. 853 | 854 | \newpage 855 | -------------------------------------------------------------------------------- /lesson03-influxdb/SOLUTIONS.md: -------------------------------------------------------------------------------- 1 | # Solution Lesson 3 - Tracing 2 | 3 | ## Solution: Configure Telegraf to use the healthcheck from our apps 4 | 5 | With the `inputs.http_response` plugin you can list a set of users that will be 6 | called at every scheduled interval (in the [agent] configuration). 7 | 8 | Checkout the documentation this plugin because it has a lot of possible features 9 | that you can enable. For example it can match the content of the body sending 1 10 | or 0 based on the actual result. 11 | 12 | Copy paste this in `../lesson03-influxdb/telegraf/telegraf.conf` and restart 13 | telegraf. 14 | 15 | ```toml 16 | [[inputs.http_response]] 17 | urls = [ 18 | "http://frontend:3000/health", 19 | "http://pay:8080/health", 20 | "http://discount:3000/health", 21 | "http://item/health" 22 | ] 23 | response_timeout = "5s" 24 | method = "GET" 25 | ``` 26 | 27 | ```bash 28 | $docker-compose restart telegraf 29 | ``` 30 | 31 | ## Solution: Import a dashboard that shows service availability 32 | 33 | This is the [official 34 | documentation](https://v2.docs.influxdata.com/v2.0/visualize-data/dashboards/create-dashboard/#create-a-new-dashboard) 35 | 36 | \newpage 37 | -------------------------------------------------------------------------------- /lesson03-influxdb/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | telegraf: 5 | image: "telegraf:1.13.4" 6 | environment: 7 | HOSTNAME: "obs" 8 | INFLUX_TOKEN: "DxroipSs9T-DILZfllbm3ALyxzGG8AKqsdreuQdnbgQHfvbbgSTJKSzqV2V96w7QyxJj8cHnA6abIOCsiMSrTw==" 9 | volumes: 10 | - /var/run/docker.sock:/var/run/docker.sock 11 | - ./telegraf:/etc/telegraf 12 | depends_on: 13 | - influxdb 14 | networks: 15 | - obs 16 | - gaworkshop 17 | 18 | jaeger: 19 | build: 20 | context: ./jaeger 21 | depends_on: 22 | - influxdb 23 | ports: 24 | - "16686:16686" 25 | environment: 26 | SPAN_STORAGE_TYPE: grpc-plugin 27 | GRPC_STORAGE_PLUGIN_CONFIGURATION_FILE: /opt/influxdb-plugin/config.yaml 28 | GRPC_STORAGE_PLUGIN_BINARY: /usr/local/bin/jaeger-influxdb-linux 29 | COLLECTOR_ZIPKIN_HTTP_PORT: 9411 30 | volumes: 31 | - ./jaeger/influxdb-plugin:/opt/influxdb-plugin 32 | networks: 33 | - obs 34 | - gaworkshop 35 | 36 | influxdb: 37 | image: "quay.io/influxdb/influxdb:2.0.0-beta" 38 | ports: 39 | - "9999:9999" 40 | networks: 41 | - obs 42 | - gaworkshop 43 | 44 | networks: 45 | obs: 46 | gaworkshop: 47 | external: true 48 | -------------------------------------------------------------------------------- /lesson03-influxdb/jaeger/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/influxdb/jaeger-all-in-one-influxdb:gianarb as influxdb 2 | 3 | FROM jaegertracing/all-in-one:1.17 4 | 5 | COPY --from=influxdb /usr/local/bin/jaeger-influxdb-linux /usr/local/bin/jaeger-influxdb-linux 6 | -------------------------------------------------------------------------------- /lesson03-influxdb/jaeger/influxdb-plugin/config.yaml: -------------------------------------------------------------------------------- 1 | influxdb.host: http://influxdb:9999 2 | influxdb.organization: workshop 3 | influxdb.bucket: workshop 4 | influxdb.token: J17Vlbmpz_Rm85r9AUn5H7Oyzg89EelLmhS0RtA97iZuSx5sGAYpzFUbs5DGmpDsyGevJlLeNwp0_5GJBPTtVw== 5 | -------------------------------------------------------------------------------- /lesson03-influxdb/telegraf/telegraf.conf: -------------------------------------------------------------------------------- 1 | [agent] 2 | interval = "30s" 3 | round_interval = true 4 | metric_batch_size = 1000 5 | metric_buffer_limit = 10000 6 | collection_jitter = "0s" 7 | flush_interval = "10s" 8 | flush_jitter = "0s" 9 | precision = "" 10 | debug = false 11 | quiet = false 12 | logfile = "" 13 | hostname = "" 14 | omit_hostname = false 15 | [[outputs.influxdb_v2]] 16 | urls = ["http://influxdb:9999"] 17 | token = "" 18 | organization = "workshop" 19 | bucket = "workshop" 20 | [[inputs.cpu]] 21 | percpu = true 22 | totalcpu = true 23 | collect_cpu_time = false 24 | report_active = false 25 | [[inputs.disk]] 26 | ignore_fs = ["tmpfs", "devtmpfs", "devfs", "overlay", "aufs", "squashfs"] 27 | [[inputs.diskio]] 28 | [[inputs.mem]] 29 | [[inputs.net]] 30 | [[inputs.processes]] 31 | [[inputs.swap]] 32 | [[inputs.system]] 33 | [[inputs.docker]] 34 | -------------------------------------------------------------------------------- /lesson04-tracing/README.md: -------------------------------------------------------------------------------- 1 | # Distributed Tracing 2 | ## Lesson 4 3 | 4 | This lesson is probably the most complicated one. We are going to instrument our 5 | application using OpenTelemetry and OpenTracing, a "standard" set of libraries 6 | to build a trace across all your application. 7 | 8 | There are libraries in many languages and luckily for all the application we 9 | have! 10 | 11 | During `lesson3-influxdb` one of the application we started with 12 | `docker-compose` was Jaeger. Our distributed tracer. 13 | 14 | So we are ready to start instrumenting our favourite application. 15 | 16 | ### Exercise: Trace applications using OpenTracing and Jager 17 | 18 | **Time: 30 minutes** 19 | 20 | These are the libraries to use across the languages, open them, follow the 21 | documentation and we should try to figure out how to get an propagate a 22 | trace across all the languages 23 | 24 | * Item (PHP): 25 | * [jonahgeorge/jcchavezs/zipkin-opentracing](https://github.com/jcchavezs/zipkin-opentracing) 26 | * [opentracing/opentracing](https://github.com/opentracing/opentracing-php) 27 | * Discount: 28 | * [open-telemetry/opentelemetry-js](https://github.com/open-telemetry/opentelemetry-js) 29 | * Pay (Java): 30 | * [open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) 31 | * Frontend (Go): 32 | * [open-telemetry/opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go) 33 | 34 | ### Tips and tricks 35 | 36 | * To take the most from this exercise we need to have our trace propagated (or 37 | coming from) the other application. So the first things you can do is to 38 | `cherry-pick`, `merge` or `apply patch` from the `shopmany` or from the 39 | `workshop` repository the commit related to the other application (we saw how 40 | to it previously). In this way you will have already a working example from 41 | other applications to look at. 42 | 43 | ### Links 44 | 45 | * [FAQ: Distributed Tracing](https://gianarb.it/blog/faq-distributed-tracing) 46 | * [Context propagation over HTTP in Go](https://medium.com/@rakyll/context-propagation-over-http-in-go-d4540996e9b0) 47 | * [Jaeger Blog](https://medium.com/jaegertracing) 48 | * [OpenTracing: An Open Standard for Distributed Tracing](https://thenewstack.io/opentracing-open-standard-distributed-tracing/) 49 | * [Why You Can’t Afford to Ignore Distributed Tracing for Observability](https://thenewstack.io/why-you-cant-afford-to-ignore-distributed-tracing-for-observability/) 50 | * [Opentracing Tutorial by Yury Shkuro](https://github.com/yurishkuro/opentracing-tutorial/) 51 | 52 | \newpage 53 | -------------------------------------------------------------------------------- /lesson0x-justforpro/README.md: -------------------------------------------------------------------------------- 1 | # Just for Pro 2 | ## Lesson 5 3 | 4 | As you can see the lesson05 doesn't have solution and this is just for pro! 5 | I put this together because I hope somebody will have time at home or during the 6 | workshop if it is running ahead of schedule to take this challange. 7 | 8 | I never run ahead of schedule, that's why I do not have a solution file for this 9 | lesson! We are gonna get over this together. 10 | 11 | ### Exercise: Connect the dots! 12 | 13 | My dream is implement this roles across the applications: 14 | 15 | 1. All of the should return an header `X-Trace-ID` with the traceID. 16 | 2. As you know every span has a SpanContext that is contains a set of k/v 17 | propagated between application. They are usually good to pass `user_id` or 18 | other information that makes the context of the request clear. I would like 19 | to have these set of values printed out by the logger for every message. With 20 | the `trace_id` too. 21 | 22 | ### Motivation 23 | 24 | As we know the secret for true happiness is aggregation. We need to be good at 25 | connecting and exposing traces, events and metrics. This exercise is all about 26 | that. 27 | 28 | \newpage 29 | -------------------------------------------------------------------------------- /patches/0001-feat-discount-Added-healthcheck.patch: -------------------------------------------------------------------------------- 1 | From 64be9f3b6237f2851291bbd2187d951007530761 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Sun, 17 Mar 2019 11:27:17 +0100 4 | Subject: [PATCH] feat(discount): Added healthcheck 5 | 6 | Now the discount service has its own healthcheck endpoint. 7 | 8 | ``` 9 | METHOD: GET 10 | PATH: /health 11 | ``` 12 | 13 | It checks if th mongodb is reachable or not. 14 | 15 | Signed-off-by: Gianluca Arbezzano 16 | --- 17 | discount/server.js | 22 ++++++++++++++++++++++ 18 | 1 file changed, 22 insertions(+) 19 | 20 | diff --git a/discount/server.js b/discount/server.js 21 | index a7cb17b..cedde93 100644 22 | --- a/discount/server.js 23 | +++ b/discount/server.js 24 | @@ -8,6 +8,28 @@ const dbName = 'shopmany'; 25 | const client = new MongoClient(url, { useNewUrlParser: true }); 26 | app.use(errorHandler) 27 | 28 | +app.get("/health", function(req, res, next) { 29 | + var resbody = { 30 | + "status": "healthy", 31 | + checks: [], 32 | + }; 33 | + var resCode = 200; 34 | + 35 | + client.connect(function(err) { 36 | + var mongoCheck = { 37 | + "name": "mongo", 38 | + "status": "healthy", 39 | + }; 40 | + if (err != null) { 41 | + mongoCheck.error = err.toString(); 42 | + mongoCheck.status = "unhealthy"; 43 | + resbody.status = "unhealthy" 44 | + resCode = 500; 45 | + } 46 | + resbody.checks.push(mongoCheck); 47 | + res.status(resCode).json(resbody) 48 | + }); 49 | +}); 50 | 51 | app.get("/discount", function(req, res, next) { 52 | client.connect(function(err) { 53 | -- 54 | 2.23.0 55 | 56 | -------------------------------------------------------------------------------- /patches/0001-feat-discount-Added-logging-support.patch: -------------------------------------------------------------------------------- 1 | From ed6ce9b8dfcf1e396d979c09cf91b980b93a789d Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Sun, 17 Mar 2019 19:20:10 +0100 4 | Subject: [PATCH] feat(discount): Added logging support 5 | 6 | Signed-off-by: Gianluca Arbezzano 7 | --- 8 | discount/package.json | 1 + 9 | discount/server.js | 15 ++++++++++++++- 10 | 2 files changed, 15 insertions(+), 1 deletion(-) 11 | 12 | diff --git a/discount/package.json b/discount/package.json 13 | index 0647009..1640ae1 100644 14 | --- a/discount/package.json 15 | +++ b/discount/package.json 16 | @@ -11,6 +11,7 @@ 17 | "license": "ISC", 18 | "dependencies": { 19 | "express": "^4.16.4", 20 | + "express-pino-logger": "^4.0.0", 21 | "mongodb": "^3.1.13" 22 | } 23 | } 24 | diff --git a/discount/server.js b/discount/server.js 25 | index cedde93..50a32a9 100644 26 | --- a/discount/server.js 27 | +++ b/discount/server.js 28 | @@ -8,6 +8,12 @@ const dbName = 'shopmany'; 29 | const client = new MongoClient(url, { useNewUrlParser: true }); 30 | app.use(errorHandler) 31 | 32 | +const logger = require('pino')() 33 | +const expressPino = require('express-pino-logger')({ 34 | + logger: logger.child({"service": "httpd"}) 35 | +}) 36 | +app.use(expressPino) 37 | + 38 | app.get("/health", function(req, res, next) { 39 | var resbody = { 40 | "status": "healthy", 41 | @@ -21,6 +27,7 @@ app.get("/health", function(req, res, next) { 42 | "status": "healthy", 43 | }; 44 | if (err != null) { 45 | + req.log.warn(err.toString()); 46 | mongoCheck.error = err.toString(); 47 | mongoCheck.status = "unhealthy"; 48 | resbody.status = "unhealthy" 49 | @@ -36,6 +43,7 @@ app.get("/discount", function(req, res, next) { 50 | db = client.db(dbName); 51 | db.collection('discount').find({}).toArray(function(err, discounts) { 52 | if (err != null) { 53 | + req.log.error(err.toString()); 54 | return next(err) 55 | } 56 | var goodDiscount = null 57 | @@ -47,6 +55,7 @@ app.get("/discount", function(req, res, next) { 58 | if (goodDiscount != null) { 59 | res.json({"discount": goodDiscount}) 60 | } else { 61 | + req.log.warn("discount not found"); 62 | res.status(404).json({ error: 'Discount not found' }); 63 | } 64 | return 65 | @@ -55,10 +64,14 @@ app.get("/discount", function(req, res, next) { 66 | }); 67 | 68 | app.use(function(req, res, next) { 69 | + req.log.warn("route not found"); 70 | return res.status(404).json({error: "route not found"}); 71 | }); 72 | 73 | function errorHandler(err, req, res, next) { 74 | + req.log.error(err.toString(), { 75 | + error_status: err.status 76 | + }); 77 | var st = err.status 78 | if (st == 0 || st == null) { 79 | st = 500; 80 | @@ -68,5 +81,5 @@ function errorHandler(err, req, res, next) { 81 | } 82 | 83 | app.listen(3000, () => { 84 | - console.log("Server running on port 3000"); 85 | + logger.info("Server running on port 3000"); 86 | }); 87 | -- 88 | 2.23.0 89 | 90 | -------------------------------------------------------------------------------- /patches/0001-feat-discount-added-tracing.patch: -------------------------------------------------------------------------------- 1 | From d646f1892643d65b409397c47b363e4e01b4c38a Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Thu, 12 Mar 2020 21:43:52 +0100 4 | Subject: [PATCH] feat(discount): added tracing 5 | 6 | Signed-off-by: Gianluca Arbezzano 7 | --- 8 | discount/package.json | 8 ++++++++ 9 | discount/server.js | 26 ++++++++++++++++++-------- 10 | discount/tracer.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 11 | 3 files changed, 68 insertions(+), 8 deletions(-) 12 | create mode 100644 discount/tracer.js 13 | 14 | diff --git a/discount/package.json b/discount/package.json 15 | index 1640ae1..fff748e 100644 16 | --- a/discount/package.json 17 | +++ b/discount/package.json 18 | @@ -10,6 +10,14 @@ 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | + "@opentelemetry/api": "^0.5.0", 23 | + "@opentelemetry/exporter-jaeger": "^0.5.0", 24 | + "@opentelemetry/node": "^0.5.0", 25 | + "@opentelemetry/plugin-http": "^0.5.0", 26 | + "@opentelemetry/plugin-dns": "^0.5.0", 27 | + "@opentelemetry/plugin-mongodb": "^0.5.0", 28 | + "@opentelemetry/tracing": "^0.5.0", 29 | + "@opentelemetry/plugin-express": "^0.5.0", 30 | "express": "^4.16.4", 31 | "express-pino-logger": "^4.0.0", 32 | "mongodb": "^3.1.13" 33 | diff --git a/discount/server.js b/discount/server.js 34 | index 50a32a9..78b89e1 100644 35 | --- a/discount/server.js 36 | +++ b/discount/server.js 37 | @@ -1,20 +1,26 @@ 38 | -var express = require("express"); 39 | +'use strict'; 40 | + 41 | +const url = process.env.DISCOUNT_MONGODB_URL || 'mongodb://discountdb:27017'; 42 | +const jaegerHost = process.env.JAEGER_HOST || 'jaeger'; 43 | 44 | +const logger = require('pino')() 45 | +const tracer = require('./tracer')('discount', jaegerHost, logger); 46 | + 47 | +var express = require("express"); 48 | var app = express(); 49 | 50 | const MongoClient = require('mongodb').MongoClient; 51 | -const url = 'mongodb://discountdb:27017'; 52 | const dbName = 'shopmany'; 53 | const client = new MongoClient(url, { useNewUrlParser: true }); 54 | -app.use(errorHandler) 55 | 56 | -const logger = require('pino')() 57 | const expressPino = require('express-pino-logger')({ 58 | logger: logger.child({"service": "httpd"}) 59 | }) 60 | + 61 | +//app.use(errorHandler) 62 | app.use(expressPino) 63 | 64 | -app.get("/health", function(req, res, next) { 65 | +app.get("/health", function(req, res) { 66 | var resbody = { 67 | "status": "healthy", 68 | checks: [], 69 | @@ -40,7 +46,11 @@ app.get("/health", function(req, res, next) { 70 | 71 | app.get("/discount", function(req, res, next) { 72 | client.connect(function(err) { 73 | - db = client.db(dbName); 74 | + if (err != null) { 75 | + req.log.error(err.toString()); 76 | + return next(err) 77 | + } 78 | + let db = client.db(dbName); 79 | db.collection('discount').find({}).toArray(function(err, discounts) { 80 | if (err != null) { 81 | req.log.error(err.toString()); 82 | @@ -63,12 +73,12 @@ app.get("/discount", function(req, res, next) { 83 | }); 84 | }); 85 | 86 | -app.use(function(req, res, next) { 87 | +app.use(function(req, res) { 88 | req.log.warn("route not found"); 89 | return res.status(404).json({error: "route not found"}); 90 | }); 91 | 92 | -function errorHandler(err, req, res, next) { 93 | +function errorHandler(err, req, res) { 94 | req.log.error(err.toString(), { 95 | error_status: err.status 96 | }); 97 | diff --git a/discount/tracer.js b/discount/tracer.js 98 | new file mode 100644 99 | index 0000000..a39d0c7 100 | --- /dev/null 101 | +++ b/discount/tracer.js 102 | @@ -0,0 +1,42 @@ 103 | +'use strict'; 104 | + 105 | +const opentelemetry = require('@opentelemetry/api'); 106 | +const { NodeTracerProvider } = require('@opentelemetry/node'); 107 | +const { SimpleSpanProcessor } = require('@opentelemetry/tracing'); 108 | +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); 109 | +const { B3Propagator } = require('@opentelemetry/core'); 110 | + 111 | +module.exports = (serviceName, jaegerHost, logger) => { 112 | + const provider = new NodeTracerProvider({ 113 | + plugins: { 114 | + dns: { 115 | + enabled: true, 116 | + path: '@opentelemetry/plugin-dns', 117 | + }, 118 | + mongodb: { 119 | + enabled: true, 120 | + path: '@opentelemetry/plugin-mongodb', 121 | + }, 122 | + http: { 123 | + enabled: true, 124 | + path: '@opentelemetry/plugin-http', 125 | + }, 126 | + express: { 127 | + enabled: true, 128 | + path: '@opentelemetry/plugin-express', 129 | + }, 130 | + } 131 | + }); 132 | + 133 | + let exporter = new JaegerExporter({ 134 | + logger: logger, 135 | + serviceName: serviceName, 136 | + host: jaegerHost 137 | + }); 138 | + 139 | + provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); 140 | + provider.register({ 141 | + propagator: new B3Propagator(), 142 | + }); 143 | + return opentelemetry.trace.getTracer("discount"); 144 | +}; 145 | -- 146 | 2.23.0 147 | 148 | -------------------------------------------------------------------------------- /patches/0001-feat-frontend-Added-healthcheck-endpoint.patch: -------------------------------------------------------------------------------- 1 | From 07879e69ff65853685e7711420103f7f7e093c25 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Thu, 14 Mar 2019 18:34:17 +0100 4 | Subject: [PATCH] feat(frontend): Added healthcheck endpoint 5 | 6 | Now the frontend service has its healthcheck to validate if service that 7 | returns the list of items is working. 8 | 9 | Signed-off-by: Gianluca Arbezzano 10 | --- 11 | frontend/handler/health.go | 87 ++++++++++++++++++++++++++++++++++++++ 12 | frontend/main.go | 1 + 13 | 2 files changed, 88 insertions(+) 14 | create mode 100644 frontend/handler/health.go 15 | 16 | diff --git a/frontend/handler/health.go b/frontend/handler/health.go 17 | new file mode 100644 18 | index 0000000..733d28f 19 | --- /dev/null 20 | +++ b/frontend/handler/health.go 21 | @@ -0,0 +1,87 @@ 22 | +package handler 23 | + 24 | +import ( 25 | + "encoding/json" 26 | + "fmt" 27 | + "io/ioutil" 28 | + "net/http" 29 | + 30 | + "github.com/gianarb/shopmany/frontend/config" 31 | +) 32 | + 33 | +const unhealthy = "unhealty" 34 | +const healthy = "healthy" 35 | + 36 | +type healthResponse struct { 37 | + Status string 38 | + Checks []check 39 | +} 40 | + 41 | +type check struct { 42 | + Error string 43 | + Status string 44 | + Name string 45 | +} 46 | + 47 | +func NewHealthHandler(config config.Config, hclient *http.Client) *healthHandler { 48 | + return &healthHandler{ 49 | + config: config, 50 | + hclient: hclient, 51 | + } 52 | +} 53 | + 54 | +type healthHandler struct { 55 | + config config.Config 56 | + hclient *http.Client 57 | +} 58 | + 59 | +func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 60 | + b := healthResponse{ 61 | + Status: unhealthy, 62 | + Checks: []check{}, 63 | + } 64 | + w.Header().Add("Content-Type", "application/json") 65 | + 66 | + itemCheck := checkItem(h.config.ItemHost, h.hclient) 67 | + if itemCheck.Status == healthy { 68 | + b.Status = healthy 69 | + } 70 | + 71 | + b.Checks = append(b.Checks, itemCheck) 72 | + 73 | + body, err := json.Marshal(b) 74 | + if err != nil { 75 | + w.WriteHeader(500) 76 | + } 77 | + if b.Status == unhealthy { 78 | + w.WriteHeader(500) 79 | + } 80 | + fmt.Fprintf(w, string(body)) 81 | +} 82 | + 83 | +func checkItem(host string, hclient *http.Client) check { 84 | + c := check{ 85 | + Name: "item", 86 | + Error: "", 87 | + Status: unhealthy, 88 | + } 89 | + req, _ := http.NewRequest("GET", fmt.Sprintf("%s/health", host), nil) 90 | + resp, err := hclient.Do(req) 91 | + if err != nil { 92 | + c.Error = err.Error() 93 | + return c 94 | + } 95 | + defer resp.Body.Close() 96 | + if resp.StatusCode >= 200 && resp.StatusCode < 300 { 97 | + c.Status = healthy 98 | + return c 99 | + } 100 | + b, err := ioutil.ReadAll(resp.Body) 101 | + if err != nil { 102 | + c.Error = err.Error() 103 | + return c 104 | + } 105 | + c.Error = string(b) 106 | + 107 | + return c 108 | +} 109 | diff --git a/frontend/main.go b/frontend/main.go 110 | index f78d524..ee16adc 100644 111 | --- a/frontend/main.go 112 | +++ b/frontend/main.go 113 | @@ -28,6 +28,7 @@ func main() { 114 | http.Handle("/", fs) 115 | http.Handle("/api/items", handler.NewGetItemsHandler(config, httpClient)) 116 | http.Handle("/api/pay", handler.NewPayHandler(config, httpClient)) 117 | + http.Handle("/health", handler.NewHealthHandler(config, httpClient)) 118 | 119 | log.Println("Listening on port 3000...") 120 | http.ListenAndServe(":3000", nil) 121 | -- 122 | 2.23.0 123 | 124 | -------------------------------------------------------------------------------- /patches/0001-feat-frontend-Added-logging.patch: -------------------------------------------------------------------------------- 1 | From a305d0c7fcd110767c6ef696eb927d79b896e017 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Thu, 14 Mar 2019 19:17:55 +0100 4 | Subject: [PATCH] feat(frontend): Added logging 5 | 6 | Signed-off-by: Gianluca Arbezzano 7 | --- 8 | frontend/go.mod | 7 ++++++- 9 | frontend/go.sum | 6 ++++++ 10 | frontend/handler/getitems.go | 18 ++++++++++++++++++ 11 | frontend/handler/health.go | 9 +++++++++ 12 | frontend/handler/pay.go | 8 ++++++++ 13 | frontend/main.go | 34 +++++++++++++++++++++++++++++----- 14 | 6 files changed, 76 insertions(+), 6 deletions(-) 15 | 16 | diff --git a/frontend/go.mod b/frontend/go.mod 17 | index c9f9ab6..d86b3cb 100644 18 | --- a/frontend/go.mod 19 | +++ b/frontend/go.mod 20 | @@ -2,4 +2,9 @@ module github.com/gianarb/shopmany/frontend 21 | 22 | go 1.12 23 | 24 | -require github.com/jessevdk/go-flags v1.4.0 25 | +require ( 26 | + github.com/jessevdk/go-flags v1.4.0 27 | + go.uber.org/atomic v1.3.2 // indirect 28 | + go.uber.org/multierr v1.1.0 // indirect 29 | + go.uber.org/zap v1.9.1 30 | +) 31 | diff --git a/frontend/go.sum b/frontend/go.sum 32 | index bc46dae..ab7c346 100644 33 | --- a/frontend/go.sum 34 | +++ b/frontend/go.sum 35 | @@ -1,3 +1,9 @@ 36 | github.com/gianarb/shopmany v0.0.0-20190313091614-ac1c2f0595da h1:DxIHt5N7dhhxgDsk9pFvl4DAoggKEtNvQTOA7ZmC2eU= 37 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 38 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 39 | +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= 40 | +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 41 | +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 42 | +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 43 | +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= 44 | +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 45 | diff --git a/frontend/handler/getitems.go b/frontend/handler/getitems.go 46 | index 5019d55..54a3d32 100644 47 | --- a/frontend/handler/getitems.go 48 | +++ b/frontend/handler/getitems.go 49 | @@ -9,6 +9,7 @@ import ( 50 | "strconv" 51 | 52 | "github.com/gianarb/shopmany/frontend/config" 53 | + "go.uber.org/zap" 54 | ) 55 | 56 | type ItemsResponse struct { 57 | @@ -62,27 +63,41 @@ func getDiscountPerItem(ctx context.Context, hclient *http.Client, itemID int, d 58 | type getItemsHandler struct { 59 | config config.Config 60 | hclient *http.Client 61 | + logger *zap.Logger 62 | } 63 | 64 | func NewGetItemsHandler(config config.Config, hclient *http.Client) *getItemsHandler { 65 | + logger, _ := zap.NewProduction() 66 | return &getItemsHandler{ 67 | config: config, 68 | hclient: hclient, 69 | + logger: logger, 70 | } 71 | } 72 | 73 | +func (h *getItemsHandler) WithLogger(logger *zap.Logger) { 74 | + h.logger = logger 75 | +} 76 | + 77 | func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 78 | ctx := r.Context() 79 | w.Header().Add("Content-Type", "application/json") 80 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/item", h.config.ItemHost), nil) 81 | if err != nil { 82 | + h.logger.Error(err.Error()) 83 | http.Error(w, err.Error(), 500) 84 | return 85 | } 86 | resp, err := h.hclient.Do(req) 87 | + if err != nil { 88 | + h.logger.Error(err.Error()) 89 | + http.Error(w, err.Error(), 500) 90 | + return 91 | + } 92 | defer resp.Body.Close() 93 | body, err := ioutil.ReadAll(resp.Body) 94 | if err != nil { 95 | + h.logger.Error(err.Error()) 96 | http.Error(w, err.Error(), 500) 97 | return 98 | } 99 | @@ -91,6 +106,7 @@ func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 100 | } 101 | err = json.Unmarshal(body, &items) 102 | if err != nil { 103 | + h.logger.Error(err.Error()) 104 | http.Error(w, err.Error(), 500) 105 | return 106 | } 107 | @@ -98,6 +114,7 @@ func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 108 | for k, item := range items.Items { 109 | d, err := getDiscountPerItem(ctx, h.hclient, item.ID, h.config.DiscountHost) 110 | if err != nil { 111 | + h.logger.Error(err.Error()) 112 | http.Error(w, err.Error(), 500) 113 | continue 114 | } 115 | @@ -106,6 +123,7 @@ func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 116 | 117 | b, err := json.Marshal(items) 118 | if err != nil { 119 | + h.logger.Error(err.Error()) 120 | http.Error(w, err.Error(), 500) 121 | return 122 | } 123 | diff --git a/frontend/handler/health.go b/frontend/handler/health.go 124 | index 733d28f..fa9e52f 100644 125 | --- a/frontend/handler/health.go 126 | +++ b/frontend/handler/health.go 127 | @@ -7,6 +7,7 @@ import ( 128 | "net/http" 129 | 130 | "github.com/gianarb/shopmany/frontend/config" 131 | + "go.uber.org/zap" 132 | ) 133 | 134 | const unhealthy = "unhealty" 135 | @@ -24,15 +25,22 @@ type check struct { 136 | } 137 | 138 | func NewHealthHandler(config config.Config, hclient *http.Client) *healthHandler { 139 | + logger, _ := zap.NewProduction() 140 | return &healthHandler{ 141 | config: config, 142 | hclient: hclient, 143 | + logger: logger, 144 | } 145 | } 146 | 147 | type healthHandler struct { 148 | config config.Config 149 | hclient *http.Client 150 | + logger *zap.Logger 151 | +} 152 | + 153 | +func (h *healthHandler) WithLogger(logger *zap.Logger) { 154 | + h.logger = logger 155 | } 156 | 157 | func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 158 | @@ -51,6 +59,7 @@ func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 159 | 160 | body, err := json.Marshal(b) 161 | if err != nil { 162 | + h.logger.Error(err.Error()) 163 | w.WriteHeader(500) 164 | } 165 | if b.Status == unhealthy { 166 | diff --git a/frontend/handler/pay.go b/frontend/handler/pay.go 167 | index b3a8a24..f3e5434 100644 168 | --- a/frontend/handler/pay.go 169 | +++ b/frontend/handler/pay.go 170 | @@ -5,20 +5,28 @@ import ( 171 | "net/http" 172 | 173 | "github.com/gianarb/shopmany/frontend/config" 174 | + "go.uber.org/zap" 175 | ) 176 | 177 | type payHandler struct { 178 | config config.Config 179 | hclient *http.Client 180 | + logger *zap.Logger 181 | } 182 | 183 | func NewPayHandler(config config.Config, hclient *http.Client) *payHandler { 184 | + logger, _ := zap.NewProduction() 185 | return &payHandler{ 186 | config: config, 187 | hclient: hclient, 188 | + logger: logger, 189 | } 190 | } 191 | 192 | +func (h *payHandler) WithLogger(logger *zap.Logger) { 193 | + h.logger = logger 194 | +} 195 | + 196 | func (h *payHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 197 | w.Header().Add("Content-Type", "application/json") 198 | if r.Method != "POST" { 199 | diff --git a/frontend/main.go b/frontend/main.go 200 | index ee16adc..35a084c 100644 201 | --- a/frontend/main.go 202 | +++ b/frontend/main.go 203 | @@ -8,9 +8,12 @@ import ( 204 | "github.com/gianarb/shopmany/frontend/config" 205 | "github.com/gianarb/shopmany/frontend/handler" 206 | flags "github.com/jessevdk/go-flags" 207 | + "go.uber.org/zap" 208 | ) 209 | 210 | func main() { 211 | + logger, _ := zap.NewProduction() 212 | + defer logger.Sync() 213 | config := config.Config{} 214 | _, err := flags.Parse(&config) 215 | 216 | @@ -22,14 +25,35 @@ func main() { 217 | fmt.Printf("Pay Host: %v\n", config.PayHost) 218 | fmt.Printf("Discount Host: %v\n", config.DiscountHost) 219 | 220 | + mux := http.NewServeMux() 221 | + 222 | httpClient := &http.Client{} 223 | fs := http.FileServer(http.Dir("static")) 224 | 225 | - http.Handle("/", fs) 226 | - http.Handle("/api/items", handler.NewGetItemsHandler(config, httpClient)) 227 | - http.Handle("/api/pay", handler.NewPayHandler(config, httpClient)) 228 | - http.Handle("/health", handler.NewHealthHandler(config, httpClient)) 229 | + httpdLogger := logger.With(zap.String("service", "httpd")) 230 | + getItemsHandler := handler.NewGetItemsHandler(config, httpClient) 231 | + getItemsHandler.WithLogger(logger) 232 | + payHandler := handler.NewPayHandler(config, httpClient) 233 | + payHandler.WithLogger(logger) 234 | + healthHandler := handler.NewHealthHandler(config, httpClient) 235 | + healthHandler.WithLogger(logger) 236 | + 237 | + mux.Handle("/", fs) 238 | + mux.Handle("/api/items", getItemsHandler) 239 | + mux.Handle("/api/pay", payHandler) 240 | + mux.Handle("/health", healthHandler) 241 | 242 | log.Println("Listening on port 3000...") 243 | - http.ListenAndServe(":3000", nil) 244 | + http.ListenAndServe(":3000", loggingMiddleware(httpdLogger.With(zap.String("from", "middleware")), mux)) 245 | +} 246 | + 247 | +func loggingMiddleware(logger *zap.Logger, h http.Handler) http.Handler { 248 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 249 | + logger.Info( 250 | + "HTTP Request", 251 | + zap.String("Path", r.URL.Path), 252 | + zap.String("Method", r.Method), 253 | + zap.String("RemoteAddr", r.RemoteAddr)) 254 | + h.ServeHTTP(w, r) 255 | + }) 256 | } 257 | -- 258 | 2.23.0 259 | 260 | -------------------------------------------------------------------------------- /patches/0001-feat-frontend-Instrument-http-handlers.patch: -------------------------------------------------------------------------------- 1 | From 297539e0b76235ad8ef39c5e8e0f4c1080b12cf8 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Wed, 11 Mar 2020 21:48:37 +0100 4 | Subject: [PATCH] feat(frontend): Instrument http handlers 5 | 6 | OpenTelemetry is is made of exporters, the easier to use is the stdout 7 | one. It prints JSON to the process stdout. 8 | 9 | Stdout is a good exporter but not the one you should use in production. 10 | There are a lot of open source tracer around: Zipkin, Jaeger, Honeycomb, 11 | AWS X-Ray, Google StackDriver. I tend to use Jaeger because it is in Go 12 | and it is open source. 13 | 14 | This commit adds the flag `--tracer` by default it is set to stdout, but 15 | if you use `--tracer jaeger` the traces will be send to Jaeger. You can 16 | override the Jaeger URl with `--tracer-jaeger-address` 17 | 18 | Signed-off-by: Gianluca Arbezzano 19 | --- 20 | docker-compose.yaml | 2 +- 21 | frontend/config/config.go | 8 +++--- 22 | frontend/handler/getitems.go | 11 ++++++++ 23 | frontend/handler/health.go | 9 +++++-- 24 | frontend/handler/pay.go | 4 +++ 25 | frontend/main.go | 49 +++++++++++++++++++++++++++++++++--- 26 | 6 files changed, 74 insertions(+), 9 deletions(-) 27 | 28 | diff --git a/docker-compose.yaml b/docker-compose.yaml 29 | index bb9b123..6474bea 100644 30 | --- a/docker-compose.yaml 31 | +++ b/docker-compose.yaml 32 | @@ -90,7 +90,7 @@ services: 33 | # frontend is the ui of the project 34 | frontend: 35 | image: golang:1.14.0-stretch 36 | - command: ["go", "run", "-mod", "vendor", "./main.go"] 37 | + command: ["go", "run", "-mod", "vendor", "./main.go", "--tracer", "jaeger", "--tracer-jaeger-address", "http://jaeger:14268/api/traces"] 38 | ports: 39 | - '3000:3000' 40 | volumes: 41 | diff --git a/frontend/config/config.go b/frontend/config/config.go 42 | index 5524a5b..55bd702 100644 43 | --- a/frontend/config/config.go 44 | +++ b/frontend/config/config.go 45 | @@ -1,7 +1,9 @@ 46 | package config 47 | 48 | type Config struct { 49 | - ItemHost string `long:"item-host" description:"The hostname where the item service is located" default:"http://item"` 50 | - DiscountHost string `long:"discount-host" description:"The hostname where the discount service is located" default:"http://discount:3000"` 51 | - PayHost string `long:"pay-host" description:"The hostname where the pay service is located" default:"http://pay:8080"` 52 | + ItemHost string `long:"item-host" description:"The hostname where the item service is located" default:"http://item"` 53 | + DiscountHost string `long:"discount-host" description:"The hostname where the discount service is located" default:"http://discount:3000"` 54 | + PayHost string `long:"pay-host" description:"The hostname where the pay service is located" default:"http://pay:8080"` 55 | + Tracer string `long:"tracer" description:"The place where traces get shiped to. By default it is stdout. Jaeger is also supported" default:"stdout"` 56 | + JaegerAddress string `long:"tracer-jaeger-address" description:"If Jaeger is set as tracer output this is the way you ovverride where to ship data to" default:"http://localhost:14268/api/traces"` 57 | } 58 | diff --git a/frontend/handler/getitems.go b/frontend/handler/getitems.go 59 | index 54a3d32..887b899 100644 60 | --- a/frontend/handler/getitems.go 61 | +++ b/frontend/handler/getitems.go 62 | @@ -9,6 +9,9 @@ import ( 63 | "strconv" 64 | 65 | "github.com/gianarb/shopmany/frontend/config" 66 | + "go.opentelemetry.io/otel/api/propagation" 67 | + "go.opentelemetry.io/otel/api/trace" 68 | + "go.opentelemetry.io/otel/plugin/httptrace" 69 | "go.uber.org/zap" 70 | ) 71 | 72 | @@ -32,14 +35,20 @@ type DiscountResponse struct { 73 | } `json:"discount"` 74 | } 75 | 76 | +var props = propagation.New(propagation.WithInjectors(trace.B3{})) 77 | + 78 | func getDiscountPerItem(ctx context.Context, hclient *http.Client, itemID int, discountHost string) (int, error) { 79 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/discount", discountHost), nil) 80 | if err != nil { 81 | return 0, err 82 | } 83 | + 84 | q := req.URL.Query() 85 | q.Add("itemid", strconv.Itoa(itemID)) 86 | req.URL.RawQuery = q.Encode() 87 | + 88 | + ctx, req = httptrace.W3C(ctx, req) 89 | + propagation.InjectHTTP(ctx, props, req.Header) 90 | resp, err := hclient.Do(req) 91 | if err != nil { 92 | return 0, err 93 | @@ -88,6 +97,8 @@ func (h *getItemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 94 | http.Error(w, err.Error(), 500) 95 | return 96 | } 97 | + ctx, req = httptrace.W3C(ctx, req) 98 | + propagation.InjectHTTP(ctx, props, req.Header) 99 | resp, err := h.hclient.Do(req) 100 | if err != nil { 101 | h.logger.Error(err.Error()) 102 | diff --git a/frontend/handler/health.go b/frontend/handler/health.go 103 | index fa9e52f..39fd873 100644 104 | --- a/frontend/handler/health.go 105 | +++ b/frontend/handler/health.go 106 | @@ -1,12 +1,15 @@ 107 | package handler 108 | 109 | import ( 110 | + "context" 111 | "encoding/json" 112 | "fmt" 113 | "io/ioutil" 114 | "net/http" 115 | 116 | "github.com/gianarb/shopmany/frontend/config" 117 | + "go.opentelemetry.io/otel/api/propagation" 118 | + "go.opentelemetry.io/otel/plugin/httptrace" 119 | "go.uber.org/zap" 120 | ) 121 | 122 | @@ -50,7 +53,7 @@ func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 123 | } 124 | w.Header().Add("Content-Type", "application/json") 125 | 126 | - itemCheck := checkItem(h.config.ItemHost, h.hclient) 127 | + itemCheck := checkItem(r.Context(), h.config.ItemHost, h.hclient) 128 | if itemCheck.Status == healthy { 129 | b.Status = healthy 130 | } 131 | @@ -68,13 +71,15 @@ func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 132 | fmt.Fprintf(w, string(body)) 133 | } 134 | 135 | -func checkItem(host string, hclient *http.Client) check { 136 | +func checkItem(ctx context.Context, host string, hclient *http.Client) check { 137 | c := check{ 138 | Name: "item", 139 | Error: "", 140 | Status: unhealthy, 141 | } 142 | req, _ := http.NewRequest("GET", fmt.Sprintf("%s/health", host), nil) 143 | + ctx, req = httptrace.W3C(ctx, req) 144 | + propagation.InjectHTTP(ctx, props, req.Header) 145 | resp, err := hclient.Do(req) 146 | if err != nil { 147 | c.Error = err.Error() 148 | diff --git a/frontend/handler/pay.go b/frontend/handler/pay.go 149 | index f3e5434..49d63c1 100644 150 | --- a/frontend/handler/pay.go 151 | +++ b/frontend/handler/pay.go 152 | @@ -5,6 +5,8 @@ import ( 153 | "net/http" 154 | 155 | "github.com/gianarb/shopmany/frontend/config" 156 | + "go.opentelemetry.io/otel/api/propagation" 157 | + "go.opentelemetry.io/otel/plugin/httptrace" 158 | "go.uber.org/zap" 159 | ) 160 | 161 | @@ -38,6 +40,8 @@ func (h *payHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 162 | http.Error(w, err.Error(), 500) 163 | return 164 | } 165 | + ctx, req := httptrace.W3C(r.Context(), req) 166 | + propagation.InjectHTTP(ctx, props, req.Header) 167 | req.Header.Add("Content-Type", "application/json") 168 | resp, err := h.hclient.Do(req) 169 | if err != nil { 170 | diff --git a/frontend/main.go b/frontend/main.go 171 | index 35a084c..5f5e157 100644 172 | --- a/frontend/main.go 173 | +++ b/frontend/main.go 174 | @@ -8,6 +8,11 @@ import ( 175 | "github.com/gianarb/shopmany/frontend/config" 176 | "github.com/gianarb/shopmany/frontend/handler" 177 | flags "github.com/jessevdk/go-flags" 178 | + "go.opentelemetry.io/otel/api/global" 179 | + "go.opentelemetry.io/otel/exporters/trace/jaeger" 180 | + "go.opentelemetry.io/otel/exporters/trace/stdout" 181 | + "go.opentelemetry.io/otel/plugin/othttp" 182 | + sdktrace "go.opentelemetry.io/otel/sdk/trace" 183 | "go.uber.org/zap" 184 | ) 185 | 186 | @@ -21,6 +26,44 @@ func main() { 187 | panic(err) 188 | } 189 | 190 | + exporter, err := stdout.NewExporter(stdout.Options{PrettyPrint: true}) 191 | + if err != nil { 192 | + log.Fatal(err) 193 | + } 194 | + tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), 195 | + sdktrace.WithSyncer(exporter)) 196 | + if err != nil { 197 | + log.Fatal(err) 198 | + } 199 | + global.SetTraceProvider(tp) 200 | + 201 | + if config.Tracer == "jaeger" { 202 | + 203 | + logger.Info("Used the tracer output jaeger") 204 | + // Create Jaeger Exporter 205 | + exporter, err := jaeger.NewExporter( 206 | + jaeger.WithCollectorEndpoint(config.JaegerAddress), 207 | + jaeger.WithProcess(jaeger.Process{ 208 | + ServiceName: "frontend", 209 | + }), 210 | + ) 211 | + if err != nil { 212 | + log.Fatal(err) 213 | + } 214 | + 215 | + // For demoing purposes, always sample. In a production application, you should 216 | + // configure this to a trace.ProbabilitySampler set at the desired 217 | + // probability. 218 | + tp, err := sdktrace.NewProvider( 219 | + sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), 220 | + sdktrace.WithSyncer(exporter)) 221 | + if err != nil { 222 | + log.Fatal(err) 223 | + } 224 | + global.SetTraceProvider(tp) 225 | + defer exporter.Flush() 226 | + } 227 | + 228 | fmt.Printf("Item Host: %v\n", config.ItemHost) 229 | fmt.Printf("Pay Host: %v\n", config.PayHost) 230 | fmt.Printf("Discount Host: %v\n", config.DiscountHost) 231 | @@ -39,9 +82,9 @@ func main() { 232 | healthHandler.WithLogger(logger) 233 | 234 | mux.Handle("/", fs) 235 | - mux.Handle("/api/items", getItemsHandler) 236 | - mux.Handle("/api/pay", payHandler) 237 | - mux.Handle("/health", healthHandler) 238 | + mux.Handle("/api/items", othttp.NewHandler(getItemsHandler, "http.GetItems")) 239 | + mux.Handle("/api/pay", othttp.NewHandler(payHandler, "http.Pay")) 240 | + mux.Handle("/health", othttp.NewHandler(healthHandler, "http.health")) 241 | 242 | log.Println("Listening on port 3000...") 243 | http.ListenAndServe(":3000", loggingMiddleware(httpdLogger.With(zap.String("from", "middleware")), mux)) 244 | -- 245 | 2.23.0 246 | 247 | -------------------------------------------------------------------------------- /patches/0001-feat-items-Added-healtcheck-endpoint.patch: -------------------------------------------------------------------------------- 1 | From 378cd70c0eac5bf0ab97903e09e4b319d8e5f2eb Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Thu, 14 Mar 2019 09:40:16 +0100 4 | Subject: [PATCH] feat(items): Added healtcheck endpoint 5 | 6 | Signed-off-by: Gianluca Arbezzano 7 | --- 8 | items/config/autoload/containers.global.php | 2 +- 9 | items/config/routes.php | 2 + 10 | items/src/App/src/Handler/Health.php | 49 +++++++++++++++++++++ 11 | items/src/App/src/Handler/HealthFactory.php | 14 ++++++ 12 | 4 files changed, 66 insertions(+), 1 deletion(-) 13 | create mode 100644 items/src/App/src/Handler/Health.php 14 | create mode 100644 items/src/App/src/Handler/HealthFactory.php 15 | 16 | diff --git a/items/config/autoload/containers.global.php b/items/config/autoload/containers.global.php 17 | index 3166620..511480b 100644 18 | --- a/items/config/autoload/containers.global.php 19 | +++ b/items/config/autoload/containers.global.php 20 | @@ -14,12 +14,12 @@ return [ 21 | // not require arguments to the constructor. Map a service name to the 22 | // class name. 23 | 'invokables' => [ 24 | - // Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class, 25 | ], 26 | // Use 'factories' for services provided by callbacks/factory classes. 27 | 'factories' => [ 28 | App\Service\ItemService::class => App\Service\ItemServiceFactory::class, 29 | App\Handler\Item::class => App\Handler\ItemFactory::class, 30 | + App\Handler\Health::class => App\Handler\HealthFactory::class, 31 | ], 32 | ], 33 | ]; 34 | diff --git a/items/config/routes.php b/items/config/routes.php 35 | index fc0abb7..e37ed12 100644 36 | --- a/items/config/routes.php 37 | +++ b/items/config/routes.php 38 | @@ -6,6 +6,7 @@ use Psr\Container\ContainerInterface; 39 | use Zend\Expressive\Application; 40 | use Zend\Expressive\MiddlewareFactory; 41 | use App\Handler\Item; 42 | +use App\Handler\Health; 43 | 44 | /** 45 | * Setup routes with a single request method: 46 | @@ -22,4 +23,5 @@ use App\Handler\Item; 47 | */ 48 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { 49 | $app->get('/item', Item::class); 50 | + $app->get('/health', Health::class); 51 | }; 52 | diff --git a/items/src/App/src/Handler/Health.php b/items/src/App/src/Handler/Health.php 53 | new file mode 100644 54 | index 0000000..47c210e 55 | --- /dev/null 56 | +++ b/items/src/App/src/Handler/Health.php 57 | @@ -0,0 +1,49 @@ 58 | +username = $username; 73 | + $this->hostname = $hostname; 74 | + $this->password = $password; 75 | + $this->dbname = $dbname; 76 | + } 77 | + 78 | + public function handle(ServerRequestInterface $request) : ResponseInterface 79 | + { 80 | + $statusCode = 500; 81 | + $body = new \stdClass(); 82 | + $body->status = "unhealthy"; 83 | + $mySqlCheck = new \stdClass(); 84 | + $mySqlCheck->name = "mysql"; 85 | + $mySqlCheck->status = "unhealthy"; 86 | + 87 | + try { 88 | + $this->pdo = new PDO("mysql:host=$this->hostname;port=3306;dbname=$this->dbname", $this->username, $this->password); 89 | + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 90 | + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 91 | + 92 | + $statusCode = 200; 93 | + $body->status = "healthy"; 94 | + $mySqlCheck->status = "healthy"; 95 | + 96 | + } catch(\PDOException $ex){ 97 | + $mySqlCheck->error = $ex->getMessage(); 98 | + } 99 | + $body->checks = [$mySqlCheck]; 100 | + 101 | + $response = new JsonResponse($body); 102 | + $response = $response->withStatus($statusCode); 103 | + 104 | + return $response; 105 | + } 106 | +} 107 | diff --git a/items/src/App/src/Handler/HealthFactory.php b/items/src/App/src/Handler/HealthFactory.php 108 | new file mode 100644 109 | index 0000000..e974128 110 | --- /dev/null 111 | +++ b/items/src/App/src/Handler/HealthFactory.php 112 | @@ -0,0 +1,14 @@ 113 | +get('config')['mysql']; 124 | + return new Health($mysqlConfig['hostname'], $mysqlConfig['user'], $mysqlConfig['pass'], $mysqlConfig['dbname']); 125 | + } 126 | +} 127 | -- 128 | 2.23.0 129 | 130 | -------------------------------------------------------------------------------- /patches/0001-feat-items-Injected-logger.patch: -------------------------------------------------------------------------------- 1 | From f412a291861afc30784da7dcdf54defcfb7d9476 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Thu, 14 Mar 2019 10:27:38 +0100 4 | Subject: [PATCH] feat(items): Injected logger 5 | 6 | The item service is not logging using Monolog 7 | 8 | Signed-off-by: Gianluca Arbezzano 9 | --- 10 | items/Dockerfile | 5 ++ 11 | items/composer.json | 3 +- 12 | items/config/autoload/containers.global.php | 2 + 13 | items/config/pipeline.php | 2 + 14 | items/src/App/src/Handler/Item.php | 10 ++++ 15 | items/src/App/src/Handler/ItemFactory.php | 3 +- 16 | .../App/src/Middleware/LoggerMiddleware.php | 54 +++++++++++++++++++ 17 | .../Middleware/LoggerMiddlewareFactory.php | 20 +++++++ 18 | items/src/App/src/Service/LoggerFactory.php | 20 +++++++ 19 | 9 files changed, 117 insertions(+), 2 deletions(-) 20 | create mode 100644 items/src/App/src/Middleware/LoggerMiddleware.php 21 | create mode 100644 items/src/App/src/Middleware/LoggerMiddlewareFactory.php 22 | create mode 100644 items/src/App/src/Service/LoggerFactory.php 23 | 24 | diff --git a/items/Dockerfile b/items/Dockerfile 25 | index 58a1e86..2184cb1 100644 26 | --- a/items/Dockerfile 27 | +++ b/items/Dockerfile 28 | @@ -2,3 +2,8 @@ FROM php:7.2-apache 29 | 30 | RUN a2enmod rewrite 31 | RUN docker-php-ext-install pdo_mysql 32 | + 33 | +RUN find /etc/apache2/sites-enabled/* -exec sed -i 's/#*[Cc]ustom[Ll]og/#CustomLog/g' {} \; 34 | +RUN find /etc/apache2/sites-enabled/* -exec sed -i 's/#*[Ee]rror[Ll]og/#ErrorLog/g' {} \; 35 | +RUN a2disconf other-vhosts-access-log 36 | + 37 | diff --git a/items/composer.json b/items/composer.json 38 | index 50bea51..c0badf9 100644 39 | --- a/items/composer.json 40 | +++ b/items/composer.json 41 | @@ -46,7 +46,8 @@ 42 | "zendframework/zend-expressive-fastroute": "^3.0", 43 | "zendframework/zend-expressive-helpers": "^5.0", 44 | "zendframework/zend-servicemanager": "^3.3", 45 | - "zendframework/zend-stdlib": "^3.1" 46 | + "zendframework/zend-stdlib": "^3.1", 47 | + "monolog/monolog": "1.24.0" 48 | }, 49 | "require-dev": { 50 | "phpunit/phpunit": "^7.0.1", 51 | diff --git a/items/config/autoload/containers.global.php b/items/config/autoload/containers.global.php 52 | index 511480b..12a5b18 100644 53 | --- a/items/config/autoload/containers.global.php 54 | +++ b/items/config/autoload/containers.global.php 55 | @@ -20,6 +20,8 @@ return [ 56 | App\Service\ItemService::class => App\Service\ItemServiceFactory::class, 57 | App\Handler\Item::class => App\Handler\ItemFactory::class, 58 | App\Handler\Health::class => App\Handler\HealthFactory::class, 59 | + "Logger" => App\Service\LoggerFactory::class, 60 | + App\Middleware\LoggerMiddleware::class => App\Middleware\LoggerMiddlewareFactory::class, 61 | ], 62 | ], 63 | ]; 64 | diff --git a/items/config/pipeline.php b/items/config/pipeline.php 65 | index cfe8f0b..e9287fd 100644 66 | --- a/items/config/pipeline.php 67 | +++ b/items/config/pipeline.php 68 | @@ -14,11 +14,13 @@ use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware; 69 | use Zend\Expressive\Router\Middleware\MethodNotAllowedMiddleware; 70 | use Zend\Expressive\Router\Middleware\RouteMiddleware; 71 | use Zend\Stratigility\Middleware\ErrorHandler; 72 | +use App\Middleware\LoggerMiddleware; 73 | 74 | /** 75 | * Setup middleware pipeline: 76 | */ 77 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { 78 | + $app->pipe($container->get(LoggerMiddleware::class)); 79 | // The error handler should be the first (most outer) middleware to catch 80 | // all Exceptions. 81 | $app->pipe(ErrorHandler::class); 82 | diff --git a/items/src/App/src/Handler/Item.php b/items/src/App/src/Handler/Item.php 83 | index 2ea3d66..f1d9a64 100644 84 | --- a/items/src/App/src/Handler/Item.php 85 | +++ b/items/src/App/src/Handler/Item.php 86 | @@ -6,18 +6,28 @@ use Psr\Http\Message\ServerRequestInterface; 87 | use Psr\Http\Server\RequestHandlerInterface; 88 | use Zend\Diactoros\Response\JsonResponse; 89 | use App\Service\ItemService; 90 | +use Monolog\Logger; 91 | +use Monolog\Processor\TagProcessor; 92 | 93 | class Item implements RequestHandlerInterface 94 | { 95 | private $itemService; 96 | + private $logger; 97 | 98 | function __construct(ItemService $itemService) { 99 | $this->itemService = $itemService; 100 | + $this->logger = new Logger('item_service'); 101 | } 102 | 103 | public function handle(ServerRequestInterface $request) : ResponseInterface 104 | { 105 | + $this->logger->info("Get list of items"); 106 | $items = $this->itemService->list(); 107 | + $this->logger->info("Retrived list of items", ["num_items" => count($items)]); 108 | return new JsonResponse(['items' => $items]); 109 | } 110 | + 111 | + public function withLogger($logger) { 112 | + $this->logger = $logger; 113 | + } 114 | } 115 | diff --git a/items/src/App/src/Handler/ItemFactory.php b/items/src/App/src/Handler/ItemFactory.php 116 | index a1db1df..7de3a2d 100644 117 | --- a/items/src/App/src/Handler/ItemFactory.php 118 | +++ b/items/src/App/src/Handler/ItemFactory.php 119 | @@ -9,6 +9,7 @@ class ItemFactory 120 | { 121 | public function __invoke(ContainerInterface $container) 122 | { 123 | - return new Item($container->get(ItemService::class)); 124 | + $h = new Item($container->get(ItemService::class)); 125 | + return $h; 126 | } 127 | } 128 | diff --git a/items/src/App/src/Middleware/LoggerMiddleware.php b/items/src/App/src/Middleware/LoggerMiddleware.php 129 | new file mode 100644 130 | index 0000000..64538c1 131 | --- /dev/null 132 | +++ b/items/src/App/src/Middleware/LoggerMiddleware.php 133 | @@ -0,0 +1,54 @@ 134 | +logger = $logger; 151 | + $this->logger->pushProcessor(new TagProcessor([ 152 | + "service" => "logger_middleware", 153 | + ])); 154 | + } 155 | + 156 | + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 157 | + { 158 | + $isGood = true; 159 | + try { 160 | + $response = $handler->handle($request); 161 | + } catch (Throwable $e) { 162 | + $this->logger->panic("HTTP Server", [ 163 | + "path", $request->getUri()->getPath(), 164 | + "method", $request->getMethod(), 165 | + "status_code" => $response->getStatusCode(), 166 | + "error" => $e->getMessage(), 167 | + ]); 168 | + $isGood=false; 169 | + } 170 | + if ($isGood) { 171 | + if ($response->getStatusCode() >= 200 && $response->getStatusCode() <= 299) { 172 | + $this->logger->info("HTTP Server", [ 173 | + "path", $request->getUri()->getPath(), 174 | + "method", $request->getMethod(), 175 | + "status_code" => $response->getStatusCode(), 176 | + ]); 177 | + } else { 178 | + $this->logger->warn("HTTP Server", [ 179 | + "path", $request->getUri()->getPath(), 180 | + "method", $request->getMethod(), 181 | + "status_code" => $response->getStatusCode(), 182 | + ]); 183 | + } 184 | + } 185 | + return $response; 186 | + } 187 | +} 188 | diff --git a/items/src/App/src/Middleware/LoggerMiddlewareFactory.php b/items/src/App/src/Middleware/LoggerMiddlewareFactory.php 189 | new file mode 100644 190 | index 0000000..bd4fba9 191 | --- /dev/null 192 | +++ b/items/src/App/src/Middleware/LoggerMiddlewareFactory.php 193 | @@ -0,0 +1,20 @@ 194 | +get("Logger"); 211 | + return new LoggerMiddleware($logger); 212 | + } 213 | +} 214 | diff --git a/items/src/App/src/Service/LoggerFactory.php b/items/src/App/src/Service/LoggerFactory.php 215 | new file mode 100644 216 | index 0000000..cc60ae0 217 | --- /dev/null 218 | +++ b/items/src/App/src/Service/LoggerFactory.php 219 | @@ -0,0 +1,20 @@ 220 | +setFormatter(new JsonFormatter()); 235 | + $logger->pushHandler($handler); 236 | + return $logger; 237 | + } 238 | +} 239 | + 240 | -- 241 | 2.23.0 242 | 243 | -------------------------------------------------------------------------------- /patches/0001-feat-items-tracing-instrumentation-with-b3-and-openc.patch: -------------------------------------------------------------------------------- 1 | From 99bb8bc64da8dda88be47a74dd327ec540358ff9 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Tue, 17 Mar 2020 14:05:48 +0100 4 | Subject: [PATCH] feat(items): tracing instrumentation with b3 and opencensus 5 | 6 | Signed-off-by: Gianluca Arbezzano 7 | --- 8 | items/composer.json | 6 ++- 9 | items/config/autoload/containers.global.php | 2 + 10 | items/config/autoload/local.php | 4 ++ 11 | items/config/pipeline.php | 2 + 12 | .../App/src/Middleware/TracerMiddleware.php | 46 +++++++++++++++++++ 13 | .../Middleware/TracerMiddlewareFactory.php | 19 ++++++++ 14 | items/src/App/src/Service/TracerFactory.php | 45 ++++++++++++++++++ 15 | 7 files changed, 122 insertions(+), 2 deletions(-) 16 | create mode 100644 items/src/App/src/Middleware/TracerMiddleware.php 17 | create mode 100644 items/src/App/src/Middleware/TracerMiddlewareFactory.php 18 | create mode 100644 items/src/App/src/Service/TracerFactory.php 19 | 20 | diff --git a/items/composer.json b/items/composer.json 21 | index c0badf9..dadea8b 100644 22 | --- a/items/composer.json 23 | +++ b/items/composer.json 24 | @@ -18,6 +18,7 @@ 25 | "config": { 26 | "sort-packages": true 27 | }, 28 | + "minimum-stability": "dev", 29 | "extra": { 30 | "zf": { 31 | "component-whitelist": [ 32 | @@ -39,6 +40,8 @@ 33 | "require": { 34 | "php": "^7.1", 35 | "http-interop/http-middleware": "^0.5.0", 36 | + "monolog/monolog": "1.24.0", 37 | + "jcchavezs/zipkin-opentracing": "0.1.4", 38 | "zendframework/zend-component-installer": "^2.1.1", 39 | "zendframework/zend-config-aggregator": "^1.0", 40 | "zendframework/zend-diactoros": "^1.7.1 || ^2.0", 41 | @@ -46,8 +49,7 @@ 42 | "zendframework/zend-expressive-fastroute": "^3.0", 43 | "zendframework/zend-expressive-helpers": "^5.0", 44 | "zendframework/zend-servicemanager": "^3.3", 45 | - "zendframework/zend-stdlib": "^3.1", 46 | - "monolog/monolog": "1.24.0" 47 | + "zendframework/zend-stdlib": "^3.1" 48 | }, 49 | "require-dev": { 50 | "phpunit/phpunit": "^7.0.1", 51 | diff --git a/items/config/autoload/containers.global.php b/items/config/autoload/containers.global.php 52 | index 12a5b18..d36eb04 100644 53 | --- a/items/config/autoload/containers.global.php 54 | +++ b/items/config/autoload/containers.global.php 55 | @@ -21,7 +21,9 @@ return [ 56 | App\Handler\Item::class => App\Handler\ItemFactory::class, 57 | App\Handler\Health::class => App\Handler\HealthFactory::class, 58 | "Logger" => App\Service\LoggerFactory::class, 59 | + "Tracer" => App\Service\TracerFactory::class, 60 | App\Middleware\LoggerMiddleware::class => App\Middleware\LoggerMiddlewareFactory::class, 61 | + App\Middleware\TracerMiddleware::class => App\Middleware\TracerMiddlewareFactory::class, 62 | ], 63 | ], 64 | ]; 65 | diff --git a/items/config/autoload/local.php b/items/config/autoload/local.php 66 | index 824e725..3726cc6 100644 67 | --- a/items/config/autoload/local.php 68 | +++ b/items/config/autoload/local.php 69 | @@ -15,4 +15,8 @@ return [ 70 | "user" => "root", 71 | "pass" => "root", 72 | ], 73 | + "zipkin" => [ 74 | + "serviceName" => 'items', 75 | + "reporterURL" => 'http://jaeger:9411/api/v2/spans', 76 | + ], 77 | ]; 78 | diff --git a/items/config/pipeline.php b/items/config/pipeline.php 79 | index e9287fd..6e050ca 100644 80 | --- a/items/config/pipeline.php 81 | +++ b/items/config/pipeline.php 82 | @@ -15,12 +15,14 @@ use Zend\Expressive\Router\Middleware\MethodNotAllowedMiddleware; 83 | use Zend\Expressive\Router\Middleware\RouteMiddleware; 84 | use Zend\Stratigility\Middleware\ErrorHandler; 85 | use App\Middleware\LoggerMiddleware; 86 | +use App\Middleware\TracerMiddleware; 87 | 88 | /** 89 | * Setup middleware pipeline: 90 | */ 91 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { 92 | $app->pipe($container->get(LoggerMiddleware::class)); 93 | + $app->pipe($container->get(TracerMiddleware::class)); 94 | // The error handler should be the first (most outer) middleware to catch 95 | // all Exceptions. 96 | $app->pipe(ErrorHandler::class); 97 | diff --git a/items/src/App/src/Middleware/TracerMiddleware.php b/items/src/App/src/Middleware/TracerMiddleware.php 98 | new file mode 100644 99 | index 0000000..6da42be 100 | --- /dev/null 101 | +++ b/items/src/App/src/Middleware/TracerMiddleware.php 102 | @@ -0,0 +1,46 @@ 103 | +tracer = $tracer; 122 | + } 123 | + 124 | + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 125 | + { 126 | + $spanContext = $this->tracer->extract( 127 | + Formats\HTTP_HEADERS, 128 | + $request 129 | + ); 130 | + $span = $this->tracer->startSpan($request->getMethod(), [ 131 | + 'child_of' => $spanContext, 132 | + 'tags' => [ 133 | + Tags\HTTP_METHOD => $request->getMethod(), 134 | + 'http.path' => $request->getUri()->getPath(), 135 | + ] 136 | + ]); 137 | + 138 | + try { 139 | + $response = $handler->handle($request); 140 | + $span->setTag(Tags\HTTP_STATUS_CODE, $response->getStatusCode()); 141 | + return $response; 142 | + } catch (\Throwable $e) { 143 | + $span->setTag(Tags\ERROR, $e->getMessage()); 144 | + } finally { 145 | + $span->finish(); 146 | + } 147 | + } 148 | +} 149 | diff --git a/items/src/App/src/Middleware/TracerMiddlewareFactory.php b/items/src/App/src/Middleware/TracerMiddlewareFactory.php 150 | new file mode 100644 151 | index 0000000..fe49d64 152 | --- /dev/null 153 | +++ b/items/src/App/src/Middleware/TracerMiddlewareFactory.php 154 | @@ -0,0 +1,19 @@ 155 | +get("Tracer"); 171 | + return new TracerMiddleware($tracer); 172 | + } 173 | +} 174 | diff --git a/items/src/App/src/Service/TracerFactory.php b/items/src/App/src/Service/TracerFactory.php 175 | new file mode 100644 176 | index 0000000..6795347 177 | --- /dev/null 178 | +++ b/items/src/App/src/Service/TracerFactory.php 179 | @@ -0,0 +1,45 @@ 180 | +get('config')['zipkin'] ?? []; 199 | + if (empty($zipkinConfig)) { 200 | + // If zipkin is not configured then we return an empty tracer. 201 | + return NoopTracer::create(); 202 | + } 203 | + 204 | + $endpoint = Endpoint::create($zipkinConfig['serviceName']); 205 | + $reporter = new HttpReporter(CurlFactory::create(), ["endpoint_url" => $zipkinConfig['reporterURL'] ?? 'http://localhost:9411/api/v2/spans']); 206 | + $sampler = BinarySampler::createAsAlwaysSample(); 207 | + $tracing = TracingBuilder::create() 208 | + ->havingLocalEndpoint($endpoint) 209 | + ->havingSampler($sampler) 210 | + ->havingReporter($reporter) 211 | + ->build(); 212 | + 213 | + $zipkinTracer = new Tracer($tracing); 214 | + 215 | + register_shutdown_function(function () { 216 | + /* Flush the tracer to the backend */ 217 | + $zipkinTracer = GlobalTracer::get(); 218 | + $zipkinTracer->flush(); 219 | + }); 220 | + 221 | + GlobalTracer::set($zipkinTracer); 222 | + return $zipkinTracer; 223 | + } 224 | +} 225 | -- 226 | 2.23.0 227 | 228 | -------------------------------------------------------------------------------- /patches/0001-feat-pay-Added-healthcheck.patch: -------------------------------------------------------------------------------- 1 | From 360c2265f77163da6c30f1aaa662c2d26ee43ff3 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Sat, 23 Mar 2019 15:48:10 +0100 4 | Subject: [PATCH] feat(pay): Added healthcheck 5 | 6 | Signed-off-by: Gianluca Arbezzano 7 | --- 8 | pay/src/main/java/pay/Application.java | 23 ++++++++++++++++- 9 | pay/src/main/java/pay/HealthCheck.java | 31 +++++++++++++++++++++++ 10 | pay/src/main/java/pay/HealthResponse.java | 29 +++++++++++++++++++++ 11 | 3 files changed, 82 insertions(+), 1 deletion(-) 12 | create mode 100644 pay/src/main/java/pay/HealthCheck.java 13 | create mode 100644 pay/src/main/java/pay/HealthResponse.java 14 | 15 | diff --git a/pay/src/main/java/pay/Application.java b/pay/src/main/java/pay/Application.java 16 | index c66e0c0..ef1194a 100644 17 | --- a/pay/src/main/java/pay/Application.java 18 | +++ b/pay/src/main/java/pay/Application.java 19 | @@ -4,11 +4,11 @@ import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.http.ResponseEntity; 22 | import org.springframework.web.bind.annotation.*; 23 | +import javax.servlet.http.HttpServletResponse; 24 | 25 | @SpringBootApplication 26 | @RestController 27 | public class Application { 28 | - 29 | private PayRepository payRepository; 30 | 31 | public Application(PayRepository payRepository) { 32 | @@ -27,6 +27,27 @@ public class Application { 33 | return ResponseEntity.ok("Success"); 34 | } 35 | 36 | + @GetMapping("/health") 37 | + @ResponseBody 38 | + public HealthResponse health(HttpServletResponse response) { 39 | + HealthResponse h = new HealthResponse(); 40 | + String status = "unhealthy"; 41 | + 42 | + HealthCheck mysqlC = new HealthCheck(); 43 | + mysqlC.setName("mysql"); 44 | + try { 45 | + payRepository.count(); 46 | + status = "healthy"; 47 | + mysqlC.setStatus("healthy"); 48 | + } catch (Exception e) { 49 | + mysqlC.setStatus("unhealthy"); 50 | + mysqlC.setError(e.getMessage()); 51 | + response.setStatus(500); 52 | + } 53 | + h.setStatus(status); 54 | + h.addHealthCheck(mysqlC); 55 | + return h; 56 | + } 57 | 58 | public static void main(String[] args) { 59 | SpringApplication.run(Application.class, args); 60 | diff --git a/pay/src/main/java/pay/HealthCheck.java b/pay/src/main/java/pay/HealthCheck.java 61 | new file mode 100644 62 | index 0000000..b3b7723 63 | --- /dev/null 64 | +++ b/pay/src/main/java/pay/HealthCheck.java 65 | @@ -0,0 +1,31 @@ 66 | +package pay; 67 | + 68 | +public class HealthCheck { 69 | + private String status; 70 | + private String name; 71 | + private String error; 72 | + 73 | + public String getStatus() { 74 | + return status; 75 | + } 76 | + 77 | + public void setStatus(String status) { 78 | + this.status = status; 79 | + } 80 | + 81 | + public String getName() { 82 | + return name; 83 | + } 84 | + 85 | + public void setName(String name) { 86 | + this.name = name; 87 | + } 88 | + 89 | + public String getError() { 90 | + return error; 91 | + } 92 | + 93 | + public void setError(String error) { 94 | + this.error = error; 95 | + } 96 | +} 97 | diff --git a/pay/src/main/java/pay/HealthResponse.java b/pay/src/main/java/pay/HealthResponse.java 98 | new file mode 100644 99 | index 0000000..8431f53 100 | --- /dev/null 101 | +++ b/pay/src/main/java/pay/HealthResponse.java 102 | @@ -0,0 +1,29 @@ 103 | +package pay; 104 | + 105 | +import java.util.*; 106 | + 107 | +public class HealthResponse { 108 | + private String status; 109 | + 110 | + private List checks; 111 | + 112 | + public HealthResponse () { 113 | + this.checks = new ArrayList(); 114 | + } 115 | + 116 | + public String getStatus() { 117 | + return status; 118 | + } 119 | + 120 | + public void setStatus(String status) { 121 | + this.status = status; 122 | + } 123 | + 124 | + public void addHealthCheck(HealthCheck h) { 125 | + this.checks.add(h); 126 | + } 127 | + 128 | + public List getChecks() { 129 | + return checks; 130 | + } 131 | +} 132 | -- 133 | 2.23.0 134 | 135 | -------------------------------------------------------------------------------- /patches/0001-feat-pay-add-log4j2.patch: -------------------------------------------------------------------------------- 1 | From 8e21fd8af39ccb5225edf25fe5d03486fd155534 Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Sat, 23 Mar 2019 16:09:13 +0100 4 | Subject: [PATCH] feat(pay): add log4j2 5 | 6 | Co-Authored-by: Walter Dal Mut 7 | Signed-off-by: Gianluca Arbezzano 8 | --- 9 | pay/build.gradle | 11 +++++- 10 | pay/src/main/java/pay/AppConfig.java | 14 +++++++ 11 | pay/src/main/java/pay/Application.java | 4 ++ 12 | pay/src/main/java/pay/LoggerInterceptor.java | 39 ++++++++++++++++++++ 13 | pay/src/main/resources/log4j2.xml | 16 ++++++++ 14 | 5 files changed, 83 insertions(+), 1 deletion(-) 15 | create mode 100644 pay/src/main/java/pay/AppConfig.java 16 | create mode 100644 pay/src/main/java/pay/LoggerInterceptor.java 17 | create mode 100644 pay/src/main/resources/log4j2.xml 18 | 19 | diff --git a/pay/build.gradle b/pay/build.gradle 20 | index a8e253c..50bb905 100644 21 | --- a/pay/build.gradle 22 | +++ b/pay/build.gradle 23 | @@ -26,9 +26,18 @@ sourceCompatibility = 1.8 24 | targetCompatibility = 1.8 25 | 26 | dependencies { 27 | - compile("org.springframework.boot:spring-boot-starter-web") 28 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 29 | //compile("com.h2database:h2") 30 | compile 'mysql:mysql-connector-java' 31 | + compile("com.fasterxml.jackson.core:jackson-databind") 32 | + compile("org.springframework.boot:spring-boot-starter-web"){ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'} 33 | + compile('org.springframework.boot:spring-boot-starter-log4j2') 34 | testCompile('org.springframework.boot:spring-boot-starter-test') 35 | } 36 | + 37 | + 38 | +configurations { 39 | + all { 40 | + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' 41 | + } 42 | +} 43 | diff --git a/pay/src/main/java/pay/AppConfig.java b/pay/src/main/java/pay/AppConfig.java 44 | new file mode 100644 45 | index 0000000..bb788cb 46 | --- /dev/null 47 | +++ b/pay/src/main/java/pay/AppConfig.java 48 | @@ -0,0 +1,14 @@ 49 | +package pay; 50 | + 51 | +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 52 | +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 53 | +import org.springframework.stereotype.Component; 54 | + 55 | +@Component 56 | +public class AppConfig extends WebMvcConfigurerAdapter { 57 | + 58 | + @Override 59 | + public void addInterceptors(InterceptorRegistry registry) { 60 | + registry.addInterceptor(new LoggerInterceptor()); 61 | + } 62 | +} 63 | diff --git a/pay/src/main/java/pay/Application.java b/pay/src/main/java/pay/Application.java 64 | index ef1194a..1d8d39d 100644 65 | --- a/pay/src/main/java/pay/Application.java 66 | +++ b/pay/src/main/java/pay/Application.java 67 | @@ -5,10 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; 68 | import org.springframework.http.ResponseEntity; 69 | import org.springframework.web.bind.annotation.*; 70 | import javax.servlet.http.HttpServletResponse; 71 | +import org.slf4j.Logger; 72 | +import org.slf4j.LoggerFactory; 73 | 74 | @SpringBootApplication 75 | @RestController 76 | public class Application { 77 | + private static final Logger logger = LoggerFactory.getLogger(Application.class); 78 | private PayRepository payRepository; 79 | 80 | public Application(PayRepository payRepository) { 81 | @@ -40,6 +43,7 @@ public class Application { 82 | status = "healthy"; 83 | mysqlC.setStatus("healthy"); 84 | } catch (Exception e) { 85 | + logger.error("Mysql healthcheck failed", e.getMessage()); 86 | mysqlC.setStatus("unhealthy"); 87 | mysqlC.setError(e.getMessage()); 88 | response.setStatus(500); 89 | diff --git a/pay/src/main/java/pay/LoggerInterceptor.java b/pay/src/main/java/pay/LoggerInterceptor.java 90 | new file mode 100644 91 | index 0000000..654229f 92 | --- /dev/null 93 | +++ b/pay/src/main/java/pay/LoggerInterceptor.java 94 | @@ -0,0 +1,39 @@ 95 | +package pay; 96 | + 97 | +import org.springframework.stereotype.Component; 98 | +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 99 | +import javax.servlet.http.HttpServletRequest; 100 | +import javax.servlet.http.HttpServletResponse; 101 | +import org.slf4j.Logger; 102 | +import org.slf4j.LoggerFactory; 103 | + 104 | +@Component 105 | +public class LoggerInterceptor 106 | + extends HandlerInterceptorAdapter { 107 | + private static final Logger logger = LoggerFactory.getLogger(Application.class); 108 | + 109 | + @Override 110 | + public boolean preHandle( 111 | + HttpServletRequest request, 112 | + HttpServletResponse response, 113 | + Object handler) { 114 | + long startTime = System.currentTimeMillis(); 115 | + logger.info("[Start HTTP Request]: Path" + request.getRequestURL().toString() 116 | + + " StartTime=" + startTime); 117 | + request.setAttribute("startTime", startTime); 118 | + 119 | + return true; 120 | + } 121 | + 122 | + @Override 123 | + public void afterCompletion( 124 | + HttpServletRequest request, 125 | + HttpServletResponse response, 126 | + Object handler, 127 | + Exception ex) { 128 | + long startTime = (Long) request.getAttribute("startTime"); 129 | + logger.info("[End HTTP Request]: Path" + request.getRequestURL().toString() 130 | + + " EndTime=" + System.currentTimeMillis() 131 | + + " TimeTaken="+ (System.currentTimeMillis() - startTime)); 132 | + } 133 | +} 134 | diff --git a/pay/src/main/resources/log4j2.xml b/pay/src/main/resources/log4j2.xml 135 | new file mode 100644 136 | index 0000000..7403409 137 | --- /dev/null 138 | +++ b/pay/src/main/resources/log4j2.xml 139 | @@ -0,0 +1,16 @@ 140 | + 141 | + 142 | + 143 | + 144 | + 145 | + 146 | + 147 | + 148 | + 149 | + 150 | + 151 | + 152 | + 153 | + 154 | + 155 | + 156 | -- 157 | 2.23.0 158 | 159 | -------------------------------------------------------------------------------- /patches/0001-fix-pay-Trace-with-B3-and-opentelemetry.patch: -------------------------------------------------------------------------------- 1 | From 7917579a9541b1ef207e950f697165e622b55fee Mon Sep 17 00:00:00 2001 2 | From: Gianluca Arbezzano 3 | Date: Sun, 15 Mar 2020 14:34:41 +0100 4 | Subject: [PATCH] fix(pay): Trace with B3 and opentelemetry 5 | 6 | Signed-off-by: Gianluca Arbezzano 7 | --- 8 | pay/.gitignore | 10 ++ 9 | pay/build.gradle | 6 + 10 | pay/gradlew | 172 ------------------- 11 | pay/src/main/java/pay/AppConfig.java | 1 + 12 | pay/src/main/java/pay/Application.java | 22 +++ 13 | pay/src/main/java/pay/TracerInterceptor.java | 63 +++++++ 14 | 6 files changed, 102 insertions(+), 172 deletions(-) 15 | create mode 100644 pay/.gitignore 16 | delete mode 100755 pay/gradlew 17 | create mode 100644 pay/src/main/java/pay/TracerInterceptor.java 18 | 19 | diff --git a/pay/.gitignore b/pay/.gitignore 20 | new file mode 100644 21 | index 0000000..70463c2 22 | --- /dev/null 23 | +++ b/pay/.gitignore 24 | @@ -0,0 +1,10 @@ 25 | +.gradle 26 | +build 27 | +.settings 28 | +.idea 29 | +.project 30 | +gradle/wrapper/gradle-wrapper.jar 31 | +gradle/wrapper/gradle-wrapper.properties 32 | +gradlew 33 | +gradlew.bat 34 | + 35 | diff --git a/pay/build.gradle b/pay/build.gradle 36 | index 50bb905..422f0ed 100644 37 | --- a/pay/build.gradle 38 | +++ b/pay/build.gradle 39 | @@ -33,6 +33,12 @@ dependencies { 40 | compile("org.springframework.boot:spring-boot-starter-web"){ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'} 41 | compile('org.springframework.boot:spring-boot-starter-log4j2') 42 | testCompile('org.springframework.boot:spring-boot-starter-test') 43 | + compile('io.opentelemetry:opentelemetry-api:0.2.4') 44 | + compile('io.opentelemetry:opentelemetry-sdk:0.2.4') 45 | + compile('io.opentelemetry:opentelemetry-exporters-jaeger:0.2.4') 46 | + compile('io.opentelemetry:opentelemetry-exporters-logging:0.2.4') 47 | + compile('io.grpc:grpc-protobuf:1.24.0') 48 | + compile('io.grpc:grpc-netty-shaded:1.24.0') 49 | } 50 | 51 | 52 | diff --git a/pay/gradlew b/pay/gradlew 53 | deleted file mode 100755 54 | index cccdd3d..0000000 55 | --- a/pay/gradlew 56 | +++ /dev/null 57 | @@ -1,172 +0,0 @@ 58 | -#!/usr/bin/env sh 59 | - 60 | -############################################################################## 61 | -## 62 | -## Gradle start up script for UN*X 63 | -## 64 | -############################################################################## 65 | - 66 | -# Attempt to set APP_HOME 67 | -# Resolve links: $0 may be a link 68 | -PRG="$0" 69 | -# Need this for relative symlinks. 70 | -while [ -h "$PRG" ] ; do 71 | - ls=`ls -ld "$PRG"` 72 | - link=`expr "$ls" : '.*-> \(.*\)$'` 73 | - if expr "$link" : '/.*' > /dev/null; then 74 | - PRG="$link" 75 | - else 76 | - PRG=`dirname "$PRG"`"/$link" 77 | - fi 78 | -done 79 | -SAVED="`pwd`" 80 | -cd "`dirname \"$PRG\"`/" >/dev/null 81 | -APP_HOME="`pwd -P`" 82 | -cd "$SAVED" >/dev/null 83 | - 84 | -APP_NAME="Gradle" 85 | -APP_BASE_NAME=`basename "$0"` 86 | - 87 | -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 88 | -DEFAULT_JVM_OPTS="" 89 | - 90 | -# Use the maximum available, or set MAX_FD != -1 to use that value. 91 | -MAX_FD="maximum" 92 | - 93 | -warn () { 94 | - echo "$*" 95 | -} 96 | - 97 | -die () { 98 | - echo 99 | - echo "$*" 100 | - echo 101 | - exit 1 102 | -} 103 | - 104 | -# OS specific support (must be 'true' or 'false'). 105 | -cygwin=false 106 | -msys=false 107 | -darwin=false 108 | -nonstop=false 109 | -case "`uname`" in 110 | - CYGWIN* ) 111 | - cygwin=true 112 | - ;; 113 | - Darwin* ) 114 | - darwin=true 115 | - ;; 116 | - MINGW* ) 117 | - msys=true 118 | - ;; 119 | - NONSTOP* ) 120 | - nonstop=true 121 | - ;; 122 | -esac 123 | - 124 | -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 125 | - 126 | -# Determine the Java command to use to start the JVM. 127 | -if [ -n "$JAVA_HOME" ] ; then 128 | - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 129 | - # IBM's JDK on AIX uses strange locations for the executables 130 | - JAVACMD="$JAVA_HOME/jre/sh/java" 131 | - else 132 | - JAVACMD="$JAVA_HOME/bin/java" 133 | - fi 134 | - if [ ! -x "$JAVACMD" ] ; then 135 | - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 136 | - 137 | -Please set the JAVA_HOME variable in your environment to match the 138 | -location of your Java installation." 139 | - fi 140 | -else 141 | - JAVACMD="java" 142 | - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 143 | - 144 | -Please set the JAVA_HOME variable in your environment to match the 145 | -location of your Java installation." 146 | -fi 147 | - 148 | -# Increase the maximum file descriptors if we can. 149 | -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 150 | - MAX_FD_LIMIT=`ulimit -H -n` 151 | - if [ $? -eq 0 ] ; then 152 | - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 153 | - MAX_FD="$MAX_FD_LIMIT" 154 | - fi 155 | - ulimit -n $MAX_FD 156 | - if [ $? -ne 0 ] ; then 157 | - warn "Could not set maximum file descriptor limit: $MAX_FD" 158 | - fi 159 | - else 160 | - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 161 | - fi 162 | -fi 163 | - 164 | -# For Darwin, add options to specify how the application appears in the dock 165 | -if $darwin; then 166 | - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 167 | -fi 168 | - 169 | -# For Cygwin, switch paths to Windows format before running java 170 | -if $cygwin ; then 171 | - APP_HOME=`cygpath --path --mixed "$APP_HOME"` 172 | - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 173 | - JAVACMD=`cygpath --unix "$JAVACMD"` 174 | - 175 | - # We build the pattern for arguments to be converted via cygpath 176 | - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 177 | - SEP="" 178 | - for dir in $ROOTDIRSRAW ; do 179 | - ROOTDIRS="$ROOTDIRS$SEP$dir" 180 | - SEP="|" 181 | - done 182 | - OURCYGPATTERN="(^($ROOTDIRS))" 183 | - # Add a user-defined pattern to the cygpath arguments 184 | - if [ "$GRADLE_CYGPATTERN" != "" ] ; then 185 | - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 186 | - fi 187 | - # Now convert the arguments - kludge to limit ourselves to /bin/sh 188 | - i=0 189 | - for arg in "$@" ; do 190 | - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 191 | - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 192 | - 193 | - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 194 | - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 195 | - else 196 | - eval `echo args$i`="\"$arg\"" 197 | - fi 198 | - i=$((i+1)) 199 | - done 200 | - case $i in 201 | - (0) set -- ;; 202 | - (1) set -- "$args0" ;; 203 | - (2) set -- "$args0" "$args1" ;; 204 | - (3) set -- "$args0" "$args1" "$args2" ;; 205 | - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 206 | - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 207 | - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 208 | - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 209 | - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 210 | - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 211 | - esac 212 | -fi 213 | - 214 | -# Escape application args 215 | -save () { 216 | - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 217 | - echo " " 218 | -} 219 | -APP_ARGS=$(save "$@") 220 | - 221 | -# Collect all arguments for the java command, following the shell quoting and substitution rules 222 | -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 223 | - 224 | -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 225 | -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 226 | - cd "$(dirname "$0")" 227 | -fi 228 | - 229 | -exec "$JAVACMD" "$@" 230 | diff --git a/pay/src/main/java/pay/AppConfig.java b/pay/src/main/java/pay/AppConfig.java 231 | index bb788cb..d6e780a 100644 232 | --- a/pay/src/main/java/pay/AppConfig.java 233 | +++ b/pay/src/main/java/pay/AppConfig.java 234 | @@ -10,5 +10,6 @@ public class AppConfig extends WebMvcConfigurerAdapter { 235 | @Override 236 | public void addInterceptors(InterceptorRegistry registry) { 237 | registry.addInterceptor(new LoggerInterceptor()); 238 | + registry.addInterceptor(new TracerInterceptor()); 239 | } 240 | } 241 | diff --git a/pay/src/main/java/pay/Application.java b/pay/src/main/java/pay/Application.java 242 | index 1d8d39d..201fd73 100644 243 | --- a/pay/src/main/java/pay/Application.java 244 | +++ b/pay/src/main/java/pay/Application.java 245 | @@ -1,5 +1,11 @@ 246 | package pay; 247 | 248 | +import io.grpc.ManagedChannel; 249 | +import io.grpc.ManagedChannelBuilder; 250 | +import io.opentelemetry.exporters.jaeger.JaegerGrpcSpanExporter; 251 | +import io.opentelemetry.exporters.logging.LoggingSpanExporter; 252 | +import io.opentelemetry.sdk.OpenTelemetrySdk; 253 | +import io.opentelemetry.sdk.trace.export.SimpleSpansProcessor; 254 | import org.springframework.boot.SpringApplication; 255 | import org.springframework.boot.autoconfigure.SpringBootApplication; 256 | import org.springframework.http.ResponseEntity; 257 | @@ -54,7 +60,23 @@ public class Application { 258 | } 259 | 260 | public static void main(String[] args) { 261 | + // Create a channel towards Jaeger end point 262 | + ManagedChannel jaegerChannel = ManagedChannelBuilder.forAddress("jaeger", 14250).usePlaintext().build(); 263 | + // Export traces to Jaeger 264 | + 265 | + JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.newBuilder() 266 | + .setServiceName("pay") 267 | + .setChannel(jaegerChannel) 268 | + .setDeadlineMs(30000) 269 | + .build(); 270 | + // Export also to the console 271 | + LoggingSpanExporter loggingExporter = new LoggingSpanExporter(); 272 | + OpenTelemetrySdk.getTracerProvider().addSpanProcessor(SimpleSpansProcessor.newBuilder(loggingExporter).build()); 273 | + // Set to process the spans by the Jaeger Exporter 274 | + OpenTelemetrySdk.getTracerProvider() 275 | + .addSpanProcessor(SimpleSpansProcessor.newBuilder(jaegerExporter).build()); 276 | SpringApplication.run(Application.class, args); 277 | } 278 | 279 | } 280 | + 281 | diff --git a/pay/src/main/java/pay/TracerInterceptor.java b/pay/src/main/java/pay/TracerInterceptor.java 282 | new file mode 100644 283 | index 0000000..a37a508 284 | --- /dev/null 285 | +++ b/pay/src/main/java/pay/TracerInterceptor.java 286 | @@ -0,0 +1,63 @@ 287 | +package pay; 288 | + 289 | +import com.sun.net.httpserver.HttpExchange; 290 | +import io.opentelemetry.OpenTelemetry; 291 | +import io.opentelemetry.context.propagation.HttpTextFormat; 292 | +import io.opentelemetry.trace.Span; 293 | +import io.opentelemetry.trace.SpanContext; 294 | +import io.opentelemetry.trace.Tracer; 295 | +import io.opentelemetry.trace.propagation.B3Propagator; 296 | +import org.springframework.stereotype.Component; 297 | +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 298 | + 299 | +import javax.servlet.http.HttpServletRequest; 300 | +import javax.servlet.http.HttpServletResponse; 301 | + 302 | +import java.io.IOException; 303 | +import java.net.URL; 304 | + 305 | +@Component 306 | +public class TracerInterceptor 307 | + extends HandlerInterceptorAdapter { 308 | + // OTel API 309 | + private Tracer tracer = 310 | + OpenTelemetry.getTracerProvider().get("io.opentelemetry.pay.JaegerExample"); 311 | + 312 | + // false -> we expect multi header 313 | + B3Propagator b3Propagator = new B3Propagator(false); 314 | + 315 | + B3Propagator.Getter getter = new B3Propagator.Getter() { 316 | + @javax.annotation.Nullable 317 | + @Override 318 | + public String get(HttpServletRequest carrier, String key) { 319 | + return carrier.getHeader(key); 320 | + } 321 | + }; 322 | + private Span span; 323 | + 324 | + @Override 325 | + public boolean preHandle( 326 | + HttpServletRequest request, 327 | + HttpServletResponse response, 328 | + Object handler) throws IOException { 329 | + URL url = new URL(request.getRequestURL().toString()); 330 | + SpanContext remoteCtx = b3Propagator.extract(request, getter); 331 | + Span.Builder spanBuilder = tracer.spanBuilder(String.format("[%s] %d:%s", request.getMethod(), url.getPort(), url.getPath())).setSpanKind(Span.Kind.SERVER); 332 | + if(remoteCtx != null){ 333 | + spanBuilder.setParent(remoteCtx); 334 | + } 335 | + span = spanBuilder.startSpan(); 336 | + span.setAttribute("http.method", request.getMethod()); 337 | + span.setAttribute("http.url", url.toString()); 338 | + return true; 339 | + } 340 | + @Override 341 | + public void afterCompletion( 342 | + HttpServletRequest request, 343 | + HttpServletResponse response, 344 | + Object handler, 345 | + Exception ex) { 346 | + span.setAttribute("http.status_code", response.getStatus()); 347 | + span.end(); 348 | + } 349 | +} 350 | -- 351 | 2.23.0 352 | 353 | --------------------------------------------------------------------------------