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