├── ui
├── VERSION
├── docker_build.sh
├── Dockerfile
├── Gemfile
├── views
│ ├── create.haml
│ ├── index.haml
│ ├── layout.haml
│ └── show.haml
├── config.ru
├── middleware.rb
├── Gemfile.lock
├── helpers.rb
└── ui_app.rb
├── comment
├── VERSION
├── Gemfile
├── docker_build.sh
├── Dockerfile
├── config.ru
├── Gemfile.lock
├── helpers.rb
└── comment_app.rb
├── post-py
├── VERSION
├── requirements.txt
├── docker_build.sh
├── Dockerfile
├── helpers.py
└── post_app.py
├── README.md
└── .gitignore
/ui/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.1
2 |
--------------------------------------------------------------------------------
/comment/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.3
2 |
--------------------------------------------------------------------------------
/post-py/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.2
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Micorservices
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | docker-compose.yml
2 | build_info.txt
3 | prometheus/
4 | alertmanager/
5 |
6 | ## python specific
7 | *.pyc
8 |
--------------------------------------------------------------------------------
/post-py/requirements.txt:
--------------------------------------------------------------------------------
1 | prometheus_client==0.0.21
2 | flask==0.12.2
3 | pymongo==3.5.1
4 | structlog==17.2.0
5 | py-zipkin==0.7.0
6 | requests==2.18.4
7 |
--------------------------------------------------------------------------------
/comment/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'sinatra'
4 | gem 'bson_ext'
5 | gem 'mongo'
6 | gem 'puma'
7 | gem 'prometheus-client'
8 | gem 'rack'
9 | gem 'rufus-scheduler'
10 |
--------------------------------------------------------------------------------
/ui/docker_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo `git show --format="%h" HEAD | head -1` > build_info.txt
4 | echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
5 |
6 | docker build -t $USER_NAME/ui .
7 |
--------------------------------------------------------------------------------
/comment/docker_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo `git show --format="%h" HEAD | head -1` > build_info.txt
4 | echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
5 |
6 | docker build -t $USER_NAME/comment .
7 |
--------------------------------------------------------------------------------
/post-py/docker_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo `git show --format="%h" HEAD | head -1` > build_info.txt
4 | echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
5 |
6 | docker build -t $USER_NAME/post .
7 |
--------------------------------------------------------------------------------
/post-py/Dockerfile:
--------------------------------------------------------------------------------
1 | # FROM python:3.6.0-alpine
2 | FROM python:2.7
3 | WORKDIR /app
4 | ADD requirements.txt /app
5 | RUN pip install -r requirements.txt
6 | ADD . /app
7 | EXPOSE 5000
8 | ENTRYPOINT ["python", "post_app.py"]
9 |
--------------------------------------------------------------------------------
/ui/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:2.3.3
2 |
3 | RUN apt-get update -qq && apt-get install -y build-essential
4 |
5 | ENV APP_HOME /app
6 | RUN mkdir $APP_HOME
7 | WORKDIR $APP_HOME
8 |
9 | ADD Gemfile* $APP_HOME/
10 | RUN bundle install
11 |
12 | ADD . $APP_HOME
13 | CMD ["puma"]
14 |
--------------------------------------------------------------------------------
/ui/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'sinatra'
4 | gem 'sinatra-contrib'
5 | gem 'haml'
6 | gem 'bson_ext'
7 | gem 'faraday'
8 | gem 'puma'
9 | gem 'prometheus-client'
10 | gem 'rack'
11 | gem 'rufus-scheduler'
12 | gem 'tzinfo-data'
13 | gem 'zipkin-tracer'
14 |
--------------------------------------------------------------------------------
/comment/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:2.2
2 |
3 | RUN apt-get update -qq && apt-get install -y build-essential
4 |
5 | ENV APP_HOME /app
6 | RUN mkdir $APP_HOME
7 | WORKDIR $APP_HOME
8 |
9 | ADD Gemfile* $APP_HOME/
10 | RUN bundle install
11 |
12 | ADD . $APP_HOME
13 |
14 | CMD ["puma"]
15 |
--------------------------------------------------------------------------------
/comment/config.ru:
--------------------------------------------------------------------------------
1 | require './comment_app'
2 | require 'rack'
3 | require 'prometheus/middleware/collector'
4 | require 'prometheus/middleware/exporter'
5 |
6 | use Rack::Deflater, if: ->(_, _, _, body) { body.any? && body[0].length > 512 }
7 | use Prometheus::Middleware::Collector
8 | use Prometheus::Middleware::Exporter
9 |
10 | run Sinatra::Application
11 |
--------------------------------------------------------------------------------
/ui/views/create.haml:
--------------------------------------------------------------------------------
1 | %h2 Add blog post
2 | %form{ action: '/new', method: 'post', role: 'form'}
3 | .form-group
4 | %label{for: 'title'} Title:
5 | %input{name:'title', placeholder: 'cool title', class: 'form-control', id: 'title'}
6 | .form-group
7 | %label{for: 'link'} Link:
8 | %input{name:'link', placeholder: 'awesome link', class: 'form-control', id: 'link'}
9 | .form-group
10 | %input{class: 'btn btn-primary', type: 'submit', value:'Post it!'}
11 |
--------------------------------------------------------------------------------
/ui/config.ru:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'rack'
3 | require 'prometheus/middleware/collector'
4 | require 'prometheus/middleware/exporter'
5 | require_relative 'ui_app'
6 | require_relative 'middleware'
7 | require 'zipkin-tracer'
8 |
9 | # https://github.com/openzipkin/zipkin-ruby#sending-traces-on-incoming-requests
10 | zipkin_config = {
11 | service_name: 'ui_app',
12 | service_port: 9292,
13 | sample_rate: 1,
14 | sampled_as_boolean: false,
15 | log_tracing: true,
16 | json_api_host: 'http://zipkin:9411/api/v1/spans'
17 | }
18 |
19 | use ZipkinTracer::RackHandler, zipkin_config
20 |
21 | use Metrics
22 | use Rack::Deflater, if: ->(_, _, _, body) { body.any? && body[0].length > 512 }
23 | use Prometheus::Middleware::Collector
24 | use Prometheus::Middleware::Exporter
25 |
26 | run Sinatra::Application
27 |
--------------------------------------------------------------------------------
/comment/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | bson (4.2.2)
5 | bson_ext (1.5.1)
6 | et-orbi (1.0.8)
7 | tzinfo
8 | mongo (2.4.3)
9 | bson (>= 4.2.1, < 5.0.0)
10 | mustermann (1.0.0)
11 | prometheus-client (0.7.1)
12 | quantile (~> 0.2.0)
13 | puma (3.10.0)
14 | quantile (0.2.0)
15 | rack (2.0.3)
16 | rack-protection (2.0.0)
17 | rack
18 | rufus-scheduler (3.4.2)
19 | et-orbi (~> 1.0)
20 | sinatra (2.0.0)
21 | mustermann (~> 1.0)
22 | rack (~> 2.0)
23 | rack-protection (= 2.0.0)
24 | tilt (~> 2.0)
25 | thread_safe (0.3.6)
26 | tilt (2.0.8)
27 | tzinfo (1.2.3)
28 | thread_safe (~> 0.1)
29 |
30 | PLATFORMS
31 | ruby
32 |
33 | DEPENDENCIES
34 | bson_ext
35 | mongo
36 | prometheus-client
37 | puma
38 | rack
39 | rufus-scheduler
40 | sinatra
41 |
42 | BUNDLED WITH
43 | 1.15.4
44 |
--------------------------------------------------------------------------------
/ui/middleware.rb:
--------------------------------------------------------------------------------
1 | require 'prometheus/client'
2 |
3 | class Metrics
4 | def initialize(app)
5 | @app = app
6 | prometheus = Prometheus::Client.registry
7 | @request_count = Prometheus::Client::Counter.new(
8 | :ui_request_count,
9 | 'App Request Count'
10 | )
11 | @request_response_time = Prometheus::Client::Histogram.new(
12 | :ui_request_response_time,
13 | 'Request response time'
14 | )
15 | prometheus.register(@request_response_time)
16 | prometheus.register(@request_count)
17 | end
18 |
19 | def call(env)
20 | request_started_on = Time.now
21 | env['REQUEST_ID'] = SecureRandom.uuid # add unique ID to each request
22 | @status, @headers, @response = @app.call(env)
23 | request_ended_on = Time.now
24 | # prometheus metrics
25 | @request_response_time.observe({ path: env['REQUEST_PATH'] },
26 | request_ended_on - request_started_on)
27 | @request_count.increment(method: env['REQUEST_METHOD'],
28 | path: env['REQUEST_PATH'],
29 | http_status: @status)
30 | [@status, @headers, @response]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/post-py/helpers.py:
--------------------------------------------------------------------------------
1 | import structlog
2 | from pymongo import MongoClient
3 | from pymongo.errors import ConnectionFailure
4 | from json import dumps
5 | from flask import request
6 |
7 |
8 | log = structlog.get_logger()
9 |
10 |
11 | def http_healthcheck_handler(mongo_host, mongo_port, version):
12 | postdb = MongoClient(mongo_host, int(mongo_port),
13 | serverSelectionTimeoutMS=2000)
14 | try:
15 | postdb.admin.command('ismaster')
16 | except ConnectionFailure:
17 | postdb_status = 0
18 | else:
19 | postdb_status = 1
20 |
21 | status = postdb_status
22 | healthcheck = {
23 | 'status': status,
24 | 'dependent_services': {
25 | 'postdb': postdb_status
26 | },
27 | 'version': version
28 | }
29 | return dumps(healthcheck)
30 |
31 |
32 | def log_event(event_type, name, message, params={}):
33 | request_id = request.headers['Request-Id']
34 | if event_type == 'info':
35 | log.info(name, service='post', request_id=request_id,
36 | message=message, params=params)
37 | elif event_type == 'error':
38 | log.error(name, service='post', request_id=request_id,
39 | message=message, params=params)
40 |
--------------------------------------------------------------------------------
/ui/views/index.haml:
--------------------------------------------------------------------------------
1 | - unless @posts.nil? or @posts.empty?
2 | - @posts.each do |post|
3 | %div{ id: 'postlist'}
4 | %div{class: 'panel'}
5 | %div{class: 'panel-heading'}
6 | %div{class: 'text-center'}
7 | %div{class: 'row'}
8 | .col-sm-1
9 | %form{:action => "/post/#{post['_id']['$oid']}/vote/1", :method => "post", id: "form-upvote" }
10 | %input{:type => "hidden", :name => "_method", :value => "post"}
11 | %button{type: "submit", class: "btn btn-default btn-sm"}
12 | %span{ class: "glyphicon glyphicon-menu-up" }
13 | %h4{class: 'pull-center'} #{post['votes']}
14 | %form{:action => "/post/#{post['_id']['$oid']}/vote/-1", :method => "post", id: "form-downvote"}
15 | %input{:type => "hidden", :name => "_method", :value=> "post"}
16 | %button{type: "submit", class: "btn btn-default btn-sm"}
17 | %span{ class: "glyphicon glyphicon-menu-down" }
18 | .col-sm-8
19 | %h3{class: 'pull-left'}
20 | %a{href: "/post/#{post['_id']['$oid']}"} #{post['title']}
21 | .col-sm-3
22 | %h4{class: 'pull-right'}
23 | %small
24 | %em #{Time.at(post['created_at'].to_i).strftime('%d-%m-%Y')}
25 | %br #{Time.at(post['created_at'].to_i).strftime('%H:%M')}
26 | .panel-footer
27 | %a{href: "#{post['link']}", class: 'btn btn-link'} Go to the link
28 |
--------------------------------------------------------------------------------
/ui/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | backports (3.8.0)
5 | bson (1.12.5)
6 | bson_ext (1.12.5)
7 | bson (~> 1.12.5)
8 | concurrent-ruby (1.0.5)
9 | et-orbi (1.0.8)
10 | tzinfo
11 | faraday (0.13.1)
12 | multipart-post (>= 1.2, < 3)
13 | finagle-thrift (1.4.2)
14 | thrift (~> 0.9.3)
15 | haml (5.0.2)
16 | temple (>= 0.8.0)
17 | tilt
18 | multi_json (1.12.1)
19 | multipart-post (2.0.0)
20 | mustermann (1.0.0)
21 | prometheus-client (0.7.1)
22 | quantile (~> 0.2.0)
23 | puma (3.10.0)
24 | quantile (0.2.0)
25 | rack (2.0.3)
26 | rack-protection (2.0.0)
27 | rack
28 | rufus-scheduler (3.4.2)
29 | et-orbi (~> 1.0)
30 | sinatra (2.0.0)
31 | mustermann (~> 1.0)
32 | rack (~> 2.0)
33 | rack-protection (= 2.0.0)
34 | tilt (~> 2.0)
35 | sinatra-contrib (2.0.0)
36 | backports (>= 2.0)
37 | multi_json
38 | mustermann (~> 1.0)
39 | rack-protection (= 2.0.0)
40 | sinatra (= 2.0.0)
41 | tilt (>= 1.3, < 3)
42 | sucker_punch (2.0.4)
43 | concurrent-ruby (~> 1.0.0)
44 | temple (0.8.0)
45 | thread_safe (0.3.6)
46 | thrift (0.9.3.0)
47 | tilt (2.0.8)
48 | tzinfo (1.2.3)
49 | thread_safe (~> 0.1)
50 | tzinfo-data (1.2017.3)
51 | tzinfo (>= 1.0.0)
52 | zipkin-tracer (0.27.1)
53 | faraday (~> 0.8)
54 | finagle-thrift (~> 1.4.2)
55 | rack (>= 1.0)
56 | sucker_punch (~> 2.0)
57 |
58 | PLATFORMS
59 | ruby
60 |
61 | DEPENDENCIES
62 | bson_ext
63 | faraday
64 | haml
65 | prometheus-client
66 | puma
67 | rack
68 | rufus-scheduler
69 | sinatra
70 | sinatra-contrib
71 | tzinfo-data
72 | zipkin-tracer
73 |
74 | BUNDLED WITH
75 | 1.14.6
76 |
--------------------------------------------------------------------------------
/ui/views/layout.haml:
--------------------------------------------------------------------------------
1 | !!! 5
2 | %html(lang="en")
3 | %head
4 | %meta(charset="utf-8")
5 | %meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1")
6 | %meta(name="viewport" content="width=device-width, initial-scale=1.0")
7 | %title="Microservices Reddit :: #{@title}"
8 | %link{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', integrity: "sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7", type: 'text/css', rel: 'stylesheet', crossorigin: 'anonymous' }
9 | %link{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css', integrity: 'sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r', type: 'text/css', rel: 'stylesheet', crossorigin: 'anonymous' }
10 | %script{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js', integrity: 'sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS', crossorigin: 'anonymous'}
11 | %body
12 | .navbar.navbar-default.navbar-static-top
13 | .container
14 | %button.navbar-toggle(type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse")
15 | %span.icon-bar
16 | %span.icon-bar
17 | %span.icon-bar
18 | %a.navbar-brand(href="/") Microservices Reddit
19 | .navbar-collapse.collapse.navbar-responsive-collapse
20 | .container
21 | .row
22 | .col-lg-9
23 | - unless @flashes.nil? or @flashes.empty?
24 | - @flashes.each do |flash|
25 | .alert{class: flash[:type]}
26 | %strong #{flash[:message]}
27 | = yield
28 | .col-lg-3
29 | .well.sidebar-nav
30 | %h3 Menu
31 | %ul.nav.nav-list
32 | %li
33 | %a(href="/") All posts
34 | %li
35 | %a(href="/new") New post
36 |
--------------------------------------------------------------------------------
/comment/helpers.rb:
--------------------------------------------------------------------------------
1 | def obj_id(val)
2 | begin
3 | BSON::ObjectId.from_string(val)
4 | rescue BSON::ObjectId::Invalid
5 | nil
6 | end
7 | end
8 |
9 | def document_by_id(id)
10 | id = obj_id(id) if String === id
11 | if id.nil?
12 | {}.to_json
13 | else
14 | document = settings.mongo_db.find(_id: id).to_a.first
15 | (document || {}).to_json
16 | end
17 | end
18 |
19 | def healthcheck_handler(db_url, version)
20 | begin
21 | commentdb_test = Mongo::Client.new(db_url,
22 | server_selection_timeout: 2)
23 | commentdb_test.database_names
24 | commentdb_test.close
25 | rescue StandardError
26 | commentdb_status = 0
27 | else
28 | commentdb_status = 1
29 | end
30 |
31 | status = commentdb_status
32 | healthcheck = {
33 | status: status,
34 | dependent_services: {
35 | commentdb: commentdb_status
36 | },
37 | version: version
38 | }
39 |
40 | healthcheck.to_json
41 | end
42 |
43 | def set_health_gauge(metric, value)
44 | metric.set(
45 | {
46 | version: VERSION,
47 | commit_hash: BUILD_INFO[0].strip,
48 | branch: BUILD_INFO[1].strip
49 | },
50 | value
51 | )
52 | end
53 |
54 | def log_event(type, name, message, params = '{}')
55 | case type
56 | when 'error'
57 | logger.error('service=comment | ' \
58 | "event=#{name} | " \
59 | "request_id=#{request.env['HTTP_REQUEST_ID']}\n" \
60 | "message=\'#{message}\'\n" \
61 | "params: #{params}")
62 | when 'info'
63 | logger.info('service=comment | ' \
64 | "event=#{name} | " \
65 | "request_id=#{request.env['HTTP_REQUEST_ID']}\n" \
66 | "message=\'#{message}\'\n" \
67 | "params: #{params}")
68 | when 'warning'
69 | logger.warn('service=comment | ' \
70 | "event=#{name} | " \
71 | "request_id=#{request.env['HTTP_REQUEST_ID']}\n" \
72 | "message=\'#{message}\'\n" \
73 | "params: #{params}")
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/ui/views/show.haml:
--------------------------------------------------------------------------------
1 | %div{ id: 'postlist'}
2 | %div{class: 'panel'}
3 | %div{class: 'panel-heading'}
4 | %div{class: 'text-center'}
5 | %div{class: 'row'}
6 | .col-sm-1
7 | %form{action: "#{@post['_id']['$oid']}/vote/1", method: "post"}
8 | %input{:type => "hidden", :name => "_method", :value => "post"}
9 | %button{type: "submit", class: "btn btn-default btn-sm"}
10 | %span{ class: "glyphicon glyphicon-menu-up" }
11 | %h4{class: 'pull-center'} #{@post['votes']}
12 | %form{:action => "#{@post['_id']['$oid']}/vote/-1", :method => "post"}
13 | %input{:type => "hidden", :name => "_method", :value=> "post"}
14 | %button{type: "submit", class: "btn btn-default btn-sm"}
15 | %span{ class: "glyphicon glyphicon-menu-down" }
16 | .col-sm-8
17 | %h3{class: 'pull-left'}
18 | %a{href: "#{@post['_id']['$oid']}"} #{@post['title']}
19 | .col-sm-3
20 | %h4{class: 'pull-right'}
21 | %small
22 | %em #{Time.at(@post['created_at'].to_i).strftime('%d-%m-%Y')}
23 | %br #{Time.at(@post['created_at'].to_i).strftime('%H:%M')}
24 | .panel-footer
25 | %a{href: "#{@post['link']}", class: 'btn btn-link'} Go to the link
26 |
27 | - unless @comments.nil? or @comments.empty?
28 | - @comments.each do |comment|
29 | .row
30 | .col-sm-8
31 | .panel.panel-default
32 | .panel-heading
33 | %strong #{comment['name']}
34 | - unless comment['email'].empty?
35 | (#{comment['email']})
36 | %span{class: "text-muted pull-right"}
37 | %em #{Time.at(comment['created_at'].to_i).strftime('%H:%M')} #{Time.at(comment['created_at'].to_i).strftime('%d-%m-%Y')}
38 | .panel-body #{comment['body']}
39 | %form{action: "/post/#{@post['_id']['$oid']}/comment", method: 'post', role: 'form'}
40 | .col-sm-4
41 | .form-group
42 | %input{name: 'name', type: 'text', class: 'form-control', placeholder: 'name'}
43 | .col-sm-4
44 | .form-group
45 | %input{name: 'email', type: 'email', class: 'form-control', placeholder: 'email'}
46 | .col-sm-8
47 | .form-group
48 | %textarea{name: 'body', class: 'form-control', placeholder: 'put a nice comment :)'}
49 | %button{class: 'btn btn-block btn-primary'} Post my comment
50 |
--------------------------------------------------------------------------------
/ui/helpers.rb:
--------------------------------------------------------------------------------
1 | def flash_danger(message)
2 | session[:flashes] << { type: 'alert-danger', message: message }
3 | end
4 |
5 | def flash_success(message)
6 | session[:flashes] << { type: 'alert-success', message: message }
7 | end
8 |
9 | def log_event(type, name, message, params = '{}')
10 | case type
11 | when 'error'
12 | logger.error('service=ui | ' \
13 | "event=#{name} | " \
14 | "request_id=#{request.env['REQUEST_ID']} | " \
15 | "message=\'#{message}\' | " \
16 | "params: #{params}")
17 | when 'info'
18 | logger.info('service=ui | ' \
19 | "event=#{name} | " \
20 | "request_id=#{request.env['REQUEST_ID']} | " \
21 | "message=\'#{message}\' | " \
22 | "params: #{params}")
23 | when 'warning'
24 | logger.warn('service=ui | ' \
25 | "event=#{name} | " \
26 | "request_id=#{request.env['REQUEST_ID']} | " \
27 | "message=\'#{message}\' | " \
28 | "params: #{params}")
29 | end
30 | end
31 |
32 | def http_request(method, url, params = {})
33 | unless defined?(request).nil?
34 | settings.http_client.headers[:request_id] = request.env['REQUEST_ID'].to_s
35 | end
36 |
37 | case method
38 | when 'get'
39 | response = settings.http_client.get url
40 | JSON.parse(response.body)
41 | when 'post'
42 | settings.http_client.post url, params
43 | end
44 | end
45 |
46 | def http_healthcheck_handler(post_url, comment_url, version)
47 | post_status = check_service_health(post_url)
48 | comment_status = check_service_health(comment_url)
49 |
50 | status = if comment_status == 1 && post_status == 1
51 | 1
52 | else
53 | 0
54 | end
55 |
56 | healthcheck = { status: status,
57 | dependent_services: {
58 | comment: comment_status,
59 | post: post_status
60 | },
61 | version: version }
62 | healthcheck.to_json
63 | end
64 |
65 | def check_service_health(url)
66 | name = http_request('get', "#{url}/healthcheck")
67 | rescue StandardError
68 | 0
69 | else
70 | name['status']
71 | end
72 |
73 | def set_health_gauge(metric, value)
74 | metric.set(
75 | {
76 | version: VERSION,
77 | commit_hash: BUILD_INFO[0].strip,
78 | branch: BUILD_INFO[1].strip
79 | },
80 | value
81 | )
82 | end
83 |
--------------------------------------------------------------------------------
/comment/comment_app.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra'
2 | require 'json/ext'
3 | require 'uri'
4 | require 'mongo'
5 | require 'prometheus/client'
6 | require 'rufus-scheduler'
7 | require_relative 'helpers'
8 |
9 | # Database connection info
10 | COMMENT_DATABASE_HOST ||= ENV['COMMENT_DATABASE_HOST'] || '127.0.0.1'
11 | COMMENT_DATABASE_PORT ||= ENV['COMMENT_DATABASE_PORT'] || '27017'
12 | COMMENT_DATABASE ||= ENV['COMMENT_DATABASE'] || 'test'
13 | DB_URL ||= "mongodb://#{COMMENT_DATABASE_HOST}:#{COMMENT_DATABASE_PORT}"
14 |
15 | # App version and build info
16 | VERSION ||= File.read('VERSION').strip
17 | BUILD_INFO = File.readlines('build_info.txt')
18 |
19 | configure do
20 | Mongo::Logger.logger.level = Logger::WARN
21 | db = Mongo::Client.new(DB_URL, database: COMMENT_DATABASE,
22 | heartbeat_frequency: 2)
23 | set :mongo_db, db[:comments]
24 | set :bind, '0.0.0.0'
25 | set :server, :puma
26 | set :logging, false
27 | set :mylogger, Logger.new(STDOUT)
28 | end
29 |
30 | # Create and register metrics
31 | prometheus = Prometheus::Client.registry
32 | comment_health_gauge = Prometheus::Client::Gauge.new(
33 | :comment_health,
34 | 'Health status of Comment service'
35 | )
36 | comment_health_db_gauge = Prometheus::Client::Gauge.new(
37 | :comment_health_mongo_availability,
38 | 'Check if MongoDB is available to Comment'
39 | )
40 | comment_count = Prometheus::Client::Counter.new(
41 | :comment_count,
42 | 'A counter of new comments'
43 | )
44 | prometheus.register(comment_health_gauge)
45 | prometheus.register(comment_health_db_gauge)
46 | prometheus.register(comment_count)
47 |
48 | # Schedule health check function
49 | scheduler = Rufus::Scheduler.new
50 | scheduler.every '5m' do
51 | check = JSON.parse(healthcheck_handler(DB_URL, VERSION))
52 | set_health_gauge(comment_health_gauge, check['status'])
53 | set_health_gauge(comment_health_db_gauge, check['dependent_services']['commentdb'])
54 | end
55 |
56 | before do
57 | env['rack.logger'] = settings.mylogger # set custom logger
58 | end
59 |
60 | after do
61 | request_id = env['HTTP_REQUEST_ID'] || 'null'
62 | logger.info('service=comment | event=request | ' \
63 | "path=#{env['REQUEST_PATH']}\n" \
64 | "request_id=#{request_id} | " \
65 | "remote_addr=#{env['REMOTE_ADDR']} | " \
66 | "method= #{env['REQUEST_METHOD']} | " \
67 | "response_status=#{response.status}")
68 | end
69 |
70 | # retrieve post's comments
71 | get '/:id/comments' do
72 | id = obj_id(params[:id])
73 | begin
74 | posts = settings.mongo_db.find(post_id: id.to_s).to_a.to_json
75 | rescue StandardError => e
76 | log_event('error', 'find_post_comments',
77 | "Couldn't retrieve comments from DB. Reason: #{e.message}",
78 | params)
79 | halt 500
80 | else
81 | log_event('info', 'find_post_comments',
82 | 'Successfully retrieved post comments from DB', params)
83 | posts
84 | end
85 | end
86 |
87 | # add a new comment
88 | post '/add_comment/?' do
89 | begin
90 | prms = { post_id: params['post_id'],
91 | name: params['name'],
92 | email: params['email'],
93 | body: params['body'],
94 | created_at: params['created_at'] }
95 | rescue StandardError => e
96 | log_event('error', 'add_comment',
97 | "Bat input data. Reason: #{e.message}", prms)
98 | end
99 | db = settings.mongo_db
100 | begin
101 | result = db.insert_one post_id: params['post_id'], name: params['name'],
102 | email: params['email'], body: params['body'],
103 | created_at: params['created_at']
104 | db.find(_id: result.inserted_id).to_a.first.to_json
105 | rescue StandardError => e
106 | log_event('error', 'add_comment',
107 | "Failed to create a comment. Reason: #{e.message}", params)
108 | halt 500
109 | else
110 | log_event('info', 'add_comment',
111 | 'Successfully created a new comment', params)
112 | comment_count.increment
113 | end
114 | end
115 |
116 | # health check endpoint
117 | get '/healthcheck' do
118 | healthcheck_handler(DB_URL, VERSION)
119 | end
120 |
121 | get '/*' do
122 | halt 404, 'Page not found'
123 | end
124 |
--------------------------------------------------------------------------------
/ui/ui_app.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra'
2 | require 'sinatra/reloader'
3 | require 'json/ext'
4 | require 'haml'
5 | require 'uri'
6 | require 'prometheus/client'
7 | require 'rufus-scheduler'
8 | require 'logger'
9 | require 'faraday'
10 | require 'zipkin-tracer'
11 | require_relative 'helpers'
12 |
13 | # Dependent services
14 | POST_SERVICE_HOST ||= ENV['POST_SERVICE_HOST'] || '127.0.0.1'
15 | POST_SERVICE_PORT ||= ENV['POST_SERVICE_PORT'] || '4567'
16 | COMMENT_SERVICE_HOST ||= ENV['COMMENT_SERVICE_HOST'] || '127.0.0.1'
17 | COMMENT_SERVICE_PORT ||= ENV['COMMENT_SERVICE_PORT'] || '4567'
18 | POST_URL ||= "http://#{POST_SERVICE_HOST}:#{POST_SERVICE_PORT}"
19 | COMMENT_URL ||= "http://#{COMMENT_SERVICE_HOST}:#{COMMENT_SERVICE_PORT}"
20 |
21 | # App version and build info
22 | VERSION ||= File.read('VERSION').strip
23 | BUILD_INFO = File.readlines('build_info.txt')
24 |
25 | configure do
26 | http_client = Faraday.new do |faraday|
27 | faraday.use ZipkinTracer::FaradayHandler
28 | faraday.request :url_encoded # form-encode POST params
29 | # faraday.response :logger
30 | faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
31 | end
32 | set :http_client, http_client
33 | set :bind, '0.0.0.0'
34 | set :server, :puma
35 | set :logging, false
36 | set :mylogger, Logger.new(STDOUT)
37 | enable :sessions
38 | end
39 |
40 | # create and register metrics
41 | prometheus = Prometheus::Client.registry
42 | ui_health_gauge = Prometheus::Client::Gauge.new(
43 | :ui_health,
44 | 'Health status of UI service'
45 | )
46 | ui_health_post_gauge = Prometheus::Client::Gauge.new(
47 | :ui_health_post_availability,
48 | 'Check if Post service is available to UI'
49 | )
50 | ui_health_comment_gauge = Prometheus::Client::Gauge.new(
51 | :ui_health_comment_availability,
52 | 'Check if Comment service is available to UI'
53 | )
54 | prometheus.register(ui_health_gauge)
55 | prometheus.register(ui_health_post_gauge)
56 | prometheus.register(ui_health_comment_gauge)
57 |
58 | # Schedule health check function
59 | scheduler = Rufus::Scheduler.new
60 | scheduler.every '5m' do
61 | check = JSON.parse(http_healthcheck_handler(POST_URL, COMMENT_URL, VERSION))
62 | set_health_gauge(ui_health_gauge, check['status'])
63 | set_health_gauge(ui_health_post_gauge, check['dependent_services']['post'])
64 | set_health_gauge(ui_health_comment_gauge, check['dependent_services']['comment'])
65 | end
66 |
67 | # before each request
68 | before do
69 | session[:flashes] = [] if session[:flashes].class != Array
70 | env['rack.logger'] = settings.mylogger # set custom logger
71 | end
72 |
73 | # after each request
74 | after do
75 | request_id = env['REQUEST_ID'] || 'null'
76 | logger.info("service=ui | event=request | path=#{env['REQUEST_PATH']} | " \
77 | "request_id=#{request_id} | " \
78 | "remote_addr=#{env['REMOTE_ADDR']} | " \
79 | "method= #{env['REQUEST_METHOD']} | " \
80 | "response_status=#{response.status}")
81 | end
82 |
83 | # show all posts
84 | get '/' do
85 | @title = 'All posts'
86 | begin
87 | @posts = http_request('get', "#{POST_URL}/posts")
88 | rescue StandardError => e
89 | flash_danger('Can\'t show blog posts, some problems with the post ' \
90 | 'service. Refresh?')
91 | log_event('error', 'show_all_posts',
92 | "Failed to read from Post service. Reason: #{e.message}")
93 | else
94 | log_event('info', 'show_all_posts',
95 | 'Successfully showed the home page with posts')
96 | end
97 | @flashes = session[:flashes]
98 | session[:flashes] = nil
99 | haml :index
100 | end
101 |
102 | # show a form for creating a new post
103 | get '/new' do
104 | @title = 'New post'
105 | @flashes = session[:flashes]
106 | session[:flashes] = nil
107 | haml :create
108 | end
109 |
110 | # talk to Post service in order to creat a new post
111 | post '/new/?' do
112 | if params['link'] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]
113 | begin
114 | http_request('post', "#{POST_URL}/add_post", title: params['title'],
115 | link: params['link'],
116 | created_at: Time.now.to_i)
117 | rescue StandardError => e
118 | flash_danger("Can't save your post, some problems with the post service")
119 | log_event('error', 'post_create',
120 | "Failed to create a post. Reason: #{e.message}", params)
121 | else
122 | flash_success('Post successuly published')
123 | log_event('info', 'post_create', 'Successfully created a post', params)
124 | end
125 | redirect '/'
126 | else
127 | flash_danger('Invalid URL')
128 | log_event('warning', 'post_create', 'Invalid URL', params)
129 | redirect back
130 | end
131 | end
132 |
133 | # talk to Post service in order to vote on a post
134 | post '/post/:id/vote/:type' do
135 | begin
136 | http_request('post', "#{POST_URL}/vote", id: params[:id],
137 | type: params[:type])
138 | rescue StandardError => e
139 | flash_danger('Can\'t vote, some problems with the post service')
140 | log_event('error', 'vote',
141 | "Failed to vote. Reason: #{e.message}", params)
142 | else
143 | log_event('info', 'vote', 'Successful vote', params)
144 | end
145 | redirect back
146 | end
147 |
148 | # show a specific post
149 | get '/post/:id' do
150 | begin
151 | @post = http_request('get', "#{POST_URL}/post/#{params[:id]}")
152 | rescue StandardError => e
153 | log_event('error', 'show_post',
154 | "Counldn't show the post. Reason: #{e.message}", params)
155 | halt 404, 'Not found'
156 | end
157 |
158 | begin
159 | @comments = http_request('get', "#{COMMENT_URL}/#{params[:id]}/comments")
160 | rescue StandardError => e
161 | log_event('error', 'show_post',
162 | "Counldn't show the comments. Reason: #{e.message}", params)
163 | flash_danger("Can't show comments, some problems with the comment service")
164 | else
165 | log_event('info', 'show_post',
166 | 'Successfully showed the post', params)
167 | end
168 | @flashes = session[:flashes]
169 | session[:flashes] = nil
170 | haml :show
171 | end
172 |
173 | # talk to Comment service in order to comment on a post
174 | post '/post/:id/comment' do
175 | begin
176 | http_request('post', "#{COMMENT_URL}/add_comment",
177 | post_id: params[:id],
178 | name: params[:name],
179 | email: params[:email],
180 | created_at: Time.now.to_i,
181 | body: params[:body])
182 | rescue StandardError => e
183 | log_event('error', 'create_comment',
184 | "Counldn't create a comment. Reason: #{e.message}", params)
185 | flash_danger("Can\'t save the comment,
186 | some problems with the comment service")
187 | else
188 | log_event('info', 'create_comment',
189 | 'Successfully created a new post', params)
190 |
191 | flash_success('Comment successuly published')
192 | end
193 | redirect back
194 | end
195 |
196 | # health check endpoint
197 | get '/healthcheck' do
198 | http_healthcheck_handler(POST_URL, COMMENT_URL, VERSION)
199 | end
200 |
201 | get '/*' do
202 | halt 404, 'Page not found'
203 | end
204 |
--------------------------------------------------------------------------------
/post-py/post_app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import prometheus_client
3 | import time
4 | import structlog
5 | import traceback
6 | import requests
7 | from flask import Flask, request, Response, abort, logging
8 | from pymongo import MongoClient
9 | from bson.objectid import ObjectId
10 | from bson.json_util import dumps
11 | from helpers import http_healthcheck_handler, log_event
12 | from py_zipkin.zipkin import zipkin_span, ZipkinAttrs
13 |
14 |
15 | CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8')
16 | POST_DATABASE_HOST = os.getenv('POST_DATABASE_HOST', '127.0.0.1')
17 | POST_DATABASE_PORT = os.getenv('POST_DATABASE_PORT', '27017')
18 | ZIPKIN_HOST = os.getenv('ZIPKIN_HOST', 'zipkin')
19 | ZIPKIN_PORT = os.getenv('ZIPKIN_PORT', '9411')
20 | ZIPKIN_URL = "http://{0}:{1}/api/v1/spans".format(ZIPKIN_HOST, ZIPKIN_PORT)
21 |
22 | log = structlog.get_logger()
23 |
24 | app = Flask(__name__)
25 |
26 |
27 | def init(app):
28 | # appication version info
29 | app.version = None
30 | with open('VERSION') as f:
31 | app.version = f.read().rstrip()
32 |
33 | # prometheus metrics
34 | app.post_read_db_seconds = prometheus_client.Histogram(
35 | 'post_read_db_seconds',
36 | 'Request DB time'
37 | )
38 | app.post_count = prometheus_client.Counter(
39 | 'post_count',
40 | 'A counter of new posts'
41 | )
42 | # database client connection
43 | app.db = MongoClient(
44 | POST_DATABASE_HOST,
45 | int(POST_DATABASE_PORT)
46 | ).users_post.posts
47 |
48 |
49 | def http_transport(encoded_span):
50 | # The collector expects a thrift-encoded list of spans. Instead of
51 | # decoding and re-encoding the already thrift-encoded message, we can just
52 | # add header bytes that specify that what follows is a list of length 1.
53 | body = '\x0c\x00\x00\x00\x01' + encoded_span
54 | requests.post(ZIPKIN_URL, data=body,
55 | headers={'Content-Type': 'application/x-thrift'})
56 |
57 |
58 | # Prometheus endpoint
59 | @app.route('/metrics')
60 | def metrics():
61 | return Response(prometheus_client.generate_latest(),
62 | mimetype=CONTENT_TYPE_LATEST)
63 |
64 | # Retrieve information about all posts
65 | @zipkin_span(service_name='post', span_name='db_find_all_posts')
66 | def find_posts():
67 | try:
68 | posts = app.db.find().sort('created_at', -1)
69 | except Exception as e:
70 | log_event('error', 'find_all_posts',
71 | "Failed to retrieve posts from the database. \
72 | Reason: {}".format(str(e)))
73 | abort(500)
74 | else:
75 | log_event('info', 'find_all_posts',
76 | 'Successfully retrieved all posts from the database')
77 | return dumps(posts)
78 |
79 |
80 | @app.route("/posts")
81 | def posts():
82 | with zipkin_span(
83 | service_name='post',
84 | zipkin_attrs=ZipkinAttrs(
85 | trace_id=request.headers['X-B3-TraceID'],
86 | span_id=request.headers['X-B3-SpanID'],
87 | parent_span_id=request.headers['X-B3-ParentSpanID'],
88 | flags=request.headers['X-B3-Flags'],
89 | is_sampled=request.headers['X-B3-Sampled'],
90 | ),
91 | span_name='/posts',
92 | transport_handler=http_transport,
93 | port=5000,
94 | sample_rate=100,
95 | ):
96 | posts = find_posts()
97 | return posts
98 |
99 |
100 | # Vote for a post
101 | @app.route('/vote', methods=['POST'])
102 | def vote():
103 | try:
104 | post_id = request.values.get('id')
105 | vote_type = request.values.get('type')
106 | except Exception as e:
107 | log_event('error', 'request_error',
108 | "Bad input parameters. Reason: {}".format(str(e)))
109 | abort(400)
110 | try:
111 | post = app.db.find_one({'_id': ObjectId(post_id)})
112 | post['votes'] += int(vote_type)
113 | app.db.update_one({'_id': ObjectId(post_id)},
114 | {'$set': {'votes': post['votes']}})
115 | except Exception as e:
116 | log_event('error', 'post_vote',
117 | "Failed to vote for a post. Reason: {}".format(str(e)),
118 | {'post_id': post_id, 'vote_type': vote_type})
119 | abort(500)
120 | else:
121 | log_event('info', 'post_vote', 'Successful vote',
122 | {'post_id': post_id, 'vote_type': vote_type})
123 | return 'OK'
124 |
125 |
126 | # Add new post
127 | @app.route('/add_post', methods=['POST'])
128 | def add_post():
129 | try:
130 | title = request.values.get('title')
131 | link = request.values.get('link')
132 | created_at = request.values.get('created_at')
133 | except Exception as e:
134 | log_event('error', 'request_error',
135 | "Bad input parameters. Reason: {}".format(str(e)))
136 | abort(400)
137 | try:
138 | app.db.insert({'title': title, 'link': link,
139 | 'created_at': created_at, 'votes': 0})
140 | except Exception as e:
141 | log_event('error', 'post_create',
142 | "Failed to create a post. Reason: {}".format(str(e)),
143 | {'title': title, 'link': link})
144 | abort(500)
145 | else:
146 | log_event('info', 'post_create', 'Successfully created a new post',
147 | {'title': title, 'link': link})
148 | app.post_count.inc()
149 | return 'OK'
150 |
151 |
152 | # Retrieve information about a post
153 | @zipkin_span(service_name='post', span_name='db_find_single_post')
154 | def find_post(id):
155 | start_time = time.time()
156 | try:
157 | post = app.db.find_one({'_id': ObjectId(id)})
158 | except Exception as e:
159 | log_event('error', 'post_find',
160 | "Failed to find the post. Reason: {}".format(str(e)),
161 | request.values)
162 | abort(500)
163 | else:
164 | stop_time = time.time() # + 0.3
165 | resp_time = stop_time - start_time
166 | app.post_read_db_seconds.observe(resp_time)
167 | time.sleep(3)
168 | log_event('info', 'post_find',
169 | 'Successfully found the post information',
170 | {'post_id': id})
171 | return dumps(post)
172 |
173 |
174 | # Find a post
175 | @app.route('/post/')
176 | def get_post(id):
177 | with zipkin_span(
178 | service_name='post',
179 | zipkin_attrs=ZipkinAttrs(
180 | trace_id=request.headers['X-B3-TraceID'],
181 | span_id=request.headers['X-B3-SpanID'],
182 | parent_span_id=request.headers['X-B3-ParentSpanID'],
183 | flags=request.headers['X-B3-Flags'],
184 | is_sampled=request.headers['X-B3-Sampled'],
185 | ),
186 | span_name='/post/',
187 | transport_handler=http_transport,
188 | port=5000,
189 | sample_rate=100,
190 | ):
191 | post = find_post(id)
192 | return post
193 |
194 |
195 | # Health check endpoint
196 | @app.route('/healthcheck')
197 | def healthcheck():
198 | return http_healthcheck_handler(POST_DATABASE_HOST,
199 | POST_DATABASE_PORT,
200 | app.version)
201 |
202 |
203 | # Log every request
204 | @app.after_request
205 | def after_request(response):
206 | request_id = request.headers['Request-Id'] \
207 | if 'Request-Id' in request.headers else None
208 | log.info('request',
209 | service='post',
210 | request_id=request_id,
211 | path=request.full_path,
212 | addr=request.remote_addr,
213 | method=request.method,
214 | response_status=response.status_code)
215 | return response
216 |
217 |
218 | # Log Exceptions
219 | @app.errorhandler(Exception)
220 | def exceptions(e):
221 | request_id = request.headers['Request-Id'] \
222 | if 'Request-Id' in request.headers else None
223 | tb = traceback.format_exc()
224 | log.error('internal_error',
225 | service='post',
226 | request_id=request_id,
227 | path=request.full_path,
228 | remote_addr=request.remote_addr,
229 | method=request.method,
230 | traceback=tb)
231 | return 'Internal Server Error', 500
232 |
233 |
234 | if __name__ == "__main__":
235 | init(app)
236 | logg = logging.getLogger('werkzeug')
237 | logg.disabled = True # disable default logger
238 | # define log structure
239 | structlog.configure(processors=[
240 | structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
241 | structlog.stdlib.add_log_level,
242 | # to see indented logs in the terminal, uncomment the line below
243 | # structlog.processors.JSONRenderer(indent=2, sort_keys=True)
244 | # and comment out the one below
245 | structlog.processors.JSONRenderer(sort_keys=True)
246 | ])
247 | app.run(host='0.0.0.0', debug=True)
248 |
--------------------------------------------------------------------------------