├── .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 | 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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------