├── test └── org │ └── zalando │ └── stups │ └── friboo │ ├── test.edn │ ├── ring_test.clj │ ├── dev_test.clj │ ├── system │ ├── cron_test.clj │ ├── http.yml │ ├── db_test.clj │ ├── mgmt_http_test.clj │ ├── metrics_test.clj │ └── http_test.clj │ ├── test_utils.clj │ ├── system_test.clj │ └── config_test.clj ├── resources ├── hystrix-dashboard │ ├── css │ │ ├── simplegrid │ │ │ ├── README.txt │ │ │ ├── 1236_grid.css │ │ │ ├── LICENSE.txt │ │ │ ├── 986_grid.css │ │ │ ├── percentage_grid.css │ │ │ └── 720_grid.css │ │ ├── global.css │ │ └── resets.css │ ├── images │ │ ├── hystrix-logo.png │ │ └── hystrix-logo-tagline-tiny.png │ ├── components │ │ ├── hystrixCommand │ │ │ ├── magnifying-glass-icon.png │ │ │ ├── magnifying-glass-icon-20.png │ │ │ ├── templates │ │ │ │ ├── hystrixCircuitProperties.html │ │ │ │ ├── hystrixCircuitContainer.html │ │ │ │ └── hystrixCircuit.html │ │ │ ├── hystrixCommand.css │ │ │ └── hystrixCommand.js │ │ └── hystrixThreadPool │ │ │ ├── templates │ │ │ ├── hystrixThreadPool.html │ │ │ └── hystrixThreadPoolContainer.html │ │ │ ├── hystrixThreadPool.css │ │ │ └── hystrixThreadPool.js │ ├── js │ │ ├── LICENSE │ │ ├── tmpl.js │ │ └── jquery.tinysort.min.js │ ├── monitor │ │ ├── monitor.css │ │ └── monitor.html │ └── index.html └── log4j2.xml ├── template ├── resources │ └── leiningen │ │ └── new │ │ └── friboo │ │ ├── dev-config.edn │ │ ├── gitignore │ │ ├── api_test.clj │ │ ├── api.clj │ │ ├── README.md │ │ ├── core_test.clj │ │ ├── project.clj │ │ ├── core.clj │ │ ├── api.yaml │ │ └── user.clj ├── .gitignore ├── itest.sh ├── project.clj ├── README.md ├── test │ └── leiningen │ │ └── new │ │ └── friboo_test.clj ├── src │ └── leiningen │ │ └── new │ │ └── friboo.clj └── LICENSE ├── MAINTAINERS ├── .zappr.yaml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── profiles.clj ├── src └── org │ └── zalando │ └── stups │ └── friboo │ ├── ring.clj │ ├── dev.clj │ ├── log.clj │ ├── system.clj │ ├── config_decrypt.clj │ ├── system │ ├── mgmt_http.clj │ ├── cron.clj │ ├── db.clj │ ├── metrics.clj │ └── http.clj │ └── config.clj ├── dev └── user.clj ├── make.sh ├── CHANGELOG.md ├── project.clj ├── LICENSE └── README.md /test/org/zalando/stups/friboo/test.edn: -------------------------------------------------------------------------------- 1 | {:foo "bar"} 2 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/simplegrid/README.txt: -------------------------------------------------------------------------------- 1 | http://simplegrid.info/ -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/dev-config.edn: -------------------------------------------------------------------------------- 1 | {:api-example-param "foo"} 2 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Tobias Sarnowski 2 | Dmitrii Balakhonskii 3 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/images/hystrix-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/friboo/master/resources/hystrix-dashboard/images/hystrix-logo.png -------------------------------------------------------------------------------- /.zappr.yaml: -------------------------------------------------------------------------------- 1 | approvals: 2 | groups: 3 | zalando: 4 | minimum: 2 5 | from: 6 | orgs: 7 | - "zalando" 8 | X-Zalando-Team: "automata" 9 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/images/hystrix-logo-tagline-tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/friboo/master/resources/hystrix-dashboard/images/hystrix-logo-tagline-tiny.png -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | /.idea 13 | /*.iml 14 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixCommand/magnifying-glass-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/friboo/master/resources/hystrix-dashboard/components/hystrixCommand/magnifying-glass-icon.png -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixCommand/magnifying-glass-icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/friboo/master/resources/hystrix-dashboard/components/hystrixCommand/magnifying-glass-icon-20.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Leiningen 2 | /target 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | 12 | # IntelliJ 13 | /.idea 14 | *.iml 15 | 16 | # VIM 17 | *.swp 18 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/gitignore: -------------------------------------------------------------------------------- 1 | # Leiningen 2 | pom.xml 3 | pom.xml.asc 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /target/ 8 | /checkouts/ 9 | .lein-* 10 | .nrepl-port 11 | 12 | # IntelliJ IDEA 13 | /.idea 14 | *.iml 15 | 16 | # Development 17 | /dev-config.edn 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | sudo: false 7 | 8 | addons: 9 | postgresql: "9.4" 10 | 11 | script: lein test 12 | 13 | after_success: 14 | - CLOVERAGE_VERSION=1.0.9 lein cloverage --codecov 15 | - bash <(curl -s https://codecov.io/bash) -f target/coverage/codecov.json 16 | 17 | notifications: 18 | email: 19 | on_failure: change 20 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixCommand/templates/hystrixCircuitProperties.html: -------------------------------------------------------------------------------- 1 |
2 |
Median
3 |
<%= sla_medianLastMinute %>ms
4 |
99th
5 |
<%= sla_percentile99LastMinute %>ms
6 |
7 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns {{namespace}}.api-test 2 | (:require [clojure.test :refer :all] 3 | [midje.sweet :refer :all] 4 | [{{namespace}}.api :refer :all])) 5 | 6 | (deftest can-get-hello 7 | (is (= (get-hello {:configuration {:example-param "foo"}} {:name "Friboo"} nil) 8 | {:status 200 9 | :headers {} 10 | :body {:message "Hello Friboo" :details {:X-friboo "foo"}}}))) 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Pull requests only 4 | 5 | **DON'T** push to the master branch directly. Always use feature branches and let people discuss changes in pull requests. 6 | Pull requests should only be merged after all discussions have been concluded and at least 1 reviewer has given their 7 | **approval**. 8 | 9 | ## Guidelines 10 | 11 | - **every change** needs a test 12 | - strive to achieve 100% code coverage 13 | - keep the current code style -------------------------------------------------------------------------------- /profiles.clj: -------------------------------------------------------------------------------- 1 | {:test {:dependencies [[midje "1.8.3"] 2 | [org.postgresql/postgresql "9.4.1212"]] 3 | :env {:tokeninfo-url "default-tokeninfo"}} 4 | :dev {:repl-options {:init-ns user} 5 | :source-paths ["dev"] 6 | :dependencies [[midje "1.8.3"] 7 | [org.postgresql/postgresql "9.4.1212"] 8 | [org.clojure/tools.namespace "0.2.11"] 9 | [org.clojure/java.classpath "0.2.3"]]}} 10 | -------------------------------------------------------------------------------- /resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /template/itest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\t\n' 4 | set -x 5 | 6 | # Build the template itself 7 | lein do clean, test 8 | 9 | # Generate a project based on the template and run tests in it 10 | cd target 11 | # We don't need to install it to ~/.m2, because it's already available on the classpath 12 | DEBUG=1 lein new friboo com.example/foo-bar 13 | 14 | pushd foo-bar 15 | lein test 16 | lein uberjar 17 | popd 18 | 19 | # Just in case we want to try it outside of target/ 20 | lein install 21 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/ring.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.ring 2 | (:require [ring.util.response :refer :all])) 3 | 4 | ;; some convinience helpers 5 | 6 | (defn content-type-json 7 | "Sets the content-type of the response to 'application/json'." 8 | [response] 9 | (content-type response "application/json")) 10 | 11 | (defn single-response 12 | "Returns 404 if results is empty or the first result, ignoring all others." 13 | [results] 14 | (if (empty? results) 15 | (not-found {}) 16 | (response (first results)))) 17 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/ring_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.ring-test 2 | (:require [clojure.test :refer :all] 3 | [midje.sweet :refer :all] 4 | [org.zalando.stups.friboo.ring :refer :all])) 5 | 6 | (deftest wrap-midje-facts 7 | 8 | (facts "about content-type-json" 9 | (content-type-json {}) => {:headers {"Content-Type" "application/json"}}) 10 | 11 | (facts "about single-response" 12 | (single-response []) => (contains {:status 404}) 13 | (single-response ["foo"]) => (contains {:status 200 :body "foo"})) 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/dev.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.dev 2 | (:require [clojure.edn :as edn]) 3 | (:import (java.net ServerSocket) 4 | (org.apache.logging.log4j LogManager))) 5 | 6 | (defn slurp-if-exists [file] 7 | (when (.exists (clojure.java.io/as-file file)) 8 | (slurp file))) 9 | 10 | (defn load-dev-config [file] 11 | (edn/read-string (slurp-if-exists file))) 12 | 13 | (defn get-free-port [] 14 | (let [sock (ServerSocket. 0)] 15 | (try 16 | (.getLocalPort sock) 17 | (finally 18 | (.close sock))))) 19 | 20 | (defn reload-log4j2-config [] 21 | (.reconfigure (LogManager/getContext false))) 22 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Tools for interactive development with the REPL. This file should 3 | not be included in a production build of the application." 4 | (:require 5 | [clojure.java.javadoc :refer [javadoc]] 6 | [clojure.pprint :refer [pprint]] 7 | [clojure.reflect :refer [reflect]] 8 | [clojure.repl :refer [apropos dir doc find-doc pst source]] 9 | [clojure.tools.namespace.repl :refer [refresh refresh-all]] 10 | [clojure.test :refer [run-all-tests]])) 11 | 12 | (defn run-tests [] 13 | (run-all-tests #"org.zalando.stups.friboo.*-test")) 14 | 15 | (defn tests 16 | "Stops the system, reloads modified source files and runs tests" 17 | [] 18 | (refresh :after 'user/run-tests)) 19 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\t\n' 4 | 5 | DB_CONTAINER=friboo-db 6 | DB_PORT=5432 7 | 8 | cmd_db() { 9 | docker rm -fv $DB_CONTAINER || true 10 | docker run -dt --name $DB_CONTAINER \ 11 | -p $DB_PORT:5432 \ 12 | postgres 13 | } 14 | 15 | # Print all defined cmd_ 16 | cmd_help() { 17 | compgen -A function cmd_ 18 | } 19 | 20 | # Run multiple commands without args 21 | cmd_mm() { 22 | for cmd in "$@"; do 23 | cmd_$cmd 24 | done 25 | } 26 | 27 | if [[ $# -eq 0 ]]; then 28 | echo Please provide a subcommand 29 | exit 1 30 | fi 31 | 32 | SUBCOMMAND=$1 33 | shift 34 | 35 | # Enable verbose mode 36 | set -x 37 | # Run the subcommand 38 | cmd_${SUBCOMMAND} $@ 39 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/dev_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.dev-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [midje.sweet :refer :all] 5 | [org.zalando.stups.friboo.dev :refer :all])) 6 | 7 | (deftest wrap-midje-facts 8 | 9 | (facts "about slurp-if-exists" 10 | (slurp-if-exists "nonexistent-foo") => nil 11 | (slurp-if-exists "test/org/zalando/stups/friboo/test.edn") => "{:foo \"bar\"}\n") 12 | 13 | (facts "about load-dev-config" 14 | (load-dev-config "nonexistent-foo") => nil 15 | (load-dev-config "test/org/zalando/stups/friboo/test.edn") => {:foo "bar"}) 16 | 17 | (facts "about get-free-port" 18 | (repeatedly 100 get-free-port) => (has every? #(< 1024 % 65536))) 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/system/cron_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.cron-test 2 | (:require [org.zalando.stups.friboo.system.cron :refer :all] 3 | [clojure.test :refer :all] 4 | [overtone.at-at :as at] 5 | [com.stuartsierra.component :as component])) 6 | 7 | (def-cron-component TestCron [state] 8 | (at/at (at/now) (job deliver state 42) pool)) 9 | 10 | (deftest test-cron-component-lifecycle 11 | ;; Here we make sure that the component is started and stopped properly. 12 | (let [state (promise) 13 | cron-component (component/start (map->TestCron {:state state}))] 14 | (is (= 42 (deref state 500 :not-delivered))) 15 | (-> cron-component 16 | component/stop 17 | component/stop))) ; stopping twice shouldn't break anything 18 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/api.clj: -------------------------------------------------------------------------------- 1 | (ns {{namespace}}.api 2 | (:require [org.zalando.stups.friboo.ring :refer :all] 3 | [org.zalando.stups.friboo.log :as log] 4 | [org.zalando.stups.friboo.config :refer [require-config]] 5 | [com.stuartsierra.component :as component] 6 | [ring.util.response :as r])) 7 | 8 | (defrecord Controller [configuration] 9 | component/Lifecycle 10 | (start [this] 11 | (log/info "Starting API Controller") 12 | this) 13 | (stop [this] 14 | (log/info "Stopping API Controller") 15 | this)) 16 | 17 | (defn get-hello 18 | "Says hello" 19 | [{:as this :keys [configuration]} {:as params :keys [name]} request] 20 | (log/debug "API configuration: %s" configuration) 21 | (log/info "Hello called for %s" name) 22 | (r/response {:message (str "Hello " name) :details {:X-friboo (require-config configuration :example-param)}})) 23 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/README.md: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | 3 | A project based on Friboo library. 4 | 5 | ## Development 6 | 7 | Run the application: 8 | 9 | ``` 10 | $ lein repl 11 | user=> (reset) 12 | ``` 13 | 14 | For REPL-driven interactive development configuration variables can be provided in `dev-config.edn` file, which will be read on each system restart. 15 | 16 | ## Testing 17 | 18 | ``` 19 | $ lein test 20 | ``` 21 | 22 | ## Building 23 | 24 | ``` 25 | $ lein uberjar 26 | ``` 27 | 28 | ## Running 29 | 30 | ``` 31 | $ lein run 32 | ``` 33 | 34 | The following configuration environment variables are available: 35 | 36 | | Variable | Meaning | Default | Example | 37 | |---|---|---|---| 38 | | API_EXAMPLE_PARAM | Example parameter with `:api-` prefix | `bar` | `foo` | 39 | 40 | ## License 41 | 42 | Copyright © 2016 FIXME 43 | 44 | Distributed under the Eclipse Public License either version 1.0 or (at 45 | your option) any later version. 46 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/simplegrid/1236_grid.css: -------------------------------------------------------------------------------- 1 | /* SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) 2 | * http://simplegrid.info 3 | * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) 4 | * License: http://creativecommons.org/licenses/MIT/ */ 5 | 6 | /* Containers */ 7 | body { font-size: 1.125em; } 8 | .grid{ width:1206px; } 9 | 10 | /* 6-Col Grid Sizes */ 11 | .slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:176px; } /* Sixths */ 12 | .slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:382px; } /* Thirds */ 13 | .slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:794px; } /* Two-Thirds */ 14 | .slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:1000px; } /* Five-Sixths */ 15 | 16 | /* 4-Col Grid Sizes */ 17 | .slot-6,.slot-7,.slot-8,.slot-9{ width:279px; } /* Quarters */ 18 | .slot-6-7-8,.slot-7-8-9{ width:897px; } /* Three-Quarters */ 19 | 20 | /* 6-Col/4-Col Shared Grid Sizes */ 21 | .slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:588px; } /* Halves */ -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns {{namespace}}.core-test 2 | (:require [clojure.test :refer :all] 3 | [midje.sweet :refer :all] 4 | [org.zalando.stups.friboo.dev :as dev] 5 | [com.stuartsierra.component :as component] 6 | [clj-http.client :as http] 7 | [{{namespace}}.core :refer :all])) 8 | 9 | (deftest test-core-system 10 | 11 | (facts "about run" 12 | (let [dev-config (dev/load-dev-config "./dev-config.edn") 13 | test-config (merge {:http-port (dev/get-free-port)} 14 | dev-config) 15 | system (run test-config) 16 | port (-> system :http :configuration :port)] 17 | (try 18 | (facts "In the beginning there are no memories" 19 | (http/get (str "http://localhost:" port "/hello/Friboo") {:as :json}) 20 | => (contains {:status 200 :body {:details {:X-friboo "foo"}, :message "Hello Friboo"}})) 21 | (finally 22 | (component/stop system))))) 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /template/project.clj: -------------------------------------------------------------------------------- 1 | (defproject friboo/lein-template "2.0.0-beta2-SNAPSHOT" 2 | :description "Leiningen template for Friboo library" 3 | :url "https://github.com/zalando/friboo/template" 4 | :license {:name "Apache License" 5 | :url "http://www.apache.org/licenses/"} 6 | :eval-in-leiningen true 7 | :deploy-repositories [["releases" :clojars]] 8 | :plugins [[lein-shell "0.5.0"]] 9 | :release-tasks [["shell" "git" "diff" "--exit-code"] 10 | ["change" "version" "leiningen.release/bump-version" "release"] 11 | ["vcs" "commit"] 12 | ["vcs" "tag" "template-"] 13 | ["deploy"] 14 | ["change" "version" "leiningen.release/bump-version"] 15 | ["vcs" "commit"] 16 | ["vcs" "push"]] 17 | :vcs :git 18 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.8.0"] 19 | [org.clojure/tools.namespace "0.2.11"] 20 | [org.clojure/java.classpath "0.2.3"] 21 | [midje "1.8.3"]]}}) 22 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/project.clj: -------------------------------------------------------------------------------- 1 | (defproject {{raw-name}} "0.0.1-SNAPSHOT" 2 | :description "Example project based on zalando/friboo" 3 | :url "http://example.com/FIXME" 4 | :license {:name "The Apache License, Version 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0"} 6 | :min-lein-version "2.0.0" 7 | :dependencies [[org.clojure/clojure "1.8.0"] 8 | [org.zalando.stups/friboo "2.0.0-beta5"]] 9 | :main ^:skip-aot {{namespace}}.core 10 | :uberjar-name "{{name}}.jar" 11 | :target-path "target/%s" 12 | :manifest {"Implementation-Version" ~#(:version %)} 13 | :plugins [[lein-cloverage "1.0.9"] 14 | [lein-set-version "0.4.1"]] 15 | :aliases {"cloverage" ["with-profile" "test" "cloverage"]} 16 | :profiles {:uberjar {:aot :all} 17 | :dev {:repl-options {:init-ns user} 18 | :source-paths ["dev"] 19 | :dependencies [[org.clojure/tools.namespace "0.2.11"] 20 | [org.clojure/java.classpath "0.2.3"] 21 | [midje "1.8.3"]]}}) 22 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/simplegrid/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Crowd Favorite, Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.test-utils 2 | (:require [clojure.test :refer :all] 3 | [com.stuartsierra.component :as component]) 4 | (:import (java.net ServerSocket))) 5 | 6 | (defn track 7 | "Adds a tuple on call for an action." 8 | ([a action] 9 | (fn [& all-args] 10 | (swap! a conj {:key action 11 | :args (into [] all-args)})))) 12 | 13 | (defn throwing 14 | "Returns a function that throws with the provided arguments when executed" 15 | [& [msg data]] 16 | (fn [& _] 17 | (throw (ex-info 18 | (or msg "any exception") 19 | (or data {}))))) 20 | 21 | (defmacro same! 22 | [x y] 23 | `(is (= ~x ~y))) 24 | 25 | (defmacro true! 26 | [x] 27 | `(same! true ~x)) 28 | 29 | (defmacro false! 30 | [x] 31 | `(same! false ~x)) 32 | 33 | (defn get-free-port [] 34 | (let [sock (ServerSocket. 0)] 35 | (try 36 | (.getLocalPort sock) 37 | (finally 38 | (.close sock))))) 39 | 40 | (defmacro with-comp [[comp-sym comp-init] & body] 41 | `(let [~comp-sym (component/start ~comp-init)] 42 | (try 43 | ~@body 44 | (finally 45 | (component/stop ~comp-sym))))) 46 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # friboo-template 2 | 3 | A Leiningen template for Friboo library (https://github.com/zalando/friboo). 4 | 5 | This document only describes development guidelines. For usage please refer the the main README of Friboo. 6 | 7 | ## Development 8 | 9 | In order to try the template out without releasing to clojars, install it to the local `~/.m2` and specify `--snapshot` flag: 10 | 11 | ``` 12 | $ lein install 13 | $ cd ../.. 14 | $ lein new friboo --snapshot 15 | ``` 16 | 17 | ## Testing 18 | 19 | So far this is a manual step. 20 | 21 | ``` 22 | $ ./itest.sh 23 | ``` 24 | 25 | ## License 26 | 27 | Copyright © 2016 Zalando SE 28 | 29 | Licensed under the Apache License, Version 2.0 (the "License"); 30 | you may not use this file except in compliance with the License. 31 | You may obtain a copy of the License at 32 | 33 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 34 | 35 | Unless required by applicable law or agreed to in writing, software 36 | distributed under the License is distributed on an "AS IS" BASIS, 37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 | See the License for the specific language governing permissions and 39 | limitations under the License. 40 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/core.clj: -------------------------------------------------------------------------------- 1 | (ns {{namespace}}.core 2 | (:require [org.zalando.stups.friboo.config :as config] 3 | [org.zalando.stups.friboo.system :as system] 4 | [org.zalando.stups.friboo.system.http :as http] 5 | [org.zalando.stups.friboo.log :as log] 6 | [com.stuartsierra.component :as component] 7 | [{{namespace}}.api :as api]) 8 | (:gen-class)) 9 | 10 | (def default-http-config 11 | {:http-port 8080}) 12 | 13 | (defn run 14 | "Initializes and starts the whole system." 15 | [args-config] 16 | (let [config (config/load-config 17 | (merge default-http-config 18 | args-config) 19 | [:http :api]) 20 | system (component/map->SystemMap 21 | {:http (component/using 22 | (http/make-http "api.yaml" (:http config)) 23 | {:controller :api}) 24 | :api (component/using 25 | (api/map->Controller {:configuration (:api config)}) 26 | [])})] 27 | (system/run config system))) 28 | 29 | (defn -main 30 | "The actual main for our uberjar." 31 | [& args] 32 | (try 33 | (run {}) 34 | (catch Exception e 35 | (log/error e "Could not start the system because of %s." (str e)) 36 | (System/exit 1)))) 37 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/log.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.log 2 | (:require [clojure.tools.logging :as clojure-logging] 3 | [cheshire.core :as json] 4 | ; loads joda datetime json serialization 5 | [io.sarnowski.swagger1st.parser])) 6 | 7 | (defn format-value 8 | "Formats dynamic information for logs." 9 | [value] 10 | (str "[" (json/encode value) "]")) 11 | 12 | (defn format-values 13 | "Formats dynamic information for logs." 14 | [& values] 15 | (map format-value values)) 16 | 17 | (defmacro logf 18 | "Logs a message, formatted with clear distinction for dynamic values." 19 | [level exception message & more] 20 | `(when (clojure-logging/enabled? ~level) 21 | (let [more# (format-values ~@more) 22 | msg# (apply format ~message more#)] 23 | (clojure-logging/log ~level ~exception msg#)))) 24 | 25 | (defmacro trace [message & args] 26 | `(logf :trace nil ~message ~@args)) 27 | 28 | (defmacro debug [message & args] 29 | `(logf :debug nil ~message ~@args)) 30 | 31 | (defmacro info [message & args] 32 | `(logf :info nil ~message ~@args)) 33 | 34 | (defmacro warn [message & args] 35 | `(logf :warn nil ~message ~@args)) 36 | 37 | (defmacro error [exception message & args] 38 | `(logf :error ~exception ~message ~@args)) 39 | 40 | ; TODO for now audit goes to info as well 41 | (defmacro audit [message & args] 42 | `(logf :info nil ~message ~@args)) 43 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/simplegrid/986_grid.css: -------------------------------------------------------------------------------- 1 | /* SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) 2 | * http://simplegrid.info 3 | * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) 4 | * License: http://creativecommons.org/licenses/MIT/ */ 5 | 6 | /* Containers */ 7 | body { font-size: 100%; } 8 | .grid{ width:966px; } 9 | 10 | /* Slots Setup */ 11 | .slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:30px; } 12 | 13 | /* 6-Col Grid Sizes */ 14 | .slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:136px; } /* Sixths */ 15 | .slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:302px; } /* Thirds */ 16 | .slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:634px; } /* Two-Thirds */ 17 | .slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:800px; } /* Five-Sixths */ 18 | 19 | /* 4-Col Grid Sizes */ 20 | .slot-6,.slot-7,.slot-8,.slot-9{ width:219px; } /* Quarters */ 21 | .slot-6-7-8,.slot-7-8-9{ width:717px; } /* Three-Quarters */ 22 | 23 | /* 6-Col/4-Col Shared Grid Sizes */ 24 | .slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:468px; } /* Halves */ -------------------------------------------------------------------------------- /template/test/leiningen/new/friboo_test.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.new.friboo-test 2 | (:require [clojure.test :refer :all] 3 | [midje.sweet :refer :all] 4 | [leiningen.new.friboo :refer :all])) 5 | 6 | (deftest test-prepare-data 7 | (facts "" 8 | (prepare-data "foo") 9 | => (contains {:raw-name "foo" 10 | :name "foo" 11 | :namespace "foo" 12 | :nested-dirs "foo" 13 | :package "foo" 14 | :db-prefix "f"}) 15 | (prepare-data "foo-bar") 16 | => (contains {:raw-name "foo-bar" 17 | :name "foo-bar" 18 | :namespace "foo-bar" 19 | :package "foo_bar" 20 | :nested-dirs "foo_bar" 21 | :db-prefix "fb"}) 22 | (prepare-data "foo/bar") 23 | => (contains {:raw-name "foo/bar" 24 | :name "bar" 25 | :namespace "foo.bar" 26 | :package "foo.bar" 27 | :nested-dirs "foo/bar" 28 | :db-prefix "b"}) 29 | (prepare-data "foo.baz/aaa-bbb") 30 | => (contains {:raw-name "foo.baz/aaa-bbb" 31 | :db-prefix "ab" 32 | :name "aaa-bbb" 33 | :namespace "foo.baz.aaa-bbb" 34 | :package "foo.baz.aaa_bbb" 35 | :nested-dirs "foo/baz/aaa_bbb"})) 36 | ) 37 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/js/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Michael Bostock 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * The name Michael Bostock may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/api.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | 3 | # basic meta information 4 | info: 5 | title: {{name}} 6 | version: '1.0' 7 | description: Very nice {{name}} 8 | 9 | externalDocs: 10 | description: External docs 11 | url: http://example.github.io/ 12 | 13 | # technical configuration 14 | basePath: / 15 | produces: 16 | - application/json 17 | consumes: 18 | - application/json 19 | 20 | paths: 21 | 22 | '/': 23 | get: 24 | summary: Application root 25 | operationId: org.zalando.stups.friboo.system.http/redirect-to-swagger-ui 26 | responses: 27 | default: 28 | description: "Redirects to /ui/" 29 | 30 | '/hello/{name}': 31 | get: 32 | summary: Says hello 33 | description: | 34 | Says hello personally to {name} 35 | parameters: 36 | - $ref: '#/parameters/Name' 37 | tags: 38 | - General 39 | operationId: "{{namespace}}.api/get-hello" 40 | responses: 41 | 200: 42 | description: Hello is said 43 | default: 44 | $ref: '#/responses/Error' 45 | 46 | parameters: 47 | Name: 48 | name: name 49 | in: path 50 | type: string 51 | description: Name of the person to greet 52 | 53 | responses: 54 | Error: 55 | description: An error occured. 56 | schema: 57 | $ref: '#/definitions/Error' 58 | 59 | definitions: 60 | Error: 61 | type: object 62 | properties: 63 | message: 64 | type: string 65 | example: 66 | message: Internal Server Error 67 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/simplegrid/percentage_grid.css: -------------------------------------------------------------------------------- 1 | /* Extension of SimpleGrid by benjchristensen to allow percentage based sizing on very large displays 2 | * 3 | * SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) 4 | * http://simplegrid.info 5 | * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) 6 | * License: http://creativecommons.org/licenses/MIT/ */ 7 | 8 | /* Containers */ 9 | body { font-size: 1.125em; } 10 | .grid{ width:100%; } 11 | 12 | /* Slots Setup */ 13 | .slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:0px; } 14 | 15 | 16 | /* 6-Col Grid Sizes */ 17 | .slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:16.6%; } /* Sixths */ 18 | .slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:33.3%; } /* Thirds */ 19 | .slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:66.6%; } /* Two-Thirds */ 20 | .slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:83.3%; } /* Five-Sixths */ 21 | 22 | /* 4-Col Grid Sizes */ 23 | .slot-6,.slot-7,.slot-8,.slot-9{ width:25%; } /* Quarters */ 24 | .slot-6-7-8,.slot-7-8-9{ width:75%; } /* Three-Quarters */ 25 | 26 | /* 6-Col/4-Col Shared Grid Sizes */ 27 | .slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:50%; } /* Halves */ -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixThreadPool/templates/hystrixThreadPool.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 7 | 10 | 11 |
12 | 13 |
14 |
Active
15 |
<%= currentActiveCount%>
16 | 17 |
Max Active
18 |
<%= addCommas(rollingMaxActiveThreads)%>
19 |
20 | 21 |
22 |
Queued
23 |
<%= currentQueueSize %>
24 |
Executions
25 |
<%= addCommas(rollingCountThreadsExecuted)%>
26 |
27 |
28 |
Pool Size
29 |
<%= currentPoolSize %>
30 |
Queue Size
31 |
<%= propertyValue_queueSizeRejectionThreshold %>
32 |
33 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/js/tmpl.js: -------------------------------------------------------------------------------- 1 | 2 | //Simple JavaScript Templating 3 | //John Resig - http://ejohn.org/ - MIT Licensed 4 | // http://ejohn.org/blog/javascript-micro-templating/ 5 | (function(window, undefined) { 6 | var cache = {}; 7 | 8 | window.tmpl = function tmpl(str, data) { 9 | try { 10 | // Figure out if we're getting a template, or if we need to 11 | // load the template - and be sure to cache the result. 12 | var fn = !/\W/.test(str) ? 13 | cache[str] = cache[str] || 14 | tmpl(document.getElementById(str).innerHTML) : 15 | 16 | // Generate a reusable function that will serve as a template 17 | // generator (and which will be cached). 18 | new Function("obj", 19 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 20 | 21 | // Introduce the data as local variables using with(){} 22 | "with(obj){p.push('" + 23 | 24 | // Convert the template into pure JavaScript 25 | str 26 | .replace(/[\r\t\n]/g, " ") 27 | .split("<%").join("\t") 28 | .replace(/((^|%>)[^\t]*)'/g, "$1\r") 29 | .replace(/\t=(.*?)%>/g, "',$1,'") 30 | .split("\t").join("');") 31 | .split("%>").join("p.push('") 32 | .split("\r").join("\\'") 33 | + "');}return p.join('');"); 34 | 35 | //console.log(fn); 36 | 37 | // Provide some basic currying to the user 38 | return data ? fn(data) : fn; 39 | }catch(e) { 40 | console.log(e); 41 | } 42 | }; 43 | })(window); 44 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | <% 4 | var displayName = name; 5 | var toolTip = ""; 6 | if(displayName.length > 32) { 7 | displayName = displayName.substring(0,4) + "..." + displayName.substring(displayName.length-20, displayName.length); 8 | toolTip = "title=\"" + name + "\""; 9 | } 10 | %> 11 | 12 |
13 |

><%= displayName %>

14 |
15 |
16 |
17 | 18 | 19 | 33 | 34 |
35 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/global.css: -------------------------------------------------------------------------------- 1 | @IMPORT url("resets.css"); 2 | 3 | body { 4 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 5 | } 6 | 7 | img, object, embed { 8 | max-width: 100%; 9 | } 10 | 11 | img { 12 | height: auto; 13 | } 14 | 15 | 16 | #header { 17 | background: #FFFFFF url(../images/hystrix-logo-tagline-tiny.png) no-repeat scroll 99% 0%; 18 | height: 65px; 19 | margin-bottom: 5px; 20 | } 21 | 22 | #header h2 { 23 | float:left; 24 | color: black; 25 | position:relative; 26 | padding-left: 20px; 27 | top: 26px; 28 | font-size: 20px; 29 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 30 | } 31 | 32 | #header .header_nav { 33 | position:absolute; 34 | top:48px; 35 | right:15px; 36 | } 37 | 38 | #header .header_links { 39 | float:left; 40 | color: lightgray; 41 | font-size: 18px; 42 | top: 3px; 43 | padding-left: 10px; 44 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 45 | } 46 | 47 | #header .header_links a { 48 | color: white; 49 | } 50 | 51 | #header .header_clusters { 52 | float:left; 53 | position:relative; 54 | padding-left: 10px; 55 | top: -1px; 56 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 57 | } 58 | 59 | 60 | @media screen and (min-width: 1500px) { 61 | 62 | #header .header_nav { 63 | top:13px; 64 | right:130px; 65 | } 66 | 67 | #header { 68 | background: #FFFFFF url(../images/hystrix-logo-tagline-tiny.png) no-repeat scroll 99% 50%; 69 | height: 65px; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/system.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system 2 | (:require [com.stuartsierra.component :as component] 3 | [org.zalando.stups.friboo.log :as log]) 4 | (:import (org.apache.logging.log4j LogManager Level) 5 | (org.apache.logging.log4j.core LoggerContext) 6 | (org.apache.logging.log4j.core.config Configuration LoggerConfig) 7 | (clojure.lang ExceptionInfo))) 8 | 9 | (defn set-log-level! 10 | "Changes the log level of the log4j2 root logger." 11 | [level & {:keys [logger-name] 12 | :or {logger-name LogManager/ROOT_LOGGER_NAME}}] 13 | (let [^Level level (Level/getLevel level) 14 | ^LoggerContext ctx (LogManager/getContext false) 15 | ^Configuration config (.getConfiguration ctx) 16 | ^LoggerConfig logger (.getLoggerConfig config logger-name)] 17 | (.setLevel logger level) 18 | (.updateLoggers ctx config))) 19 | 20 | (def stups-logger-name "org.zalando.stups") 21 | 22 | ;; TODO This is mostly about setting logging levels, can better be done by a separate function 23 | (defn run 24 | "Boots a whole new system." 25 | [{system-config :system} system] 26 | (log/info "Starting system...") 27 | 28 | (if-let [stups-log-level (:stups-log-level system-config)] 29 | (do 30 | (log/warn "Setting %s log level to %s." stups-logger-name stups-log-level) 31 | (set-log-level! stups-log-level :logger-name stups-logger-name))) 32 | 33 | (if-let [log-level (:log-level system-config)] 34 | (do 35 | (log/warn "Setting log level to %s." log-level) 36 | (set-log-level! log-level))) 37 | 38 | (try 39 | (let [system (component/start system)] 40 | (log/info "System started.") 41 | system) 42 | (catch ExceptionInfo e 43 | (when-let [{:as exd :keys [system]} (ex-data e)] 44 | (component/stop system)) 45 | (throw e)))) 46 | -------------------------------------------------------------------------------- /template/src/leiningen/new/friboo.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.new.friboo 2 | (:require [leiningen.new.templates :refer [renderer year date project-name 3 | ->files sanitize-ns name-to-path 4 | multi-segment sanitize]] 5 | [leiningen.core.main :as main] 6 | [clojure.string :as str])) 7 | 8 | (defn db-prefix [name] 9 | (->> (str/split name #"(-|_)") 10 | (map first) 11 | (apply str))) 12 | 13 | (defn prepare-data [name] 14 | (let [namespace (sanitize-ns name)] 15 | {:raw-name name 16 | :name (project-name name) 17 | :namespace namespace 18 | :package (sanitize namespace) 19 | :nested-dirs (name-to-path namespace) 20 | :db-prefix (db-prefix (project-name name)) 21 | :year (year) 22 | :date (date)})) 23 | 24 | (defn friboo 25 | "A Friboo project template" 26 | [name] 27 | (let [render (renderer "friboo") 28 | data (prepare-data name)] 29 | (main/debug "Template data:" data) 30 | (main/info "Generating a project called" name "based on the 'friboo' template.") 31 | (->files data 32 | ["project.clj" (render "project.clj" data)] 33 | ["README.md" (render "README.md" data)] 34 | [".gitignore" (render "gitignore" data)] 35 | ["dev-config.edn" (render "dev-config.edn" data)] 36 | ["resources/api.yaml" (render "api.yaml" data)] 37 | ["dev/user.clj" (render "user.clj" data)] 38 | ["src/{{nested-dirs}}/api.clj" (render "api.clj" data)] 39 | ["src/{{nested-dirs}}/core.clj" (render "core.clj" data)] 40 | ["test/{{nested-dirs}}/core_test.clj" (render "core_test.clj" data)] 41 | ["test/{{nested-dirs}}/api_test.clj" (render "api_test.clj" data)] 42 | "resources"))) 43 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/js/jquery.tinysort.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery TinySort - A plugin to sort child nodes by (sub) contents or attributes. 3 | * 4 | * Version: 1.0.5 5 | * 6 | * Copyright (c) 2008-2011 Ron Valstar http://www.sjeiti.com/ 7 | * 8 | * Dual licensed under the MIT and GPL licenses: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * http://www.gnu.org/licenses/gpl.html 11 | */ 12 | (function(b){b.tinysort={id:"TinySort",version:"1.0.5",copyright:"Copyright (c) 2008-2011 Ron Valstar",uri:"http://tinysort.sjeiti.com/",defaults:{order:"asc",attr:"",place:"start",returns:false,useVal:false}};b.fn.extend({tinysort:function(h,j){if(h&&typeof(h)!="string"){j=h;h=null}var e=b.extend({},b.tinysort.defaults,j);var p={};this.each(function(t){var v=(!h||h=="")?b(this):b(this).find(h);var u=e.order=="rand"?""+Math.random():(e.attr==""?(e.useVal?v.val():v.text()):v.attr(e.attr));var s=b(this).parent();if(!p[s]){p[s]={s:[],n:[]}}if(v.length>0){p[s].s.push({s:u,e:b(this),n:t})}else{p[s].n.push({e:b(this),n:t})}});for(var g in p){var d=p[g];d.s.sort(function k(t,s){var i=t.s.toLowerCase?t.s.toLowerCase():t.s;var u=s.s.toLowerCase?s.s.toLowerCase():s.s;if(c(t.s)&&c(s.s)){i=parseFloat(t.s);u=parseFloat(s.s)}return(e.order=="asc"?1:-1)*(iu?1:0))})}var m=[];for(var g in p){var d=p[g];var n=[];var f=b(this).length;switch(e.place){case"first":b.each(d.s,function(s,t){f=Math.min(f,t.n)});break;case"org":b.each(d.s,function(s,t){n.push(t.n)});break;case"end":f=d.n.length;break;default:f=0}var q=[0,0];for(var l=0;l=f&&l0?d[1]:false}function a(e,f){var d=false;b.each(e,function(h,g){if(!d){d=g==f}});return d}b.fn.TinySort=b.fn.Tinysort=b.fn.tsort=b.fn.tinysort})(jQuery); -------------------------------------------------------------------------------- /resources/hystrix-dashboard/monitor/monitor.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding-left: 20px; 3 | padding-right: 20px; 4 | } 5 | 6 | .row { 7 | width: 100%; 8 | margin: 0 auto; 9 | overflow: hidden; 10 | } 11 | 12 | .spacer { 13 | width: 100%; 14 | margin: 0 auto; 15 | padding-top:4px; 16 | clear:both; 17 | } 18 | 19 | 20 | .last { 21 | margin-right: 0px; 22 | } 23 | 24 | .menubar { 25 | overflow: hidden; 26 | border-bottom: 1px solid black; 27 | } 28 | 29 | .menubar div { 30 | padding-bottom:5px; 31 | 32 | margin: 0 auto; 33 | overflow: hidden; 34 | 35 | font-size: 80%; 36 | font-family:'Bookman Old Style',Bookman,'URW Bookman L','Palatino Linotype',serif; 37 | 38 | float:left; 39 | } 40 | 41 | .menubar .title { 42 | float: left; 43 | padding-right: 20px; 44 | 45 | font-size: 110%; 46 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 47 | font-weight: bold; 48 | 49 | vertical-align: bottom; 50 | } 51 | 52 | .menubar .menu_actions { 53 | float: left; 54 | position:relative; 55 | top: 4px; 56 | } 57 | 58 | .menubar .menu_legend { 59 | float: right; 60 | position:relative; 61 | top: 4px; 62 | 63 | } 64 | 65 | h3.sectionHeader { 66 | color: black; 67 | font-size: 110%; 68 | padding-top: 4px; 69 | padding-bottom: 4px; 70 | padding-left: 8px; 71 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 72 | background: lightgrey; 73 | } 74 | 75 | .success { 76 | color: green; 77 | } 78 | 79 | .shortCircuited { 80 | color: blue; 81 | } 82 | 83 | .timeout { 84 | color: #FF9900; /* shade of orange */ 85 | } 86 | 87 | .failure { 88 | color: red; 89 | } 90 | 91 | .rejected { 92 | color: purple; 93 | } 94 | 95 | .exceptionsThrown { 96 | color: brown; 97 | } 98 | 99 | .badRequest { 100 | color: lightSeaGreen; 101 | } 102 | 103 | @media screen and (max-width: 1100px) { 104 | .container { 105 | padding-left: 5px; 106 | padding-right: 5px; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /template/resources/leiningen/new/friboo/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Tools for interactive development with the REPL. This file should 3 | not be included in a production build of the application." 4 | (:require [clojure.java.javadoc :refer [javadoc]] 5 | [clojure.pprint :refer [pprint]] 6 | [clojure.reflect :refer [reflect]] 7 | [clojure.repl :refer [apropos dir doc find-doc pst source]] 8 | [clojure.tools.namespace.repl :refer [refresh refresh-all]] 9 | [com.stuartsierra.component :as component] 10 | [clojure.test :refer [run-all-tests]] 11 | [{{namespace}}.core :as core] 12 | [org.zalando.stups.friboo.system :as system] 13 | [org.zalando.stups.friboo.dev :as dev])) 14 | 15 | ;; A Var containing an object representing the application under development. 16 | (defonce system nil) 17 | 18 | (defn start 19 | "Starts the system running, sets the Var #'system." 20 | [extra-config] 21 | (dev/reload-log4j2-config) 22 | (#'system/set-log-level! "DEBUG" :logger-name "{{package}}") 23 | (#'system/set-log-level! "DEBUG" :logger-name "org.zalando.stups") 24 | (alter-var-root #'system (constantly (core/run (merge {:system-log-level "INFO"} 25 | (dev/load-dev-config "./dev-config.edn") 26 | extra-config))))) 27 | 28 | (defn stop 29 | "Stops the system if it is currently running, updates the Var 30 | #'system." 31 | [] 32 | (alter-var-root #'system 33 | (fn [s] (when s (component/stop s))))) 34 | 35 | (defn go 36 | "Initializes and starts the system running." 37 | ([extra-config] 38 | (start extra-config) 39 | :ready) 40 | ([] 41 | (go {}))) 42 | 43 | (defn reset 44 | "Stops the system, reloads modified source files, and restarts it." 45 | [] 46 | (stop) 47 | (refresh :after 'user/go)) 48 | 49 | (defn run-tests [] 50 | (run-all-tests #"{{namespace}}.*-test")) 51 | 52 | (defn tests 53 | "Stops the system, reloads modified source files and runs tests" 54 | [] 55 | (stop) 56 | (refresh :after 'user/run-tests)) 57 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/config_decrypt.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.config-decrypt 2 | (:require [org.zalando.stups.friboo.log :as log] 3 | [amazonica.aws.kms :as kms] 4 | [clojure.data.codec.base64 :as b64]) 5 | (:import (java.nio ByteBuffer))) 6 | 7 | (def aws-kms-prefix "aws:kms:") 8 | 9 | (defn- get-kms-ciphertext-blob [s] 10 | "Convert config string to ByteBuffer" 11 | (-> s 12 | (clojure.string/replace-first aws-kms-prefix "") 13 | .getBytes 14 | b64/decode 15 | ByteBuffer/wrap)) 16 | 17 | (defn decrypt-value-with-aws-kms [value aws-region-id] 18 | "Use AWS Key Management Service to decrypt the given string (must be encoded as Base64)" 19 | (->> value 20 | get-kms-ciphertext-blob 21 | (#(kms/decrypt {:endpoint aws-region-id} {:ciphertext-blob %})) 22 | :plaintext 23 | .array 24 | (map char) 25 | (apply str))) 26 | 27 | (defn- decrypt-value [key value aws-region-id] 28 | "Decrypt a single value, returns original value if it's not encrypted" 29 | (if (and (string? value) (.startsWith value aws-kms-prefix)) 30 | (do 31 | (log/info "Decrypting configuration %s." key) 32 | (decrypt-value-with-aws-kms value aws-region-id)) 33 | value)) 34 | 35 | (defn- to-real-boolean 36 | "Maps a boolean string to boolean or returns the string." 37 | [value] 38 | (if (string? value) 39 | (case value 40 | "true" true 41 | "false" false 42 | value) 43 | value)) 44 | 45 | (defn- to-real-number 46 | "Maps a number string to long or returns the string." 47 | [value] 48 | (if (and (string? value) (not (nil? (re-matches #"^[0-9]+$" value)))) 49 | (Long/parseLong value) 50 | value)) 51 | 52 | (defn decrypt [config] 53 | "Decrypt all values in a config map" 54 | (into {} (for [[k v] config] 55 | [k (-> (decrypt-value k v (:aws-region-id config)) 56 | (to-real-boolean) 57 | (to-real-number))]))) 58 | 59 | (defn decrypt-config 60 | "Decrypts 2-level config map." 61 | [config-map] 62 | (into {} (for [[config-ns subconfig] config-map] 63 | [config-ns (decrypt subconfig)]))) 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.0.0 (date TBD) 4 | 5 | ### Breaking changes 6 | 7 | - `org.zalando.stups.friboo.system.http/def-http-component` is removed in favor of `org.zalando.stups.friboo.system.http/make-http`. For the old Zalando-specific functionality use `org.zalando.stups.friboo.zalando-internal.system.http/make-zalando-http` from [friboo-ext-zalando]. 8 | - Notion of controller is introduced to decouple Http serving and API handling. Controller is a component that contains all the dependencies that API handlers might need access to - such as database, metrics, authorizer etc. API handlers 9 | by default are called with `[controller params request]` now, this can be customized through `{:s1st-options {:executor }}`. `system_test.clj` contains an example. 10 | - `org.zalando.stups.friboo.system.db/def-db-component` is removed. One should use `org.zalando.stups.friboo.system.db/map->DB` to create DB component, `:auto-migration?` is provided as part of normal config. 11 | - `org.zalando.stups.friboo.config/load-configuration` is broken down into `org.zalando.stups.friboo.config/load-config` and `org.zalando.stups.friboo.zalando-internal.config/load-config`, signature changed. 12 | - `org.zalando.stups.friboo.system.db/generate-hystrix-commands` now by default does not use any nonfatal exception detection, every exception will cause circuit breaking. For the old behavior use `(generate-hystrix-commands :ignore-exception-fn? org.zalando.stups.friboo.zalando-internal.system.db/ignore-nonfatal-psqlexception)` from [friboo-ext-zalando]. 13 | 14 | [friboo-ext-zalando]: https://github.com/zalando-incubator/friboo-ext-zalando 15 | 16 | Things moved to [friboo-ext-zalando] and broken there: 17 | 18 | - `org.zalando.stups.friboo.zalando-internal.config` `HTTP_TOKENINFO_URL` environment variable is ignored now, `TOKENINFO_URL` and `GLOBAL_TOKENINFO_URL` are used to enable OAuth 2.0 access token checking in `Http` component created by `make-zalando-http`. 19 | - `org.zalando.stups.friboo.auth` namespace is renamed to `org.zalando.stups.friboo.zalando-specific.auth`, `Authorizer` component is introduced, 20 | - `fetch-auth` and `get-auth` functions changed signatures. `HTTP_MAGNIFICENT_URL` should be replaced with `AUTH_MAGNIFICENT_URL`. 21 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/system_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system-test 2 | (:require [clojure.test :refer :all] 3 | [midje.sweet :refer :all] 4 | [org.zalando.stups.friboo.config :as config] 5 | [org.zalando.stups.friboo.system.http :as http] 6 | [com.stuartsierra.component :as component] 7 | [org.zalando.stups.friboo.test-utils :as u] 8 | [org.zalando.stups.friboo.system :as system]) 9 | (:import (clojure.lang ExceptionInfo))) 10 | 11 | (defn simple-run 12 | "Initializes and starts the whole system." 13 | [args-configuration] 14 | (let [configuration (config/load-config 15 | (merge {} 16 | args-configuration) 17 | [:http]) 18 | system (component/map->SystemMap 19 | {:http (http/make-http "org/zalando/stups/friboo/system/http.yml" (:http configuration))})] 20 | (component/start system))) 21 | 22 | (defrecord FakeComponent [throw-error? counter initialized] 23 | component/Lifecycle 24 | (start [this] 25 | (when throw-error? 26 | (throw (ex-info "error" {}))) 27 | (swap! counter inc) 28 | (assoc this :initialized true)) 29 | (stop [this] 30 | (when initialized 31 | (swap! counter dec)) 32 | (assoc this :initialized false))) 33 | 34 | (defn error-run [counter] 35 | (let [system (component/map->SystemMap 36 | {:comp1 (map->FakeComponent {:counter counter}) 37 | :comp2 (component/using 38 | (map->FakeComponent {:counter counter :throw-error? true}) 39 | [:comp1])})] 40 | (system/run {:system {:stups-log-level "INFO" :log-level "INFO"}} system))) 41 | 42 | (deftest works 43 | 44 | (let [system (simple-run {:http-port (u/get-free-port)})] 45 | (try 46 | (fact "After the system starts, httpd is set" 47 | (-> system :http :httpd) => some?) 48 | (finally 49 | (component/stop system)))) 50 | 51 | (facts "when one of the components fails to start, the system is shut down" 52 | (let [counter (atom 0)] 53 | (fact "system throws an error" 54 | (error-run counter)) => (throws ExceptionInfo) 55 | (fact "counter should be 0" 56 | @counter => 0))) 57 | 58 | ) 59 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/resets.css: -------------------------------------------------------------------------------- 1 | /* 2 | html5doctor.com Reset Stylesheet 3 | v1.6.1 4 | Last Updated: 2010-09-17 5 | Author: Richard Clark - http://richclarkdesign.com 6 | Twitter: @rich_clark 7 | */ 8 | 9 | html, body, div, span, object, iframe, 10 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 11 | abbr, address, cite, code, 12 | del, dfn, em, img, ins, kbd, q, samp, 13 | small, strong, sub, sup, var, 14 | b, i, 15 | dl, dt, dd, ol, ul, li, 16 | fieldset, form, label, legend, 17 | table, caption, tbody, tfoot, thead, tr, th, td, 18 | article, aside, canvas, details, figcaption, figure, 19 | footer, header, hgroup, menu, nav, section, summary, 20 | time, mark, audio, video { 21 | margin:0; 22 | padding:0; 23 | border:0; 24 | outline:0; 25 | font-size:100%; 26 | vertical-align:baseline; 27 | background:transparent; 28 | } 29 | 30 | body { 31 | line-height:1; 32 | } 33 | 34 | article,aside,details,figcaption,figure, 35 | footer,header,hgroup,menu,nav,section { 36 | display:block; 37 | } 38 | 39 | nav ul { 40 | list-style:none; 41 | } 42 | 43 | blockquote, q { 44 | quotes:none; 45 | } 46 | 47 | blockquote:before, blockquote:after, 48 | q:before, q:after { 49 | content:''; 50 | content:none; 51 | } 52 | 53 | a { 54 | margin:0; 55 | padding:0; 56 | font-size:100%; 57 | vertical-align:baseline; 58 | background:transparent; 59 | } 60 | 61 | /* change colours to suit your needs */ 62 | ins { 63 | background-color:#ff9; 64 | color:#000; 65 | text-decoration:none; 66 | } 67 | 68 | /* change colours to suit your needs */ 69 | mark { 70 | background-color:#ff9; 71 | color:#000; 72 | font-style:italic; 73 | font-weight:bold; 74 | } 75 | 76 | del { 77 | text-decoration: line-through; 78 | } 79 | 80 | abbr[title], dfn[title] { 81 | border-bottom:1px dotted; 82 | cursor:help; 83 | } 84 | 85 | table { 86 | border-collapse:collapse; 87 | border-spacing:0; 88 | } 89 | 90 | /* change border colour to suit your needs */ 91 | hr { 92 | display:block; 93 | height:1px; 94 | border:0; 95 | border-top:1px solid #cccccc; 96 | margin:1em 0; 97 | padding:0; 98 | } 99 | 100 | input, select { 101 | vertical-align:middle; 102 | } -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixCommand/templates/hystrixCircuitContainer.html: -------------------------------------------------------------------------------- 1 |
2 | <% 3 | var displayName = name; 4 | var toolTip = ""; 5 | if(displayName.length > 32) { 6 | displayName = displayName.substring(0,4) + "..." + displayName.substring(displayName.length-20, displayName.length); 7 | toolTip = "title=\"" + name + "\""; 8 | } 9 | %> 10 | 11 |
12 |
13 | <% if(includeDetailIcon) { %> 14 |

style="padding-right:16px"> 15 | <%= displayName %> 16 | 17 |

18 | <% } else { %> 19 |

><%= displayName %>

20 | <% } %> 21 |
22 |
23 |
24 |
25 |
26 | 27 | 40 |
41 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/config_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.config-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [midje.sweet :refer :all] 5 | [org.zalando.stups.friboo.config :refer :all] 6 | [org.zalando.stups.friboo.config-decrypt :refer :all] 7 | [amazonica.aws.kms :as kms] 8 | [environ.core :as environ]) 9 | (:import [java.nio ByteBuffer])) 10 | 11 | (deftest test-config-parse 12 | (is (= {:connect-timeout "123"} (:ldap (parse-namespaces {:ldap-connect-timeout "123"} [:ldap])))) 13 | (is (= {:connect-timeout "123"} (:ldap (parse-namespaces {:ldap-connect-timeout "123" :ldapfoo "bar"} [:ldap]))))) 14 | 15 | (deftest test-mask 16 | (is (= {:a "b" :password "MASKED" :private-stuff "MASKED" :my-secret-key "MASKED"} 17 | (mask {:a "b", :password "secret" :private-stuff "foobar" :my-secret-key "key"})))) 18 | 19 | (deftest test-decrypt 20 | (is (= {:a "a" :b "b"} (decrypt {:a "a" :b "b"})))) 21 | 22 | (deftest test-decrypt-value-with-aws-kms 23 | (with-redefs [kms/decrypt (constantly {:plaintext (-> "secret" 24 | .getBytes 25 | ByteBuffer/wrap)})] 26 | (is (= "secret" (decrypt-value-with-aws-kms "abc" "region-1"))))) 27 | 28 | (deftest wrap-midje-facts 29 | 30 | (facts "about load-config" 31 | 32 | (fact "If TOKENINFO_URL is not set, no remapping is done" 33 | (with-redefs [environ/env {}] 34 | (load-config nil [:http] {:mapping {:http-tokeninfo-url :tokeninfo-url}})) 35 | => {:system {} 36 | :http {}}) 37 | 38 | (fact "TOKENINFO_URL is duplicated as HTTP_TOKENINFO_URL" 39 | (with-redefs [environ/env {:tokeninfo-url ..tokeninfo-url..}] 40 | (load-config nil [:http] {:mapping {:http-tokeninfo-url :tokeninfo-url}})) 41 | => {:system {} 42 | :http {:tokeninfo-url ..tokeninfo-url..}}) 43 | 44 | (fact "User's HTTP_TOKENINFO_URL takes precedence over the duplication" 45 | (with-redefs [environ/env {:tokeninfo-url ..tokeninfo-url.. 46 | :http-tokeninfo-url ..http-tokeninfo-url..}] 47 | (load-config nil [:http] {:mapping {:http-tokeninfo-url :tokeninfo-url}})) 48 | => {:system {} 49 | :http {:tokeninfo-url ..http-tokeninfo-url..}}) 50 | ) 51 | 52 | (facts "about require-config" 53 | (require-config {:foo "bar"} :baz) => (throws IllegalArgumentException)) 54 | 55 | ) 56 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hystrix Dashboard 6 | 7 | 8 | 9 | 10 | 37 | 38 | 39 |
40 | 41 |
42 | 43 |
44 |
45 | 46 |

Hystrix Dashboard

47 | 48 |

49 | Cluster via Turbine (default cluster): http://turbine-hostname:port/turbine.stream 50 |
51 | Cluster via Turbine (custom cluster): http://turbine-hostname:port/turbine.stream?cluster=[clusterName] 52 |
53 | Single Hystrix App: http://hystrix-app:port/hystrix.stream 54 |

55 | Delay: ms 56 |      57 | Title:

58 | Authorization:
59 |
60 | 61 |

62 |
63 | 64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/system/http.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | 3 | # basic meta information 4 | info: 5 | title: friboo-frontend 6 | version: '1.0' 7 | description: Very nice friboo-frontend 8 | 9 | externalDocs: 10 | description: External docs 11 | url: http://example.github.io/ 12 | 13 | # technical configuration 14 | basePath: / 15 | produces: 16 | - application/json 17 | consumes: 18 | - application/json 19 | 20 | security: 21 | - oauth2: [uid] 22 | 23 | paths: 24 | 25 | '/': 26 | get: 27 | summary: Application root 28 | operationId: org.zalando.stups.friboo.system.http/redirect-to-swagger-ui 29 | responses: 30 | default: 31 | description: "Redirects to /ui/" 32 | 33 | '/info': 34 | get: 35 | summary: Gives info 36 | description: Gives info 37 | operationId: "org.zalando.stups.friboo.system.http-test/get-info" 38 | responses: 39 | 200: 40 | description: Info is given 41 | default: 42 | $ref: '#/responses/Error' 43 | 44 | '/foo/{foo_id}': 45 | put: 46 | summary: Updates a foo 47 | description: Updates a foo 48 | operationId: "org.zalando.stups.friboo.system.http-test/put-foo" 49 | parameters: 50 | - name: foo_id 51 | in: path 52 | type: string 53 | - name: page 54 | in: query 55 | type: integer 56 | - name: foo 57 | in: body 58 | schema: 59 | type: object 60 | properties: 61 | foo_text: 62 | type: string 63 | responses: 64 | 201: 65 | description: Foo is updated 66 | default: 67 | $ref: '#/responses/Error' 68 | 69 | # '/error': 70 | # get: 71 | # summary: Throws an error 72 | # description: Throws an error 73 | # operationId: "org.zalando.stups.friboo.system.http-test/get-error" 74 | # responses: 75 | # 200: 76 | # description: Never happens 77 | # default: 78 | # $ref: '#/responses/Error' 79 | 80 | responses: 81 | Error: 82 | description: An error occured. 83 | schema: 84 | $ref: '#/definitions/Error' 85 | 86 | definitions: 87 | Error: 88 | type: object 89 | properties: 90 | message: 91 | type: string 92 | example: 93 | message: Internal Server Error 94 | 95 | securityDefinitions: 96 | oauth2: 97 | type: oauth2 98 | flow: implicit 99 | authorizationUrl: https://example.com/oauth2/dialog 100 | scopes: 101 | uid: Unique identifier of the user accessing the service. 102 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/css/simplegrid/720_grid.css: -------------------------------------------------------------------------------- 1 | /* SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid) 2 | * http://simplegrid.info 3 | * by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com) 4 | * License: http://creativecommons.org/licenses/MIT/ */ 5 | 6 | /* Containers */ 7 | body { font-size: 0.875em; padding: 0; } 8 | .grid{ margin:0 auto; padding: 0 10px; width:700px; } 9 | .row{ clear:left; } 10 | 11 | /* Slots Setup */ 12 | .slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:20px; } 13 | 14 | /* 6-Col Grid Sizes */ 15 | .slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:100px; } /* Sixths */ 16 | .slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:220px; } /* Thirds */ 17 | .slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:460px; } /* Two-Thirds */ 18 | .slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:580px; } /* Five-Sixths */ 19 | 20 | /* 4-Col Grid Sizes */ 21 | .slot-6,.slot-7,.slot-8,.slot-9{ width:160px; } /* Quarters */ 22 | .slot-6-7-8,.slot-7-8-9{ width:520px; } /* Three-Quarters */ 23 | 24 | /* 6-Col/4-Col Shared Grid Sizes */ 25 | .slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:340px; } /* Halves */ 26 | .slot-0-1-2-3-4-5, .slot-6-7-8-9{ width: 100%; } /* Full-Width */ 27 | 28 | /* Zeroing Out Leftmost Slot Margins */ 29 | .slot-0,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-6,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-1 .slot-1,.slot-1-2 .slot-1,.slot-1-2 .slot-1-2,.slot-1-2-3 .slot-1,.slot-1-2-3 .slot-1-2,.slot-1-2-3 .slot-1-2-3,.slot-1-2-3-4 .slot-1,.slot-1-2-3-4 .slot-1-2,.slot-1-2-3-4 .slot-1-2-3,.slot-1-2-3-4 .slot-1-2-3-4,.slot-1-2-3-4-5 .slot-1,.slot-1-2-3-4-5 .slot-1-2,.slot-1-2-3-4-5 .slot-1-2-3,.slot-1-2-3-4-5 .slot-1-2-3-4,.slot-1-2-3-4-5 .slot-1-2-3-4-5,.slot-2 .slot-2,.slot-2-3 .slot-2,.slot-2-3 .slot-2-3,.slot-2-3-4 .slot-2,.slot-2-3-4 .slot-2-3,.slot-2-3-4 .slot-2-3-4,.slot-2-3-4-5 .slot-2,.slot-2-3-4-5 .slot-2-3,.slot-2-3-4-5 .slot-2-3-4,.slot-2-3-4-5 .slot-2-3-4-5,.slot-3 .slot-3,.slot-3-4 .slot-3,.slot-3-4 .slot-3-4,.slot-3-4-5 .slot-3,.slot-3-4-5 .slot-3-4,.slot-3-4-5 .slot-3-4-5,.slot-4 .slot-4,.slot-4-5 .slot-4,.slot-4-5 .slot-4-5,.slot-5 .slot-5,.slot-7 .slot-7,.slot-7-8 .slot-7,.slot-7-8 .slot-7-8,.slot-7-8-9 .slot-7,.slot-7-8-9 .slot-7-8,.slot-7-8-9 .slot-7-8-9,.slot-8 .slot-8,.slot-8-9 .slot-8,.slot-8-9 .slot-8-9{ margin-left:0 !important; } /* Important is to avoid repeating this in larger screen css files */ 30 | 31 | /* Row Clearfix */ 32 | .row:after{ visibility:hidden; display:block; font-size:0; content:" "; clear:both; height:0; } 33 | .row{ zoom:1; } -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/system/mgmt_http.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.mgmt-http 2 | (:require [com.stuartsierra.component :refer [Lifecycle]] 3 | [org.zalando.stups.friboo.log :as log] 4 | [org.zalando.stups.friboo.system.metrics :refer [add-metrics-servlet add-metrics-filter]] 5 | [ring.util.response :as r] 6 | [ring.adapter.jetty :as jetty] 7 | [ring.util.servlet :as servlet] 8 | [ring.middleware.resource :refer [wrap-resource]] 9 | [ring.middleware.content-type :refer [wrap-content-type]] 10 | [ring.middleware.not-modified :refer [wrap-not-modified]]) 11 | (:import (org.eclipse.jetty.servlet ServletHolder ServletContextHandler) 12 | (com.netflix.hystrix.contrib.metrics.eventstream HystrixMetricsStreamServlet))) 13 | 14 | (def hystrix-dashboard-handler 15 | (-> (constantly (r/redirect "/monitor/monitor.html")) 16 | (wrap-resource "hystrix-dashboard") 17 | (wrap-content-type) 18 | (wrap-not-modified))) 19 | 20 | (defn- add-hystrix-servlet 21 | [context] 22 | (.addServlet context (ServletHolder. HystrixMetricsStreamServlet) "/hystrix.stream") 23 | (.addServlet context (ServletHolder. (servlet/servlet hystrix-dashboard-handler)) "/") 24 | context) 25 | 26 | (defn- mgmt-jetty-configurator 27 | [metrics] 28 | (fn [server] (-> (ServletContextHandler. server "/" ServletContextHandler/NO_SESSIONS) 29 | (add-hystrix-servlet) 30 | (add-metrics-servlet metrics) 31 | (add-metrics-filter metrics)))) 32 | 33 | (defn run-mgmt-jetty 34 | "Starts Jetty with Hystrix event stream servlet" 35 | [metrics options] 36 | (jetty/run-jetty (constantly nil) (assoc options :configurator (mgmt-jetty-configurator metrics)))) 37 | 38 | 39 | (defn running? [component] 40 | (:mgmt-httpd component)) 41 | 42 | (defrecord MgmtHTTP [configuration metrics] 43 | Lifecycle 44 | 45 | (start [component] 46 | (if (running? component) 47 | ; then 48 | (do 49 | (log/info "Management HTTP server already running.") 50 | component) 51 | ; else 52 | (if (:no-listen? configuration) 53 | (do 54 | (log/info "Skip creation of management HTTP server. 'no-listen?' property found") 55 | component) 56 | (let [port (:port configuration 7979) 57 | server (run-mgmt-jetty metrics (merge configuration {:join? false 58 | :port port}))] 59 | (log/info "Created a new management HTTP server") 60 | (assoc component :mgmt-httpd server))))) 61 | 62 | (stop [component] 63 | (if (running? component) 64 | ; then 65 | (do 66 | (.stop (:mgmt-httpd component)) 67 | (log/info "Shut down the management HTTP server.") 68 | (dissoc component :mgmt-httpd)) 69 | ; else 70 | (do 71 | (log/info "Management HTTP server not running.") 72 | component)))) 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/config.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.config 2 | (:require [environ.core :as environ] 3 | [org.zalando.stups.friboo.log :as log] 4 | [clojure.string :as str])) 5 | 6 | (defn- nil-or-empty? 7 | "If x is a string, returns true if nil or empty. Else returns true if x is nil" 8 | [x] 9 | (if (string? x) 10 | (empty? x) 11 | (nil? x))) 12 | 13 | (defn require-config 14 | "Helper function to fail of a configuration value is missing." 15 | [configuration key] 16 | (let [value (get configuration key)] 17 | (if (nil-or-empty? value) 18 | (throw (IllegalArgumentException. (str "Configuration " key " is required but is missing."))) 19 | value))) 20 | 21 | (defn- is-sensitive-key [k] 22 | (let [kname (name k)] 23 | (or (.contains kname "pass") 24 | (.contains kname "private") 25 | (.contains kname "secret")))) 26 | 27 | (defn mask [config] 28 | "Mask sensitive information such as passwords" 29 | (into {} (for [[k v] config] [k (if (is-sensitive-key k) "MASKED" v)]))) 30 | 31 | (defn- strip [namespace k] 32 | (keyword (str/replace-first (name k) (str (name namespace) "-") ""))) 33 | 34 | (defn- namespaced [config namespace] 35 | (if (contains? config namespace) 36 | (config namespace) 37 | (into {} (map (fn [[k v]] [(strip (name namespace) k) v]) 38 | (filter (fn [[k v]] 39 | (.startsWith (name k) (str (name namespace) "-"))) 40 | config))))) 41 | 42 | (defn parse-namespaces [config namespaces] 43 | (let [namespaced-configs (into {} (map (juxt identity (partial namespaced config)) namespaces))] 44 | (doseq [[namespace namespaced-config] namespaced-configs] 45 | (log/debug "Destructured %s into %s." namespace (mask namespaced-config))) 46 | namespaced-configs)) 47 | 48 | (defn remap-keys [input mapping] 49 | (merge 50 | (into {} (for [[new-key old-key] mapping 51 | :let [old-value (get input old-key)] 52 | :when old-value] 53 | [new-key old-value])) 54 | input)) 55 | 56 | (defn load-config 57 | "Loads the configuration from different sources and transforms it. 58 | 59 | Merges default config with environment variables: 60 | {:http-port 8080 :tokeninfo-url \"foo\"}, {:http-port 9090} -> {:http-port 9090 :tokeninfo-url \"foo\"} 61 | Then optionally renames some keys, but only if the new key does not exist: 62 | {:http-tokeninfo-url :tokeninfo-url}, {:http-port 9090 :tokeninfo-url \"foo\"} 63 | -> {:http-port 9090 :tokeninfo-url \"foo\" :http-tokeninfo-url \"foo\"} 64 | Then filters out by provided namespace prefixes: 65 | [:http], {:http-port 9090 :tokeninfo-url \"foo\" :http-tokeninfo-url \"foo\"} 66 | -> {:http-port 9090 :http-tokeninfo-url \"foo\"} 67 | Then extracts namespaces: 68 | {:http-port 9090 :http-tokeninfo-url \"foo\"} -> {:http {:port 9090 :tokeninfo-url \"foo\"}}" 69 | 70 | [default-config namespaces & [{:keys [mapping]}]] 71 | (-> (merge default-config environ/env) 72 | (remap-keys mapping) 73 | (parse-namespaces (conj namespaces :system)))) 74 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/system/db_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.db-test 2 | (:require [clojure.test :refer :all] 3 | [org.zalando.stups.friboo.system.db :refer :all] 4 | [cheshire.core :as json] 5 | [clj-time.format :as f] 6 | [org.zalando.stups.friboo.test-utils :as u] 7 | [com.stuartsierra.component :as component]) 8 | (:import (java.sql Timestamp) 9 | (com.fasterxml.jackson.databind.util ISO8601Utils) 10 | (java.text ParsePosition))) 11 | 12 | (deftest test-sql-timestamp-serialization 13 | (let [actual-millis (System/currentTimeMillis) 14 | timestamp (Timestamp. actual-millis) 15 | json (json/encode timestamp) 16 | timestamp-string (json/decode json)] 17 | (println "Milliseconds " actual-millis "have been serialized to " json) 18 | 19 | (are [parsed-millis] 20 | ; test, if serialized date can be parsed with different libs 21 | (= actual-millis parsed-millis) 22 | 23 | ; java 8 time 24 | (-> (java.time.ZonedDateTime/parse timestamp-string) .toInstant .toEpochMilli) 25 | ; joda time 26 | (-> (org.joda.time.DateTime/parse timestamp-string) .getMillis) 27 | ; clj-time default parser 28 | (-> (f/parse timestamp-string) .getMillis) 29 | ; clj-time date-time parser 30 | (-> (f/parse (f/formatters :date-time) timestamp-string) .getMillis) 31 | ; fasterxml jackson's ISO8601Utils 32 | (-> (ISO8601Utils/parse timestamp-string (ParsePosition. 0)) .getTime)))) 33 | 34 | (deftest test-db-component-lifecycle 35 | (let [close-count (atom 0) 36 | db-component {:datasource (reify java.io.Closeable 37 | (close [this] 38 | (swap! close-count inc)))} 39 | stopped-db-component (stop-component db-component)] 40 | (is (= 1 @close-count)) 41 | (stop-component stopped-db-component) 42 | (is (= 1 @close-count)))) 43 | 44 | (def test-db-config 45 | {:classname "org.postgresql.Driver" 46 | :subprotocol "postgresql" 47 | :subname "//localhost:5432/postgres" 48 | :user "postgres" 49 | :password "postgres" 50 | :init-sql "SET statement_timeout TO '60s'; SET search_path TO test_data"}) 51 | 52 | (deftest wrap-midje-facts 53 | 54 | (u/with-comp [db-comp (map->DB {:configuration test-db-config})] 55 | (component/start db-comp) 56 | (prn db-comp)) 57 | 58 | ) 59 | 60 | (generate-hystrix-commands) 61 | 62 | 63 | (deftest test-load-flyway-configuration 64 | (let [configuration {:user "user" 65 | :password "password" 66 | :flyway.table "tablename" 67 | :flyway-schemas "schemas"} 68 | jdbc-url "jdbc-url" 69 | properties (load-flyway-configuration configuration jdbc-url)] 70 | (is (= properties {"flyway.password" "password" 71 | "flyway.url" "jdbc-url" 72 | "flyway.driver" "" 73 | "flyway.user" "user" 74 | "flyway.table" "tablename" 75 | "flyway.schemas" "schemas"})))) 76 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixThreadPool/hystrixThreadPool.css: -------------------------------------------------------------------------------- 1 | .dependencyThreadPools .spacer { 2 | width: 100%; 3 | margin: 0 auto; 4 | padding-top:4px; 5 | clear:both; 6 | } 7 | 8 | 9 | .dependencyThreadPools .last { 10 | margin-right: 0px; 11 | } 12 | 13 | .dependencyThreadPools span.loading { 14 | display: block; 15 | padding-top: 6%; 16 | padding-bottom: 6%; 17 | color: gray; 18 | text-align: center; 19 | } 20 | 21 | .dependencyThreadPools span.loading.failed { 22 | color: red; 23 | } 24 | 25 | 26 | .dependencyThreadPools div.monitor { 27 | float: left; 28 | margin-right:5px; /* these are tweaked to look good on desktop and iPad portrait, and fit things densely */ 29 | margin-top:5px; 30 | } 31 | 32 | .dependencyThreadPools div.monitor p.name { 33 | font-weight:bold; 34 | font-size: 10pt; 35 | text-align: right; 36 | padding-bottom: 5px; 37 | } 38 | 39 | .dependencyThreadPools div.monitor_data { 40 | margin: 0 auto; 41 | } 42 | 43 | .dependencyThreadPools span.smaller { 44 | font-size: 8pt; 45 | color: grey; 46 | } 47 | 48 | 49 | .dependencyThreadPools div.tableRow { 50 | width:100%; 51 | white-space: nowrap; 52 | font-size: 8pt; 53 | margin: 0 auto; 54 | clear:both; 55 | } 56 | 57 | .dependencyThreadPools div.tableRow .cell { 58 | float:left; 59 | } 60 | 61 | .dependencyThreadPools div.tableRow .header { 62 | text-align:right; 63 | padding-right:5px; 64 | } 65 | 66 | .dependencyThreadPools div.tableRow .header.left { 67 | width:85px; 68 | } 69 | 70 | .dependencyThreadPools div.tableRow .header.right { 71 | width:75px; 72 | } 73 | 74 | .dependencyThreadPools div.tableRow .data { 75 | font-weight: bold; 76 | text-align:right; 77 | } 78 | 79 | .dependencyThreadPools div.tableRow .data.left { 80 | width:30px; 81 | } 82 | 83 | .dependencyThreadPools div.tableRow .data.right { 84 | width:45px; 85 | } 86 | 87 | .dependencyThreadPools div.monitor { 88 | width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */ 89 | height: 110px; 90 | } 91 | 92 | 93 | 94 | 95 | 96 | /* override the HREF when we have specified it as a tooltip to not act like a link */ 97 | .dependencyThreadPools div.monitor_data a.tooltip { 98 | text-decoration: none; 99 | cursor: default; 100 | } 101 | 102 | .dependencyThreadPools div.monitor_data a.rate { 103 | font-weight:bold; 104 | color: black; 105 | font-size: 11pt; 106 | } 107 | 108 | .dependencyThreadPools div.rate { 109 | padding-top: 1px; 110 | clear:both; 111 | text-align:right; 112 | } 113 | 114 | .dependencyThreadPools span.rate_value { 115 | font-weight:bold; 116 | } 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | .dependencyThreadPools div.monitor div.chart { 125 | } 126 | 127 | .dependencyThreadPools div.monitor div.chart svg { 128 | } 129 | 130 | .dependencyThreadPools div.monitor div.chart svg text { 131 | fill: white; 132 | } 133 | 134 | .dependencyThreadPools #hidden { 135 | width:1px; 136 | height:1px; 137 | background: lightgrey; 138 | display: none; 139 | } 140 | 141 | 142 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.zalando.stups/friboo "2.0.0-beta6-SNAPSHOT" 2 | :description "An utility library to write microservices in clojure." 3 | :url "https://github.com/zalando/friboo" 4 | 5 | :license {:name "Apache 2.0" 6 | :url "http://www.apache.org/licenses/LICENSE-2.0" 7 | :distribution :repo} 8 | 9 | :scm {:url "git@github.com:zalando/friboo.git"} 10 | 11 | :min-lein-version "2.0.0" 12 | 13 | :java-source-paths ["java"] 14 | 15 | :dependencies [[org.clojure/clojure "1.8.0"] 16 | [org.zalando/swagger1st "0.24.0" :exclusions [org.apache.httpcomponents/httpcore]] 17 | [org.zalando.stups/txdemarcator "0.7.0"] 18 | [com.stuartsierra/component "0.3.1"] 19 | [digest "1.4.5"] 20 | [ring "1.5.0"] 21 | [org.eclipse.jetty/jetty-servlet "9.2.10.v20150310"] ; needs to be in sync with ring dependency 22 | [amalloy/ring-gzip-middleware "0.1.3"] 23 | [environ "1.1.0"] 24 | [io.clj/logging "0.8.1"] 25 | [org.apache.logging.log4j/log4j-api "2.7"] 26 | [org.apache.logging.log4j/log4j-core "2.7"] 27 | [org.apache.logging.log4j/log4j-slf4j-impl "2.7"] 28 | [org.apache.logging.log4j/log4j-jcl "2.7"] 29 | [org.apache.logging.log4j/log4j-1.2-api "2.7"] 30 | [org.apache.logging.log4j/log4j-jul "2.7"] 31 | [com.jolbox/bonecp "0.8.0.RELEASE" :exclusions [com.google.guava/guava]] 32 | [org.flywaydb/flyway-core "4.0.3"] 33 | [amazonica "0.3.77" :exclusions [org.apache.httpcomponents/httpclient joda-time]] 34 | [org.clojure/data.codec "0.1.0"] 35 | [overtone/at-at "1.2.0"] 36 | [org.zalando.stups/tokens "0.9.9"] 37 | [com.netflix.hystrix/hystrix-clj "1.5.8"] 38 | [com.netflix.hystrix/hystrix-core "1.5.8"] 39 | [com.netflix.hystrix/hystrix-metrics-event-stream "1.5.8"] 40 | [org.clojure/core.incubator "0.1.4"] 41 | [metrics-clojure "2.7.0" :exclusions [io.dropwizard.metrics/metrics-core]] 42 | [io.dropwizard.metrics/metrics-servlets "3.1.2"] 43 | [org.slf4j/slf4j-api "1.7.21"] 44 | [com.fasterxml.jackson.core/jackson-core "2.8.5"] 45 | [com.fasterxml.jackson.core/jackson-databind "2.8.5"] 46 | [org.apache.httpcomponents/httpclient "4.5.2"] 47 | [org.clojure/core.memoize "0.5.9"] 48 | [commons-codec "1.10"] 49 | [com.newrelic.agent.java/newrelic-api "3.33.0"]] 50 | 51 | :plugins [[lein-cloverage "1.0.9"] 52 | [lein-environ "1.1.0"] 53 | [lein-marginalia "0.9.0"]] 54 | 55 | :pom-addition [:developers 56 | [:developer {:id "sarnowski"} 57 | [:name "Tobias Sarnowski"] 58 | [:email "tobias.sarnowski@zalando.de"] 59 | [:role "Maintainer"] 60 | [:timezone "+1"]]] 61 | 62 | :aliases {"cloverage" ["with-profile" "test" "cloverage"]} 63 | 64 | :deploy-repositories {"releases" {:url "https://oss.sonatype.org/service/local/staging/deploy/maven2/" :creds :gpg} 65 | "snapshots" {:url "https://oss.sonatype.org/content/repositories/snapshots/" :creds :gpg}}) 66 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/system/mgmt_http_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.mgmt-http-test 2 | (:require [clojure.test :refer :all] 3 | [org.zalando.stups.friboo.system.mgmt-http :refer :all] 4 | [org.zalando.stups.friboo.system.metrics :refer [add-metrics-servlet add-metrics-filter]] 5 | [org.zalando.stups.friboo.test-utils :refer [track]] 6 | [org.zalando.stups.friboo.log :as log] 7 | [ring.adapter.jetty :as jetty]) 8 | (:import (org.eclipse.jetty.util.component LifeCycle) 9 | (org.eclipse.jetty.server HandlerContainer))) 10 | 11 | (deftest test-no-listen-start-component 12 | (let [component (.start (map->MgmtHTTP {:configuration {:no-listen? true}}))] 13 | (is (not (running? component))))) 14 | 15 | (deftest test-component-lifecycle 16 | (let [configuration {:foo "bar" 17 | :no-listen? false} 18 | metrics {:name "a metrics component"} 19 | calls (atom []) 20 | component (atom (map->MgmtHTTP {:configuration configuration 21 | :metrics metrics})) 22 | mock-server (proxy [LifeCycle] [] 23 | (stop [] 24 | (log/warn "STOP has been called") 25 | ((track calls :stop-jetty))))] 26 | (with-redefs [run-mgmt-jetty (comp (constantly mock-server) (track calls :run-jetty))] 27 | ; stop not-running component 28 | (swap! component (fn [c] (.stop c))) 29 | (is (empty? @calls)) 30 | (is (not (running? @component))) 31 | (swap! calls empty) 32 | 33 | ; start component the first time 34 | (swap! component (fn [c] (.start c))) 35 | (let [run-calls (filter #(= :run-jetty (:key %)) @calls) 36 | metrics-param (first (:args (first run-calls))) 37 | options-param (second (:args (first run-calls)))] 38 | (is (= 1 (count run-calls))) 39 | (is (= metrics-param metrics)) 40 | (is (= options-param {:foo "bar" :no-listen? false :join? false :port 7979}))) 41 | (is (running? @component)) 42 | (swap! calls empty) 43 | 44 | ; start component twice should not have an effect 45 | (swap! component (fn [c] (.start c))) 46 | (is (empty? @calls)) 47 | (is (running? @component)) 48 | (swap! calls empty) 49 | 50 | ; stop component 51 | (swap! component (fn [c] (.stop c))) 52 | (is (= 1 (count (filter #(= :stop-jetty (:key %)) @calls)))) 53 | (is (not (running? @component))) 54 | (swap! calls empty)))) 55 | 56 | (deftest test-run-mgmt-jetty 57 | (let [calls (atom []) 58 | metrics {:name "a metrics component"} 59 | options {:port 1234}] 60 | (with-redefs [jetty/run-jetty (track calls :run-jetty)] 61 | (run-mgmt-jetty metrics options) 62 | (is (= (count @calls) 1)) 63 | (let [configurator (:configurator (second (:args (first @calls)))) 64 | mock-server (proxy [HandlerContainer] [])] 65 | (with-redefs [add-metrics-servlet (fn [context-arg metrics-arg] ((track calls :add-metrics-servlet) metrics-arg) context-arg) 66 | add-metrics-filter (fn [context-arg metrics-arg] ((track calls :add-metrics-filter) metrics-arg) context-arg)] 67 | (swap! calls empty) 68 | (configurator mock-server) 69 | (is (= (first (:args (first (filter #(= :add-metrics-servlet (:key %)) @calls)))) metrics)) 70 | (is (= (first (:args (first (filter #(= :add-metrics-filter (:key %)) @calls)))) metrics))))))) 71 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/system/cron.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.cron 2 | (:require [com.stuartsierra.component :as component] 3 | [org.zalando.stups.friboo.log :as log] 4 | [org.zalando.stups.friboo.config :refer [require-config]] 5 | [overtone.at-at :as at]) 6 | (:import (overtone.at_at RecurringJob ScheduledJob) 7 | (org.zalando.stups.txdemarcator Transactions) 8 | (com.newrelic.api.agent Trace NewRelic))) 9 | 10 | ;; TODO remove newrelic parts 11 | 12 | ;; got from overtaone/at-at 13 | 14 | (defn format-date 15 | "Format date object as a string such as: 15:23:35s" 16 | [date] 17 | (.format (java.text.SimpleDateFormat. "EEE hh':'mm':'ss's'") date)) 18 | 19 | (defn format-start-time 20 | [date] 21 | (if (< date (at/now)) 22 | "" 23 | (str ", starts at: " (format-date date)))) 24 | 25 | (defn recurring-job-string 26 | [job] 27 | (str "[" (:id job) "] " 28 | "recurring job created: " (format-date (:created-at job)) 29 | (format-start-time (+ (:created-at job) (:initial-delay job))) 30 | ", period: " (:ms-period job) "ms" 31 | ", desc: \"" (:desc job) "\"")) 32 | 33 | (defn scheduled-job-string 34 | [job] 35 | (str "[" (:id job) "]" 36 | " scheduled job created: " (format-date (:created-at job)) 37 | (format-start-time (+ (:created-at job) (:initial-delay job))) 38 | ", desc: \"" (:desc job) "\"")) 39 | 40 | (defn job-string 41 | [job] 42 | (cond 43 | (= RecurringJob (type job)) (recurring-job-string job) 44 | (= ScheduledJob (type job)) (scheduled-job-string job))) 45 | 46 | (definterface NewRelicTraceable 47 | (callWithTrace [transaction-name callback])) 48 | 49 | (deftype NewRelicTracer [] 50 | NewRelicTraceable 51 | ; create a method with the @Trace annotation 52 | (^{Trace {:dispatcher true}} 53 | callWithTrace [_ transaction-name callback] 54 | ; set up transaction name 55 | (NewRelic/setTransactionName nil transaction-name) 56 | (callback))) 57 | 58 | (def newrelic-tracer (NewRelicTracer.)) 59 | 60 | (defmacro job 61 | "Produces a non-argument function that also marks the execution for transaction naming." 62 | [f & args] 63 | (let [{:keys [ns name]} (-> f resolve meta) 64 | tx-name# (str ns "/" name)] 65 | `(fn [] 66 | (try 67 | (.callWithTrace newrelic-tracer ~tx-name# 68 | (fn [] 69 | (Transactions/runAsBackgroundTransaction ~tx-name# #(~f ~@args)))) 70 | (catch Exception e# 71 | (log/error e# "No job catch-all defined for job %s; bubbled up exception; no further executions will occur!" 72 | ~tx-name#) 73 | (throw e#)))))) 74 | 75 | ;; component 76 | 77 | (defn create-pool [configuration] 78 | (apply at/mk-pool (flatten (seq configuration)))) 79 | 80 | ;; TODO replace macro with function, same approach as with def-db-component 81 | (defmacro def-cron-component 82 | "Defines a new cron job component." 83 | [name dependencies & jobs] 84 | `(defrecord ~name [~(symbol "configuration") ~(symbol "pool") ~@dependencies] 85 | component/Lifecycle 86 | 87 | (start [this#] 88 | (let [~(symbol "pool") (create-pool ~(symbol "configuration"))] 89 | 90 | ~@jobs 91 | 92 | ; log scheduled jobs 93 | (let [jobs# (at/scheduled-jobs ~(symbol "pool"))] 94 | (if (empty? jobs#) 95 | (log/warn "No jobs are currently scheduled.") 96 | (dorun 97 | (map #(log/info (job-string %)) jobs#)))) 98 | 99 | (assoc this# :pool ~(symbol "pool")))) 100 | 101 | (stop [this#] 102 | (when-let [pool# (:pool this#)] 103 | (at/stop-and-reset-pool! pool# :strategy :kill)) 104 | (dissoc this# :pool)))) 105 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixCommand/hystrixCommand.css: -------------------------------------------------------------------------------- 1 | .dependencies .spacer { 2 | width: 100%; 3 | margin: 0 auto; 4 | padding-top:4px; 5 | clear:both; 6 | } 7 | 8 | 9 | .dependencies .last { 10 | margin-right: 0px; 11 | } 12 | 13 | .dependencies span.loading { 14 | display: block; 15 | padding-top: 6%; 16 | padding-bottom: 6%; 17 | color: gray; 18 | text-align: center; 19 | } 20 | 21 | .dependencies span.loading.failed { 22 | color: red; 23 | } 24 | 25 | 26 | .dependencies div.monitor { 27 | float: left; 28 | margin-right:5px; 29 | margin-top:5px; 30 | } 31 | 32 | .dependencies div.monitor p.name { 33 | font-weight:bold; 34 | font-size: 10pt; 35 | text-align: right; 36 | padding-bottom: 5px; 37 | } 38 | 39 | .dependencies div.monitor_data { 40 | margin: 0 auto; 41 | } 42 | 43 | /* override the HREF when we have specified it as a tooltip to not act like a link */ 44 | .dependencies div.monitor_data a.tooltip { 45 | text-decoration: none; 46 | cursor: default; 47 | } 48 | 49 | .dependencies div.monitor_data div.counters { 50 | text-align: right; 51 | padding-bottom: 10px; 52 | font-size: 10pt; 53 | clear: both; 54 | 55 | } 56 | 57 | .dependencies div.monitor_data div.counters div.cell { 58 | display: inline; 59 | float: right; 60 | } 61 | 62 | .dependencies .borderRight { 63 | border-right: 1px solid grey; 64 | padding-right: 6px; 65 | padding-left: 8px; 66 | } 67 | 68 | .dependencies div.cell .line { 69 | display: block; 70 | } 71 | 72 | .dependencies div.monitor_data a, 73 | .dependencies span.rate_value { 74 | font-weight:bold; 75 | } 76 | 77 | 78 | .dependencies span.smaller { 79 | font-size: 8pt; 80 | color: grey; 81 | } 82 | 83 | 84 | 85 | .dependencies div.tableRow { 86 | width:100%; 87 | white-space: nowrap; 88 | font-size: 8pt; 89 | margin: 0 auto; 90 | clear:both; 91 | padding-left:26%; 92 | } 93 | 94 | .dependencies div.tableRow .cell { 95 | float:left; 96 | } 97 | 98 | .dependencies div.tableRow .header { 99 | width:18%; 100 | text-align:right; 101 | padding-right:2%; 102 | } 103 | 104 | .dependencies div.tableRow .data { 105 | width:17%; 106 | font-weight: bold; 107 | text-align:right; 108 | } 109 | 110 | 111 | .dependencies div.monitor { 112 | width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */ 113 | height: 150px; 114 | } 115 | 116 | .dependencies .success { 117 | color: green; 118 | } 119 | 120 | .dependencies .shortCircuited { 121 | color: blue; 122 | } 123 | 124 | .dependencies .timeout { 125 | color: #FF9900; /* shade of orange */ 126 | } 127 | 128 | .dependencies .failure { 129 | color: red; 130 | } 131 | 132 | .dependencies .rejected { 133 | color: purple; 134 | } 135 | 136 | .dependencies .exceptionsThrown { 137 | color: brown; 138 | } 139 | 140 | .dependencies .badRequest { 141 | color: lightSeaGreen; 142 | } 143 | 144 | .dependencies div.monitor_data a.rate { 145 | color: black; 146 | font-size: 11pt; 147 | } 148 | 149 | .dependencies div.rate { 150 | padding-top: 1px; 151 | clear:both; 152 | text-align:right; 153 | } 154 | 155 | .dependencies .errorPercentage { 156 | color: grey; 157 | } 158 | 159 | .dependencies div.cell .errorPercentage { 160 | padding-left:5px; 161 | font-size: 12pt !important; 162 | } 163 | 164 | 165 | .dependencies div.monitor div.chart { 166 | } 167 | 168 | .dependencies div.monitor div.chart svg { 169 | } 170 | 171 | .dependencies div.monitor div.chart svg text { 172 | fill: white; 173 | } 174 | 175 | 176 | .dependencies div.circuitStatus { 177 | width:100%; 178 | white-space: nowrap; 179 | font-size: 9pt; 180 | margin: 0 auto; 181 | clear:both; 182 | text-align:right; 183 | padding-top: 4px; 184 | } 185 | 186 | .dependencies #hidden { 187 | width:1px; 188 | height:1px; 189 | background: lightgrey; 190 | display: none; 191 | } 192 | 193 | 194 | 195 | /* sparkline */ 196 | .dependencies path { 197 | stroke: steelblue; 198 | stroke-width: 1; 199 | fill: none; 200 | } 201 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/system/metrics_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.metrics-test 2 | (:require [clojure.test :refer :all] 3 | [org.zalando.stups.friboo.system.metrics :refer :all] 4 | [org.zalando.stups.friboo.test-utils :refer [track]] 5 | [metrics.core :as metrics]) 6 | (:import (org.eclipse.jetty.servlet ServletContextHandler ServletHolder) 7 | (java.util.concurrent TimeUnit))) 8 | 9 | (defn mock-request [method path] 10 | {:swagger {:key [method (seq path)]} 11 | :request-method method}) 12 | 13 | (deftest test-request2string 14 | (are [request status expected-string] 15 | (= (swagger1st-request2string request status) expected-string) 16 | (mock-request :get ["apps" :app_id "credentials"]) 200 "200.GET.apps.{app_id}.credentials" 17 | (mock-request :post ["apps"]) 400 "400.POST.apps")) 18 | 19 | (deftest test-collect-zmon-metrics 20 | (let [component (.start (map->Metrics {:configuration {:metrics-prefix "foo"}})) 21 | next-handler (constantly {:status 404 :body "not found"}) 22 | handler (collect-swagger1st-request-metrics next-handler component)] 23 | (with-redefs [swagger1st-request2string (constantly "mock")] 24 | (handler "a request")) 25 | (let [timer (-> component :metrics-registry .getTimers (get "foo.response.mock"))] 26 | (is (not (nil? timer)))))) 27 | 28 | (deftest test-collect-zmon-metrics-component-not-running 29 | (let [component (map->Metrics {}) 30 | next-handler (constantly {:status 404 :body "not found"}) 31 | handler (collect-swagger1st-request-metrics next-handler component)] 32 | (is (= handler next-handler)))) 33 | 34 | (deftest test-add-metrics-servlet 35 | (let [calls (atom []) 36 | context (proxy [ServletContextHandler] [] 37 | (addEventListener [listener] 38 | ((track calls :add-listener) listener)) 39 | (addServlet [^ServletHolder _ ^String _] 40 | ((track calls :add-servlet)))) 41 | component (.start (map->Metrics {}))] 42 | (add-metrics-servlet context component) 43 | (let [add-listener-calls (filter #(= :add-listener (:key %)) @calls) 44 | add-servlet-calls (filter #(= :add-servlet (:key %)) @calls)] 45 | (is (= 1 (count add-listener-calls))) 46 | (let [listener (-> add-listener-calls first :args first)] 47 | (is (not (nil? listener))) 48 | (is (= (.getMetricRegistry listener) (:metrics-registry component))) 49 | (is (nil? (.getRateUnit listener))) 50 | (is (= (.getDurationUnit listener) TimeUnit/MILLISECONDS)) 51 | (is (= (.getAllowedOrigin listener) "*"))) 52 | (is (= 1 (count add-servlet-calls)))))) 53 | 54 | (deftest test-add-metrics-servlet-when-component-not-running 55 | (let [calls (atom []) 56 | context (proxy [ServletContextHandler] [] 57 | (addEventListener [listener] 58 | ((track calls :add-listener) listener)) 59 | (addServlet [^ServletHolder _ ^String _] 60 | ((track calls :add-servlet)))) 61 | component (map->Metrics {})] 62 | (add-metrics-servlet context component) 63 | (is (empty? @calls)))) 64 | 65 | (deftest test-component-lifecycle 66 | (let [calls (atom []) 67 | component (atom (map->Metrics {}))] 68 | (with-redefs [metrics/new-registry (track calls :new-registry)] 69 | ; stop not-running component 70 | (swap! component (fn [c] (.stop c))) 71 | (is (empty? @calls)) 72 | (is (not (running? @component))) 73 | (swap! calls empty) 74 | 75 | ; start component the first time 76 | (swap! component (fn [c] (.start c))) 77 | (is (= 1 (count (filter #(= :new-registry (:key %)) @calls)))) 78 | (is (running? @component)) 79 | (swap! calls empty) 80 | 81 | ; start component twice should not have an effect 82 | (swap! component (fn [c] (.start c))) 83 | (is (empty? @calls)) 84 | (is (running? @component)) 85 | (swap! calls empty) 86 | 87 | ; stop component 88 | (swap! component (fn [c] (.stop c))) 89 | (is (empty? @calls)) 90 | (is (not (running? @component))) 91 | (swap! calls empty)))) 92 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixCommand/templates/hystrixCircuit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 |
8 | <%= addCommas(rollingCountTimeout) %> 9 | <% if(propertyValue_executionIsolationStrategy == 'THREAD') { %> 10 | <%= addCommas(rollingCountThreadPoolRejected) %> 11 | <% } %> 12 | <% if(propertyValue_executionIsolationStrategy == 'SEMAPHORE') { %> 13 | <%= addCommas(rollingCountSemaphoreRejected) %> 14 | <% } %> 15 | <%= addCommas(rollingCountFailure) %> 16 |
17 | 23 |
24 | 25 | 28 | 31 | 32 |
33 | <% if(propertyValue_circuitBreakerForceClosed) { %> 34 | [ Forced Closed ] 35 | <% } %> 36 | <% if(propertyValue_circuitBreakerForceOpen) { %> 37 | Circuit Forced Open 38 | <% } else { %> 39 | <% if(isCircuitBreakerOpen == reportingHosts) { %> 40 | Circuit Open 41 | <% } else if(isCircuitBreakerOpen == 0) { %> 42 | Circuit Closed 43 | <% } else { 44 | /* We have some circuits that are open */ 45 | %> 46 | Circuit <%= isCircuitBreakerOpen.replace("true", "Open").replace("false", "Closed") %>) 47 | <% } %> 48 | <% } %> 49 |
50 | 51 |
52 | 53 |
54 | <% if(typeof reportingHosts != 'undefined') { %> 55 |
Hosts
56 |
<%= reportingHosts %>
57 | <% } else { %> 58 |
Host
59 |
Single
60 | <% } %> 61 |
90th
62 |
<%= getInstanceAverage(latencyExecute['90'], reportingHosts, false) %>ms
63 |
64 |
65 |
Median
66 |
<%= getInstanceAverage(latencyExecute['50'], reportingHosts, false) %>ms
67 |
99th
68 |
<%= getInstanceAverage(latencyExecute['99'], reportingHosts, false) %>ms
69 |
70 |
71 |
Mean
72 |
<%= latencyExecute_mean %>ms
73 |
99.5th
74 |
<%= getInstanceAverage(latencyExecute['99.5'], reportingHosts, false) %>ms
75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/system/db.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.db 2 | (:require [com.stuartsierra.component :refer [Lifecycle]] 3 | [org.zalando.stups.friboo.log :as log] 4 | [org.zalando.stups.friboo.config :refer [require-config]] 5 | [cheshire.generate] 6 | [com.netflix.hystrix.core :refer [defcommand]]) 7 | (:import (com.jolbox.bonecp BoneCPDataSource) 8 | (org.flywaydb.core Flyway) 9 | (com.netflix.hystrix.exception HystrixBadRequestException) 10 | (com.fasterxml.jackson.databind.util ISO8601Utils) 11 | (java.util Properties))) 12 | 13 | (defn load-flyway-configuration 14 | [configuration jdbc-url] 15 | (let [properties (Properties.)] 16 | (doseq [property configuration] 17 | (when (.contains (name (key property)) "flyway") 18 | (.setProperty properties (clojure.string/replace (name (key property)) "-" ".") (val property)))) 19 | (.setProperty properties "flyway.driver" "") 20 | (.setProperty properties "flyway.url" jdbc-url) 21 | (.setProperty properties "flyway.user" (require-config configuration :user)) 22 | (.setProperty properties "flyway.password" (require-config configuration :password)) 23 | properties)) 24 | 25 | (defn start-component [{:as this :keys [configuration]}] 26 | (if (:datasource this) 27 | (do 28 | (log/debug "Skipping start of DB connection pool; already running.") 29 | this) 30 | 31 | (do 32 | (let [auto-migration? (:auto-migration? configuration true) 33 | jdbc-url (str "jdbc:" (require-config configuration :subprotocol) ":" (require-config configuration :subname))] 34 | 35 | (when auto-migration? 36 | (log/info "Initiating automatic DB migration for %s." jdbc-url) 37 | (doto (Flyway.) 38 | (.configure (load-flyway-configuration configuration jdbc-url)) 39 | (.migrate))) 40 | 41 | (log/info "Starting DB connection pool for %s." jdbc-url) 42 | (let [partitions (or (:partitions configuration) 3) 43 | min-pool (or (:min-pool configuration) 6) 44 | max-pool (or (:max-pool configuration) 21) 45 | datasource (doto (BoneCPDataSource.) 46 | (.setJdbcUrl jdbc-url) 47 | (.setUsername (require-config configuration :user)) 48 | (.setPassword (require-config configuration :password)) 49 | (.setMinConnectionsPerPartition (int (/ min-pool partitions))) 50 | (.setMaxConnectionsPerPartition (int (/ max-pool partitions))) 51 | (.setPartitionCount partitions) 52 | (.setStatisticsEnabled true) 53 | (.setIdleConnectionTestPeriodInMinutes 2) 54 | (.setIdleMaxAgeInMinutes 10) 55 | (.setInitSQL (or (:init-sql configuration) "")) 56 | (.setConnectionTestStatement "SELECT 1"))] 57 | (assoc this :datasource datasource)))))) 58 | 59 | (defn stop-component [this] 60 | (if-not (:datasource this) 61 | (do 62 | (log/debug "Skipping stop of DB connection pool; not running.") 63 | this) 64 | 65 | (do 66 | (log/info "Stopping DB connection pool.") 67 | (.close (:datasource this)) 68 | (assoc this :datasource nil)))) 69 | 70 | ;; Defines a DB component 71 | ;; HINT: this component is itself a valid db-spec as its a map with the key 'datasource' 72 | ;; configuration can include {:auto-migration? false} to disable automatic migration 73 | (defrecord DB [;; parameters (filled in by make-http on creation) 74 | configuration 75 | ;; dependencies (filled in by the component library before starting) 76 | ;; runtime vals (filled in by start-component) 77 | datasource 78 | ] 79 | Lifecycle 80 | (start [this] 81 | (start-component this)) 82 | (stop [this] 83 | (stop-component this))) 84 | 85 | ;; #30 cheshire drops the milliseconds by default 86 | (cheshire.generate/add-encoder java.sql.Timestamp 87 | (fn [timestamp jsonGenerator] 88 | (.writeString jsonGenerator 89 | (str (ISO8601Utils/format timestamp true))))) 90 | 91 | ;; Helpers for hystrix wrapping 92 | 93 | (defn ignore-nonfatal-exceptions 94 | "Default do-nothing nonfatal exception indicator. You would probably want to replace it with something that 95 | returns a non-nil string on bad requests errors" 96 | [e] 97 | nil) 98 | 99 | (defmacro generate-hystrix-commands 100 | "Wraps all functions in the used namespace" 101 | [& {:keys [prefix suffix ignore-exception-fn? namespace] 102 | :or {prefix "cmd-" 103 | suffix "" 104 | ignore-exception-fn? ignore-nonfatal-exceptions 105 | namespace *ns*}}] 106 | `(do ~@(map (fn [[n f]] 107 | `(defcommand ~(symbol (str prefix (name n) suffix)) 108 | [& args#] 109 | (try 110 | (apply ~f args#) 111 | (catch Throwable t# 112 | (if-let [msg# (~ignore-exception-fn? t#)] 113 | (throw (HystrixBadRequestException. msg# t#)) 114 | (throw t#)))))) 115 | (ns-publics namespace)))) 116 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/system/metrics.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.metrics 2 | (:require [com.stuartsierra.component :refer [Lifecycle]] 3 | [org.zalando.stups.friboo.log :as log] 4 | [metrics.core :as metrics] 5 | [metrics.timers :as tmr] 6 | [clojure.string :as string] 7 | [org.zalando.stups.friboo.config :as config]) 8 | (:import (java.util.concurrent TimeUnit) 9 | (com.codahale.metrics.servlets MetricsServlet$ContextListener MetricsServlet) 10 | (org.eclipse.jetty.servlet ServletHolder FilterHolder ServletContextHandler) 11 | (java.util EnumSet) 12 | (javax.servlet DispatcherType Filter ServletRequest ServletResponse FilterChain FilterConfig) 13 | (javax.servlet.http HttpServletResponse HttpServletRequest))) 14 | 15 | (defn- segment2str 16 | [x] 17 | (if (keyword? x) 18 | (str "{" (name x) "}") 19 | (str x))) 20 | 21 | (defn- path2str 22 | [coll] 23 | (string/join "." (map segment2str coll))) 24 | 25 | (defn swagger1st-request2string 26 | "GET /apps/{application_id} -> 200.GET.apps.{application_id}" 27 | [request status] 28 | (clojure.string/join "." [status 29 | (.toUpperCase (-> request :request-method name)) 30 | (-> request :swagger :key second path2str)])) 31 | 32 | (defn http-servlet-request2string 33 | "GET /hystrix.stream -> 200.GET.hystrix.stream" 34 | [^HttpServletRequest request status] 35 | (clojure.string/join "." [status 36 | (.toUpperCase (.getMethod request)) 37 | (let [path (string/replace (subs (.getServletPath request) 1) #"[/]" ".")] 38 | (if (or (string/blank? path) (= path ".")) 39 | "*ROOT*" 40 | path))])) 41 | 42 | (defn collect-swagger1st-request-metrics 43 | "Ring middleware that creates Timers for Swagger API calls 44 | and stores them in the running metrics component." 45 | [next-handler {:keys [metrics-registry configuration]}] 46 | (if metrics-registry 47 | (fn [request] 48 | (let [start (System/currentTimeMillis) 49 | response (next-handler request) 50 | status (:status response) 51 | prefix (config/require-config configuration :metrics-prefix) 52 | timer (tmr/timer metrics-registry [prefix "response" (swagger1st-request2string request status)])] 53 | (.update timer (- (System/currentTimeMillis) start) (TimeUnit/MILLISECONDS)) 54 | response)) 55 | (do 56 | (log/info "Not collecting metrics for requests. Metrics component is not running.") 57 | next-handler))) 58 | 59 | (defn running? [component] 60 | (:metrics-registry component)) 61 | 62 | (defn add-metrics-servlet 63 | [context metrics] 64 | (when-let [metrics-registry (:metrics-registry metrics)] 65 | (.addEventListener context (proxy [MetricsServlet$ContextListener] [] 66 | (getMetricRegistry [] metrics-registry) 67 | (getDurationUnit [] TimeUnit/MILLISECONDS) 68 | (getAllowedOrigin [] "*"))) 69 | (.addServlet context (ServletHolder. MetricsServlet) "/metrics")) 70 | context) 71 | 72 | (defn add-metrics-filter 73 | [^ServletContextHandler context {:keys [metrics-registry configuration]}] 74 | (when metrics-registry 75 | (.addFilter 76 | context 77 | (FilterHolder. 78 | (proxy [Filter] [] 79 | (doFilter [^ServletRequest request ^ServletResponse response ^FilterChain chain] 80 | (if (and (instance? HttpServletResponse response) (instance? HttpServletRequest request)) 81 | (let [start (System/currentTimeMillis) 82 | status (atom 500)] 83 | (try 84 | (.doFilter chain request response) 85 | (swap! status (fn [_] (.getStatus (cast HttpServletResponse response)))) 86 | 87 | (finally 88 | (let [prefix (config/require-config configuration :metrics-prefix) 89 | request-str (http-servlet-request2string request @status) 90 | timer (tmr/timer metrics-registry [prefix "response" request-str])] 91 | (.update timer (- (System/currentTimeMillis) start) (TimeUnit/MILLISECONDS)))))) 92 | (.doFilter chain request response))) 93 | 94 | (init [^FilterConfig _] nil) 95 | (destroy [] nil))) 96 | "/metrics" 97 | (EnumSet/of DispatcherType/REQUEST))) 98 | context) 99 | 100 | (def default-configuration 101 | {:metrics-prefix "friboo"}) 102 | 103 | (defrecord Metrics [configuration] 104 | Lifecycle 105 | 106 | (start [component] 107 | (if (running? component) 108 | ; then 109 | (do 110 | (log/info "Metrics registry already running.") 111 | component) 112 | ; else 113 | (let [registry (metrics/new-registry) 114 | configuration-with-defaults (merge default-configuration configuration)] 115 | (log/info "Created a new metrics registry.") 116 | (assoc component :metrics-registry registry 117 | :configuration configuration-with-defaults)))) 118 | 119 | 120 | (stop [component] 121 | (if (running? component) 122 | ; then 123 | (do 124 | (log/info "Shutting down the metrics registry.") 125 | (dissoc component :metrics-registry)) 126 | ; else 127 | (do 128 | (log/info "Metrics registry not running.") 129 | component)))) 130 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/monitor/monitor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hystrix Monitor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 |
37 |
38 | 58 |
59 |
Loading ...
60 | 61 |
62 |
63 | 64 |
65 | 74 |
75 |
Loading ...
76 |
77 | 78 | 79 | 80 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /test/org/zalando/stups/friboo/system/http_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.http-test 2 | (:require [clojure.test :refer :all] 3 | [midje.sweet :refer :all] 4 | [org.zalando.stups.friboo.system.http :refer :all :as http] 5 | [com.stuartsierra.component :as component] 6 | [clj-http.client :as client] 7 | [clojure.pprint :refer [pprint]] 8 | [org.zalando.stups.friboo.test-utils :as u]) 9 | (:import (com.netflix.hystrix.exception HystrixRuntimeException HystrixRuntimeException$FailureType))) 10 | 11 | (deftest unit-tests 12 | 13 | (facts "about merged-parameters" 14 | (merged-parameters {:parameters {:query {:foo 1} :path {:bar 2}}}) => {:foo 1 :bar 2}) 15 | 16 | (facts "about flatten1" 17 | (flatten1 {:a 1 :b [2 3]}) => [:a 1 :b [2 3]]) 18 | 19 | (facts "about with-flattened-options-map" 20 | (-> :first-arg 21 | (#'http/with-flattened-options-map vector {:a 1 :b 2})) => (vector :first-arg :a 1 :b 2)) 22 | 23 | ) 24 | 25 | (defmacro with-test-http 26 | "Helper for running tests against working component. 27 | Picks a random port, starts the component and in the end always stops it." 28 | [[url-sym filename] & body] 29 | `(let [port# (u/get-free-port) 30 | http-comp# (-> (str "org/zalando/stups/friboo/system/" ~filename) 31 | (make-http {:port port#}) 32 | (component/start)) 33 | ~url-sym (str "http://localhost:" port#)] 34 | (try 35 | ~@body 36 | (finally 37 | (component/stop http-comp#))))) 38 | 39 | (defn get-info [controller params request] 40 | ) 41 | 42 | (defn put-foo [controller params request] 43 | ) 44 | 45 | (defchecker contains-keys [expected-keys] 46 | (checker [actual] 47 | (clojure.set/superset? (set (keys actual)) (set expected-keys)))) 48 | 49 | (deftest component-tests 50 | 51 | (facts "Default behavior" 52 | 53 | (with-test-http [url "http.yml"] 54 | (fact "Redirects to /ui/ from /" 55 | (client/get (str url) {:follow-redirects false}) 56 | => (contains {:status 302 :headers (contains {"Location" "/ui/"})}))) 57 | 58 | (with-test-http [url "http.yml"] 59 | (fact "favicon.ico requests are 404ed" 60 | (client/get (str url "/favicon.ico") {:throw-exceptions false}) 61 | => (contains {:status 404}))) 62 | 63 | (with-test-http [url "http.yml"] 64 | (fact "Default headers are included in the response." 65 | (client/get (str url "/info")) 66 | => (contains {:headers (contains {"Strict-Transport-Security" anything 67 | "Access-Control-Allow-Methods" anything 68 | "Content-Type" "application/json"})}))) 69 | 70 | (with-test-http [url "http.yml"] 71 | (fact "Health endpoint is provided" 72 | (client/get (str url "/.well-known/health") {:as :json}) 73 | => (contains {:status 200 :body {:health true}}))) 74 | 75 | (with-test-http [url "http.yml"] 76 | (fact "Hystrix exceptions are converted" 77 | (client/get (str url "/info") {:throw-exceptions false :as :json :coerce :always}) 78 | => (contains {:status 503}) 79 | (provided 80 | (get-info anything anything anything) 81 | =throws=> (HystrixRuntimeException. HystrixRuntimeException$FailureType/TIMEOUT nil 82 | "Divide by zero" (ArithmeticException. "foo") nil)))) 83 | (with-test-http [url "http.yml"] 84 | (fact "ex-info exceptions are not changed" 85 | (client/get (str url "/info") {:throw-exceptions false :as :json :coerce :always}) 86 | => (contains {:status 501}) 87 | (provided 88 | (get-info anything anything anything) =throws=> (ex-info "Hello" {:foo "bar" :http-code 501})))) 89 | 90 | (with-test-http [url "http.yml"] 91 | (fact "Unhandled exceptions are nicely packaged, details are provided in the response body." 92 | (client/get (str url "/info") {:throw-exceptions false :as :json :coerce :always}) 93 | => (contains {:status 500 :body {:details "java.lang.ArithmeticException: Divide by zero" 94 | :message "Internal server error"}}) 95 | (provided 96 | (get-info anything anything anything) =throws=> (ArithmeticException. "Divide by zero")))) 97 | 98 | ) 99 | 100 | (with-test-http [url "http.yml"] 101 | (fact "Print request contents" 102 | (client/get (str url "/info") {:follow-redirects false}) 103 | => (contains {:status 200}))) 104 | 105 | (fact "About controllers" 106 | (let [port (u/get-free-port) 107 | http-comp (-> (make-http (str "org/zalando/stups/friboo/system/" "http.yml") {:port port}) 108 | ;; Instead of relying on component/using and starting the system, provide the dependency directly 109 | (merge {:controller ..controller..}) 110 | (component/start)) 111 | url (str "http://localhost:" port)] 112 | (try 113 | (fact "When controller is provided as a dependency, it is passed as the first parameter the handling functions" 114 | (client/get (str url "/info") {:follow-redirects false}) 115 | => (contains {:status 200 :body "OK"}) 116 | (provided (get-info ..controller.. anything anything) => {:body "OK"})) 117 | (finally 118 | (component/stop http-comp))))) 119 | 120 | (with-test-http [url "http.yml"] 121 | (with-redefs [get-info (fn [controller _ request] 122 | (is (fact "Controller is nil with default configuration" 123 | controller => nil)) 124 | (is (fact "Raw request object contains all the things" 125 | (keys request) => (contains [:ssl-client-cert 126 | :protocol 127 | :remote-addr 128 | :params 129 | :headers 130 | :server-port 131 | :content-length 132 | :form-params 133 | :query-params 134 | :content-type 135 | :character-encoding 136 | :swagger 137 | :configuration 138 | :uri 139 | :server-name 140 | :query-string 141 | :body 142 | :parameters]))) 143 | {:body {:ok true}})] 144 | (fact "Normal requests return json-encoded body." 145 | (client/get (str url "/info") {:as :json}) 146 | => (contains {:status 200 :body {:ok true}})))) 147 | 148 | (with-test-http [url "http.yml"] 149 | (with-redefs [put-foo (fn [_ params _] 150 | (fact "Parameters are taken from path, query and body and flattened." 151 | params => {:foo_id "123" :page 1 :foo {:foo_text "lalala"}}) 152 | {:status 201 :body {:ok true}})] 153 | (fact "Put request works" 154 | (client/put (str url "/foo/123?page=1") {:content-type :json 155 | :form-params {:foo_text "lalala"} 156 | :as :json}) 157 | => (contains {:status 201 :body {:ok true}})))) 158 | 159 | ) 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /template/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/org/zalando/stups/friboo/system/http.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.friboo.system.http 2 | (:require [io.sarnowski.swagger1st.executor :as s1stexec] 3 | [io.clj.logging :refer [with-logging-context]] 4 | [ring.util.response :as r] 5 | [org.zalando.stups.friboo.ring :as ring] 6 | [org.zalando.stups.friboo.log :as log] 7 | [io.sarnowski.swagger1st.util.api :as api] 8 | [io.sarnowski.swagger1st.core :as s1st] 9 | [io.sarnowski.swagger1st.util.api :as s1stapi] 10 | [ring.adapter.jetty :as jetty] 11 | [ring.middleware.gzip :as gzip] 12 | [com.stuartsierra.component :refer [Lifecycle]]) 13 | (:import (com.netflix.hystrix.exception HystrixRuntimeException) 14 | (org.zalando.stups.txdemarcator Transactions) 15 | (clojure.lang ExceptionInfo))) 16 | 17 | (defn merged-parameters 18 | "According to the swagger spec, parameter names are only unique with their type. This one assumes that parameter names 19 | are unique in general and flattens them for easier access." 20 | [request] 21 | (apply merge (vals (:parameters request)))) 22 | 23 | (defn make-resolver-fn [controller] 24 | "Calls operationId function with controller, flattened request params and raw request map." 25 | (fn [request-definition] 26 | (when-let [operation-fn (s1stexec/operationId-to-function request-definition)] 27 | (fn [request] 28 | (operation-fn controller (merged-parameters request) request))))) 29 | 30 | (defn middleware-chain [context middlewares] 31 | (reduce #(%2 %1) context middlewares)) 32 | 33 | (defn flatten1 34 | "Flattens the collection one level, for example, converts {:a 1 :b 2} to (:a 1 :b 2)." 35 | [coll] 36 | (apply concat coll)) 37 | 38 | (defn- with-flattened-options-map 39 | "If f is defined like this: 40 | 41 | `(defn f [first-arg & {:keys [a b] :as opts}] ... )` 42 | 43 | makes it convenient to pass `opts` as a map: 44 | 45 | `(-> first-arg (with-flattened-options-map f {:a 1 :b 2}))` 46 | will result in 47 | `(f first-arg :a 1 :b 2)` 48 | 49 | Used in `start-component` for clarity. 50 | " 51 | [first-arg f options] 52 | (apply f first-arg (flatten1 options))) 53 | 54 | (defn allow-all 55 | "Returns a swagger1st security handler that allows everything." 56 | [] 57 | (fn [request definition requirements] 58 | request)) 59 | 60 | ;; Security handler types according to http://swagger.io/specification/#securitySchemeObject 61 | (def allow-all-handlers {"oauth2" (allow-all) 62 | "basic" (allow-all) 63 | "apiKey" (allow-all)}) 64 | 65 | (defn start-component [{:as this :keys [api-resource configuration middlewares security-handlers controller s1st-options]}] 66 | (log/info "Starting HTTP daemon for API %s" api-resource) 67 | (when (:handler this) 68 | (throw (ex-info "Component already started, aborting." {}))) 69 | ;; Refer to default-middlewares object below for middleware lists 70 | ;; Create context from the YAML definition on the classpath, optionally provide validation flag 71 | (let [handler (-> (apply s1st/context :yaml-cp api-resource (flatten1 (:context s1st-options))) 72 | ;; Some middleware will need to access the component later 73 | (assoc :component this) 74 | ;; User can provide additional handlers to be executed before discoverer 75 | (middleware-chain (:before-discoverer middlewares)) 76 | ;; Enables paths for API discovery, like /ui, /swagger.json and /.well-known/schema-discovery 77 | (with-flattened-options-map s1st/discoverer (:discoverer s1st-options)) 78 | ;; User can provide additional handlers to be executed before mapper 79 | (middleware-chain (:before-mapper middlewares)) 80 | ;; Given a path, figures out the spec part describing it 81 | (s1st/mapper) 82 | ;; User can provide additional handlers to be executed before protector 83 | (middleware-chain (:before-protector middlewares)) 84 | ;; Enforces security according to the settings, depends on the spec from mapper 85 | (s1st/protector (merge allow-all-handlers security-handlers)) 86 | ;; User can provide additional handlers to be executed before parser 87 | (middleware-chain (:before-parser middlewares)) 88 | ;; Extracts parameter values from path, query and body of the request 89 | (with-flattened-options-map s1st/parser (:parser s1st-options)) 90 | ;; User can provide additional handlers to be executed before executor 91 | (middleware-chain (:before-executor middlewares)) 92 | ;; Calls the handler function for the request. Customizable through :resolver 93 | (with-flattened-options-map s1st/executor (merge {:resolver (make-resolver-fn controller)} 94 | (:executor s1st-options))))] 95 | (merge this {:handler handler 96 | :httpd (jetty/run-jetty handler (merge configuration {:join? false}))}))) 97 | 98 | (defn stop-component 99 | "Stops the Http component." 100 | [{:as this :keys [handler httpd]}] 101 | (if-not handler 102 | (do 103 | (log/debug "Skipping stop of Http because it's not running.") 104 | this) 105 | (do 106 | (log/info "Stopping Http.") 107 | (when httpd 108 | (.stop httpd)) 109 | (dissoc this :handler :httpd)))) 110 | 111 | (defrecord Http [;; parameters (filled in by make-http on creation) 112 | api-resource 113 | configuration 114 | security-handlers 115 | middlewares 116 | s1st-options 117 | ;; dependencies (filled in by the component library before starting) 118 | controller 119 | metrics 120 | audit-log 121 | ;; runtime vals (filled in by start-component) 122 | httpd 123 | handler] 124 | Lifecycle 125 | (start [this] 126 | (start-component this)) 127 | (stop [this] 128 | (stop-component this))) 129 | 130 | (defn redirect-to-swagger-ui 131 | "Can be used as operationId for GET /" 132 | [& _] 133 | (ring.util.response/redirect "/ui/")) 134 | 135 | ;; ## Middleware 136 | 137 | (defn wrap-default-content-type [next-handler content-type] 138 | (fn [request] 139 | (let [response (next-handler request)] 140 | (if (get-in response [:headers "Content-Type"]) 141 | response 142 | (assoc-in response [:headers "Content-Type"] content-type))))) 143 | 144 | (defn compute-request-info 145 | "Creates a nice, readable request info text for logline prefixing." 146 | [request] 147 | (str 148 | (.toUpperCase (-> request :request-method name)) 149 | " " 150 | (:uri request) 151 | " <- " 152 | (if-let [x-forwarded-for (-> request :headers (get "x-forwarded-for"))] 153 | x-forwarded-for 154 | (:remote-addr request)) 155 | (if-let [tokeninfo (:tokeninfo request)] 156 | (str " / " (get tokeninfo "uid") " @ " (get tokeninfo "realm")) 157 | ""))) 158 | 159 | (defn enrich-log-lines 160 | "Adds HTTP request context information to the logging facility's MDC in the 'request' key." 161 | [next-handler] 162 | (fn [request] 163 | (let [request-info (compute-request-info request)] 164 | (with-logging-context 165 | {:request (str " [" request-info "]")} 166 | (next-handler request))))) 167 | 168 | (defn health-endpoint 169 | "Adds a /.well-known/health endpoint for load balancer tests." 170 | [handler] 171 | (fn [request] 172 | (if (= (:uri request) "/.well-known/health") 173 | (-> (r/response "{\"health\": true}") 174 | (ring/content-type-json) 175 | (r/status 200)) 176 | (handler request)))) 177 | 178 | (defn add-config-to-request 179 | "Adds the HTTP configuration to the request object, so that handlers can access it." 180 | [next-handler configuration] 181 | (fn [request] 182 | (next-handler (assoc request :configuration configuration)))) 183 | 184 | (defn convert-exceptions 185 | [next-handler] 186 | (fn [request] 187 | (try 188 | (next-handler request) 189 | ;; Hystrix exceptions as 503 190 | (catch HystrixRuntimeException e 191 | (let [reason (-> e .getCause .toString) 192 | failure-type (str (.getFailureType e))] 193 | (log/warn (str "Hystrix: " (.getMessage e) " %s occurred, because %s") failure-type reason) 194 | (api/throw-error 503 (str "A dependency is unavailable: " (.getMessage e))))) 195 | ;; ex-info pass-through, will be caught inside parser 196 | (catch ExceptionInfo e 197 | (throw e)) 198 | ;; Other exceptions as 500 199 | (catch Exception e 200 | (log/error e "Unhandled exception.") 201 | (api/throw-error 500 "Internal server error" (str e)))))) 202 | 203 | (defn mark-transaction 204 | "Trigger the TransactionMarker with the swagger operationId for instrumentalisation." 205 | [next-handler] 206 | (fn [request] 207 | (let [operation-id (get-in request [:swagger :request "operationId"]) 208 | tx-parent-id (get-in request [:headers Transactions/APPDYNAMICS_HTTP_HEADER])] 209 | (Transactions/runInTransaction operation-id tx-parent-id #(next-handler request))))) 210 | 211 | (def default-middlewares 212 | "Default set of ring middlewares that are groupped by s1st phases" 213 | {:before-discoverer [#(s1st/ring % add-config-to-request (-> % :component :configuration)) 214 | #(s1st/ring % gzip/wrap-gzip) 215 | #(s1st/ring % enrich-log-lines) 216 | #(s1st/ring % s1stapi/add-hsts-header) 217 | #(s1st/ring % s1stapi/add-cors-headers) 218 | #(s1st/ring % s1stapi/surpress-favicon-requests) 219 | #(s1st/ring % health-endpoint)] 220 | :before-mapper [] 221 | :before-protector [] 222 | :before-parser [#(s1st/ring % mark-transaction)] 223 | :before-executor [;; Convert exceptions to ex-info so that parser creates nice 5xx responses 224 | #(s1st/ring % convert-exceptions) 225 | ;; now we also know the user, replace request info 226 | #(s1st/ring % enrich-log-lines) 227 | #(s1st/ring % wrap-default-content-type "application/json")]}) 228 | 229 | ;; For documentation 230 | (def default-s1st-options {;; It's possible to disable validation of the spec 231 | ;; :context {:validate? true} 232 | :context {} 233 | ;; The following parts are customizable: 234 | ;; :discoverer {:discovery-path "/.well-known/schema-discovery" 235 | ;; :definition-path "/swagger.json" 236 | ;; :ui-path "/ui/" 237 | ;; :overwrite-host? true } 238 | :discoverer {} 239 | ;; No available options for parser, this is here for future 240 | :parser {} 241 | ;; It's possible customize how exactly handler functions should be called 242 | ;; :executor {:resolver io.sarnowski.swagger1st.executor/operationId-to-function} 243 | :executor {}}) 244 | 245 | (defn make-http 246 | "Creates Http component using mostly default parameters. No security, no additional middlewares." 247 | [api-resource configuration] 248 | (map->Http {:api-resource api-resource 249 | :configuration configuration 250 | :middlewares default-middlewares 251 | :security-handlers {} ; No security by default 252 | :s1st-options default-s1st-options})) 253 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixThreadPool/hystrixThreadPool.js: -------------------------------------------------------------------------------- 1 | 2 | (function(window) { 3 | 4 | // cache the templates we use on this page as global variables (asynchronously) 5 | jQuery.get(getRelativePath("../components/hystrixThreadPool/templates/hystrixThreadPool.html"), function(data) { 6 | htmlTemplate = data; 7 | }); 8 | jQuery.get(getRelativePath("../components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html"), function(data) { 9 | htmlTemplateContainer = data; 10 | }); 11 | 12 | function getRelativePath(path) { 13 | var p = location.pathname.slice(0, location.pathname.lastIndexOf("/")+1); 14 | return p + path; 15 | } 16 | 17 | /** 18 | * Object containing functions for displaying and updating the UI with streaming data. 19 | * 20 | * Publish this externally as "HystrixThreadPoolMonitor" 21 | */ 22 | window.HystrixThreadPoolMonitor = function(containerId) { 23 | 24 | var self = this; // keep scope under control 25 | 26 | this.containerId = containerId; 27 | 28 | /** 29 | * Initialization on construction 30 | */ 31 | // intialize various variables we use for visualization 32 | var maxXaxisForCircle="40%"; 33 | var maxYaxisForCircle="40%"; 34 | var maxRadiusForCircle="125"; 35 | var maxDomain = 2000; 36 | 37 | self.circleRadius = d3.scale.pow().exponent(0.5).domain([0, maxDomain]).range(["5", maxRadiusForCircle]); // requests per second per host 38 | self.circleYaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxXaxisForCircle]); 39 | self.circleXaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxYaxisForCircle]); 40 | self.colorRange = d3.scale.linear().domain([10, 25, 40, 50]).range(["green", "#FFCC00", "#FF9900", "red"]); 41 | self.errorPercentageColorRange = d3.scale.linear().domain([0, 10, 35, 50]).range(["grey", "black", "#FF9900", "red"]); 42 | 43 | /** 44 | * We want to keep sorting in the background since data values are always changing, so this will re-sort every X milliseconds 45 | * to maintain whatever sort the user (or default) has chosen. 46 | * 47 | * In other words, sorting only for adds/deletes is not sufficient as all but alphabetical sort are dynamically changing. 48 | */ 49 | setInterval(function() { 50 | // sort since we have added a new one 51 | self.sortSameAsLast(); 52 | }, 1000) 53 | 54 | /** 55 | * END of Initialization on construction 56 | */ 57 | 58 | /** 59 | * Event listener to handle new messages from EventSource as streamed from the server. 60 | */ 61 | /* public */ self.eventSourceMessageListener = function(e) { 62 | var data = JSON.parse(e.data); 63 | if(data) { 64 | // check for reportingHosts (if not there, set it to 1 for singleHost vs cluster) 65 | if(!data.reportingHosts) { 66 | data.reportingHosts = 1; 67 | } 68 | 69 | if(data && data.type == 'HystrixThreadPool') { 70 | if (data.deleteData == 'true') { 71 | deleteThreadPool(data.escapedName); 72 | } else { 73 | displayThreadPool(data); 74 | } 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Pre process the data before displying in the UI. 81 | * e.g Get Averages from sums, do rate calculation etc. 82 | */ 83 | function preProcessData(data) { 84 | validateData(data); 85 | // escape string used in jQuery & d3 selectors 86 | data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1'); 87 | // do math 88 | converAllAvg(data); 89 | calcRatePerSecond(data); 90 | } 91 | 92 | function converAllAvg(data) { 93 | convertAvg(data, "propertyValue_queueSizeRejectionThreshold", false); 94 | 95 | // the following will break when it becomes a compound string if the property is dynamically changed 96 | convertAvg(data, "propertyValue_metricsRollingStatisticalWindowInMilliseconds", false); 97 | } 98 | 99 | function convertAvg(data, key, decimal) { 100 | if (decimal) { 101 | data[key] = roundNumber(data[key]/data["reportingHosts"]); 102 | } else { 103 | data[key] = Math.floor(data[key]/data["reportingHosts"]); 104 | } 105 | } 106 | 107 | function calcRatePerSecond(data) { 108 | var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000; 109 | 110 | var totalThreadsExecuted = data["rollingCountThreadsExecuted"]; 111 | if (totalThreadsExecuted < 0) { 112 | totalThreadsExecuted = 0; 113 | } 114 | data["ratePerSecond"] = roundNumber(totalThreadsExecuted / numberSeconds); 115 | data["ratePerSecondPerHost"] = roundNumber(totalThreadsExecuted / numberSeconds / data["reportingHosts"]); 116 | } 117 | 118 | function validateData(data) { 119 | 120 | assertNotNull(data,"type"); 121 | assertNotNull(data,"name"); 122 | // assertNotNull(data,"currentTime"); 123 | assertNotNull(data,"currentActiveCount"); 124 | assertNotNull(data,"currentCompletedTaskCount"); 125 | assertNotNull(data,"currentCorePoolSize"); 126 | assertNotNull(data,"currentLargestPoolSize"); 127 | assertNotNull(data,"currentMaximumPoolSize"); 128 | assertNotNull(data,"currentPoolSize"); 129 | assertNotNull(data,"currentQueueSize"); 130 | assertNotNull(data,"currentTaskCount"); 131 | assertNotNull(data,"rollingCountThreadsExecuted"); 132 | assertNotNull(data,"rollingMaxActiveThreads"); 133 | assertNotNull(data,"reportingHosts"); 134 | 135 | assertNotNull(data,"propertyValue_queueSizeRejectionThreshold"); 136 | assertNotNull(data,"propertyValue_metricsRollingStatisticalWindowInMilliseconds"); 137 | } 138 | 139 | function assertNotNull(data, key) { 140 | if(data[key] == undefined) { 141 | if (key == "dependencyOwner") { 142 | data["dependencyOwner"] = data.name; 143 | } else { 144 | throw new Error("Key Missing: " + key + " for " + data.name) 145 | } 146 | } 147 | } 148 | 149 | /** 150 | * Method to display the THREAD_POOL data 151 | * 152 | * @param data 153 | */ 154 | /* private */ function displayThreadPool(data) { 155 | 156 | try { 157 | preProcessData(data); 158 | } catch (err) { 159 | log("Failed preProcessData: " + err.message); 160 | return; 161 | } 162 | 163 | // add the 'addCommas' function to the 'data' object so the HTML templates can use it 164 | data.addCommas = addCommas; 165 | // add the 'roundNumber' function to the 'data' object so the HTML templates can use it 166 | data.roundNumber = roundNumber; 167 | 168 | var addNew = false; 169 | // check if we need to create the container 170 | if(!$('#THREAD_POOL_' + data.escapedName).length) { 171 | // it doesn't exist so add it 172 | var html = tmpl(htmlTemplateContainer, data); 173 | // remove the loading thing first 174 | $('#' + containerId + ' span.loading').remove(); 175 | // get the current last column and remove the 'last' class from it 176 | $('#' + containerId + ' div.last').removeClass('last'); 177 | // now create the new data and add it 178 | $('#' + containerId + '').append(html); 179 | // add the 'last' class to the column we just added 180 | $('#' + containerId + ' div.monitor').last().addClass('last'); 181 | 182 | // add the default sparkline graph 183 | d3.selectAll('#graph_THREAD_POOL_' + data.escapedName + ' svg').append("svg:path"); 184 | 185 | // remember this is new so we can trigger a sort after setting data 186 | addNew = true; 187 | } 188 | 189 | // set the rate on the div element so it's available for sorting 190 | $('#THREAD_POOL_' + data.escapedName).attr('rate_value', data.ratePerSecondPerHost); 191 | 192 | // now update/insert the data 193 | $('#THREAD_POOL_' + data.escapedName + ' div.monitor_data').html(tmpl(htmlTemplate, data)); 194 | 195 | // set variables for circle visualization 196 | var rate = data.ratePerSecondPerHost; 197 | // we will treat each item in queue as 1% of an error visualization 198 | // ie. 5 threads in queue per instance == 5% error percentage 199 | var errorPercentage = data.currentQueueSize / data.reportingHosts; 200 | 201 | updateCircle('#THREAD_POOL_' + data.escapedName + ' circle', rate, errorPercentage); 202 | 203 | if(addNew) { 204 | // sort since we added a new circuit 205 | self.sortSameAsLast(); 206 | } 207 | } 208 | 209 | /* round a number to X digits: num => the number to round, dec => the number of decimals */ 210 | /* private */ function roundNumber(num) { 211 | var dec=1; // we are hardcoding to support only 1 decimal so that our padding logic at the end is simple 212 | var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); 213 | var resultAsString = result.toString(); 214 | if(resultAsString.indexOf('.') == -1) { 215 | resultAsString = resultAsString + '.'; 216 | for(var i=0; i parseInt(maxXaxisForCircle)) { 227 | newXaxisForCircle = maxXaxisForCircle; 228 | } 229 | var newYaxisForCircle = self.circleYaxis(rate); 230 | if(parseInt(newYaxisForCircle) > parseInt(maxYaxisForCircle)) { 231 | newYaxisForCircle = maxYaxisForCircle; 232 | } 233 | var newRadiusForCircle = self.circleRadius(rate); 234 | if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) { 235 | newRadiusForCircle = maxRadiusForCircle; 236 | } 237 | 238 | d3.selectAll(cssTarget) 239 | .transition() 240 | .duration(400) 241 | .attr("cy", newYaxisForCircle) 242 | .attr("cx", newXaxisForCircle) 243 | .attr("r", newRadiusForCircle) 244 | .style("fill", self.colorRange(errorPercentage)); 245 | } 246 | 247 | /* private */ function deleteThreadPool(poolName) { 248 | $('#THREAD_POOL_' + poolName).remove(); 249 | } 250 | 251 | } 252 | 253 | // public methods for sorting 254 | HystrixThreadPoolMonitor.prototype.sortByVolume = function() { 255 | var direction = "desc"; 256 | if(this.sortedBy == 'rate_desc') { 257 | direction = 'asc'; 258 | } 259 | this.sortByVolumeInDirection(direction); 260 | } 261 | 262 | HystrixThreadPoolMonitor.prototype.sortByVolumeInDirection = function(direction) { 263 | this.sortedBy = 'rate_' + direction; 264 | $('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'rate_value'}); 265 | } 266 | 267 | HystrixThreadPoolMonitor.prototype.sortAlphabetically = function() { 268 | var direction = "asc"; 269 | if(this.sortedBy == 'alph_asc') { 270 | direction = 'desc'; 271 | } 272 | this.sortAlphabeticalInDirection(direction); 273 | } 274 | 275 | HystrixThreadPoolMonitor.prototype.sortAlphabeticalInDirection = function(direction) { 276 | this.sortedBy = 'alph_' + direction; 277 | $('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction}); 278 | } 279 | 280 | HystrixThreadPoolMonitor.prototype.sortByMetricInDirection = function(direction, metric) { 281 | $('#' + this.containerId + ' div.monitor').tsort(metric, {order: direction}); 282 | } 283 | 284 | // this method is for when new divs are added to cause the elements to be sorted to whatever the user last chose 285 | HystrixThreadPoolMonitor.prototype.sortSameAsLast = function() { 286 | if(this.sortedBy == 'alph_asc') { 287 | this.sortAlphabeticalInDirection('asc'); 288 | } else if(this.sortedBy == 'alph_desc') { 289 | this.sortAlphabeticalInDirection('desc'); 290 | } else if(this.sortedBy == 'rate_asc') { 291 | this.sortByVolumeInDirection('asc'); 292 | } else if(this.sortedBy == 'rate_desc') { 293 | this.sortByVolumeInDirection('desc'); 294 | } else if(this.sortedBy == 'error_asc') { 295 | this.sortByErrorInDirection('asc'); 296 | } else if(this.sortedBy == 'error_desc') { 297 | this.sortByErrorInDirection('desc'); 298 | } else if(this.sortedBy == 'lat90_asc') { 299 | this.sortByMetricInDirection('asc', 'p90'); 300 | } else if(this.sortedBy == 'lat90_desc') { 301 | this.sortByMetricInDirection('desc', 'p90'); 302 | } else if(this.sortedBy == 'lat99_asc') { 303 | this.sortByMetricInDirection('asc', 'p99'); 304 | } else if(this.sortedBy == 'lat99_desc') { 305 | this.sortByMetricInDirection('desc', 'p99'); 306 | } else if(this.sortedBy == 'lat995_asc') { 307 | this.sortByMetricInDirection('asc', 'p995'); 308 | } else if(this.sortedBy == 'lat995_desc') { 309 | this.sortByMetricInDirection('desc', 'p995'); 310 | } else if(this.sortedBy == 'latMean_asc') { 311 | this.sortByMetricInDirection('asc', 'pMean'); 312 | } else if(this.sortedBy == 'latMean_desc') { 313 | this.sortByMetricInDirection('desc', 'pMean'); 314 | } else if(this.sortedBy == 'latMedian_asc') { 315 | this.sortByMetricInDirection('asc', 'pMedian'); 316 | } else if(this.sortedBy == 'latMedian_desc') { 317 | this.sortByMetricInDirection('desc', 'pMedian'); 318 | } 319 | } 320 | 321 | // default sort type and direction 322 | this.sortedBy = 'alph_asc'; 323 | 324 | 325 | // a temporary home for the logger until we become more sophisticated 326 | function log(message) { 327 | console.log(message); 328 | }; 329 | 330 | function addCommas(nStr){ 331 | nStr += ''; 332 | if(nStr.length <=3) { 333 | return nStr; //shortcut if we don't need commas 334 | } 335 | x = nStr.split('.'); 336 | x1 = x[0]; 337 | x2 = x.length > 1 ? '.' + x[1] : ''; 338 | var rgx = /(\d+)(\d{3})/; 339 | while (rgx.test(x1)) { 340 | x1 = x1.replace(rgx, '$1' + ',' + '$2'); 341 | } 342 | return x1 + x2; 343 | } 344 | })(window) 345 | 346 | 347 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # friboo 2 | 3 | ![Maven Central](https://img.shields.io/maven-central/v/org.zalando.stups/friboo.svg) 4 | [![Build Status](https://travis-ci.org/zalando/friboo.svg?branch=master)](https://travis-ci.org/zalando/friboo) 5 | [![codecov](https://codecov.io/gh/zalando/friboo/branch/master/graph/badge.svg)](https://codecov.io/gh/zalando/friboo) 6 | 7 | **Friboo** is a lightweight utility library for writing microservices in Clojure. It provides several components that you can use with Stuart Sierra's [Component lifecycle framework](https://github.com/stuartsierra/component). 8 | 9 | Friboo encourages an "API First" approach based on the [Swagger specification](http://swagger.io/). As such, the REST API is defined as YAML. 10 | 11 | ## Leiningen dependency 12 | 13 | [org.zalando.stups/friboo 2.0.0] 14 | 15 | ## Why Friboo? 16 | 17 | - Friboo allows you to first define your API in a portable, language-agnostic format, and then implement it (with the help of [swagger1st](https://github.com/sarnowski/swagger1st)). 18 | - It contains ready-made components/building blocks for your applications: An HTTP server, DB access layer, metrics registry, Hystrix dashboard (in case you have compliance requirements to follow), and more. See [Components](#components). 19 | - Pluggable support for all authentication mechanisms (basic, OAuth 2.0, API keys). 20 | - It contains the "glue code" for you, and there is already a recommended way of doing things. 21 | 22 | ## Development Status 23 | 24 | In our production we use an extension library that is based on Friboo: [friboo-ext-zalando](https://github.com/zalando-incubator/friboo-ext-zalando). 25 | See the list at the end of this page. 26 | However, there is always room for improvement, so we're very much open to contributions. For more details, see our [contribution guidelines](CONTRIBUTING.md) and check the Issues Tracker for ways you can help. 27 | 28 | ## Getting Started 29 | 30 | ### Requirements 31 | 32 | * [Leiningen](http://leiningen.org/) 33 | 34 | ### Starting a New Project 35 | 36 | To start a new project based on Friboo, use the Leiningen template: 37 | 38 | $ lein new friboo com.example/friboo-is-awesome 39 | 40 | This will generate a sample project containing some "foobar" logic that can serve as a starting point in your experiments. 41 | 42 | A new directory with name `friboo-is-awesome` will be created in the current directory, containing the following files: 43 | 44 | ``` 45 | friboo-is-awesome 46 | ├── README.md 47 | ├── dev 48 | │   └── user.clj 49 | ├── dev-config.edn 50 | ├── project.clj 51 | ├── resources 52 | │   └── api 53 | │   └── api.yaml 54 | ├── src 55 | │   └── com 56 | │   └── example 57 | │   └── friboo_is_awesome 58 | │   ├── api.clj 59 | │   └── core.clj 60 | └── test 61 | └── com 62 | └── example 63 | └── friboo_is_awesome 64 | ├── api_test.clj 65 | └── core_test.clj 66 | ``` 67 | 68 | * `README.md` contains some pregenerated development tips for the new project. 69 | * `dev/user.clj` contains functions for [Reloaded Workflow](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded). 70 | * `dev-config.edn` contains environment variables that will be used during reloaded workflow (instead of putting them into `profiles.clj`). 71 | * `project.clj` contains the project definition with all dependencies and some additional plugins. 72 | * `resources/api.yaml` contains the [Swagger API definition](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) in .yaml format. 73 | * `src` directory contains these files: 74 | * `core.clj` is the [system](https://github.com/stuartsierra/component#systems) definition. 75 | * `api.clj` contains API endpoint handlers. 76 | * the `test` directory contains unit test examples using both `clojure.test` and [Midje](https://github.com/marick/Midje). 77 | 78 | ## How Friboo works 79 | 80 | There are two core parts in any Friboo application: 81 | 82 | - loading configuration by aggregating many sources 83 | - starting the [system](https://github.com/stuartsierra/component#systems) 84 | 85 | Both these parts are taken care of in `core.clj` in `run` function. The name "run" is not fixed, it can be anything. 86 | 87 | Let's put configuration aside for now. A minimal `run` function might look like this: 88 | 89 | ```clojure 90 | (require '[com.stuartsierra.component :as component] 91 | '[org.zalando.stups.friboo.system.http :as http] 92 | '[org.zalando.stups.friboo.system :as system]) 93 | 94 | (defn run [] 95 | (let [system (component/map->SystemMap 96 | {:http (http/make-http "api.yaml" {})})] 97 | (system/run {} system))) 98 | ``` 99 | 100 | Here we declare a system that has just one component created by `make-http` function. When started, this component will expose a RESTful API 101 | where requests are routed according to the Swagger definition in `api.yaml`, which is taken from the classpath (usually `resources/api.yaml`). 102 | 103 | Then we call `run` from `-main`: 104 | 105 | ```clojure 106 | (defn -main [& args] 107 | (try 108 | (run) 109 | (catch Exception e 110 | (println "Could not start the system because of" (str e)) 111 | (System/exit 1)))) 112 | ``` 113 | 114 | `run` function does not block, it immediately returns the started system that can later be stopped (as reloaded workflow suggests). 115 | 116 | This already works, but it's not too flexible. 117 | 118 | ### Parsing configuration options 119 | 120 | According to https://12factor.net/config, configuration should be provided via environment variables. 121 | However, with REPL-driven [reloaded workflow](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded) you would have to restart the JVM 122 | every time you need to change a configuration value. That's less than perfect. 123 | 124 | Friboo supports several sources of configuration: 125 | 126 | - environment variables: `HTTP_PORT=8081` 127 | - JVM properties: `http.port=8081` 128 | - development configuration from `dev-config.edn`: `{:http-port 8081}` 129 | - default configurations per component (hardcoded) 130 | 131 | Another challenge is — how to give components only the configuration they need? What if more than one component would like to use `PORT` variable? 132 | Friboo solves this with namespacing of configuration parameters. Namespace in this case is just a known prefix: `HTTP_`, `API_`, `ENGINE_` etc. 133 | 134 | Configuration is in loaded inside `run` by `load-config` function before defining the system: 135 | 136 | ```clojure 137 | (defn run [args-config] 138 | (let [config (config/load-config 139 | (merge default-http-config 140 | args-config) 141 | [:http]) 142 | system (component/map->SystemMap 143 | {:http (http/make-http "api.yaml" (:http config))})] 144 | (system/run config system))) 145 | ``` 146 | 147 | Here we make `run` accept a configuration map as an argument, it acts as an additional source of configuration (and it is used by Reloaded Workflow to inject reloadable configuration, see `dev/user.clj`). 148 | 149 | `load-config` takes 2 arguments: 150 | 151 | - map of default configuration that looks like this: 152 | 153 | ```clojure 154 | {:http-port 8081 155 | :db-password "1q2w3e4r5t" 156 | :foo-bar "foobar" 157 | ``` 158 | 159 | - list of namespaces, which are known prefixes configuration variables that we expect: 160 | 161 | ```clojure 162 | [:http :db] 163 | ``` 164 | 165 | Configuration parameters' names are normalized in the following way (this is actually done by [environ](https://github.com/weavejester/environ)): 166 | 167 | - `HTTP_PORT` becomes `:http-port` 168 | - `http.port` becomes `:http-port` 169 | 170 | `load-config` normalizes names in all configuration sources, merges them (real environment overrules the default config), filters the parameters by known prefixes and returns a nested map: 171 | 172 | ```clojure 173 | {:http {:port 8081} 174 | :db {:password "1q2w3e4r5t"}} 175 | ``` 176 | 177 | Note that `:foo-bar` parameter did not make it into the output, because it does not start with `:http-` nor `:db-`. 178 | 179 | After we have this configuration loaded, it's very straightforward to give each component its part: 180 | 181 | ```clojure 182 | {:http (http/make-http "api.yaml" (:http config)) 183 | :db (db/make-db (:db config))} 184 | ``` 185 | 186 | `system/run` also takes the entire configuration as the first argument and uses the `:system` part of it. 187 | 188 | ## Components 189 | 190 | ### HTTP Component 191 | 192 | HTTP component starts a HTTP server and routes the requests based on the Swagger API definition. It lives in `org.zalando.stups.friboo.system.http` namespace. 193 | 194 | It has an optional dependency `:controller` that is given to all 195 | API handlers as first argument. The use case is to make it contain some configuration 196 | and dependencies that the handlers should have access to. 197 | 198 | ```yaml 199 | paths: 200 | '/hello/{name}': 201 | get: 202 | operationId: "com.example.myapp.api/get-hello" 203 | responses: {} 204 | ``` 205 | 206 | Part of system map (we make `:api` component to be a simple map, it's not necessary 207 | for every component to implement `com.stuartsierra.component/Lifecycle` protocol): 208 | 209 | ```clojure 210 | :http (component/using 211 | (http/make-http "api.yaml" (:http config)) 212 | {:controller :api}) 213 | :api {:configuration (:api config)} 214 | ``` 215 | 216 | `{:controller :api}` means that `:api` component will be available to `:http` under the name `:controller`, that's what it expects. 217 | 218 | In `com.example.myapp.api` namespace: 219 | 220 | ```clojure 221 | (defn get-hello [{:keys [configuration]} {:keys [name]} request] 222 | (response {:message (str "Hello " name)})) 223 | ``` 224 | 225 | `get-hello` (and every other API handler function) is called with 3 arguments: 226 | 227 | - `:controller` (`:api` component in our example) 228 | - merged parameters map from path, query and body parameters 229 | - raw request map 230 | 231 | Every handler function is expected to return a map representing a HTTP response: 232 | 233 | ```clojure 234 | {:body {:message "Hello Michael"} 235 | :headers {} 236 | :status 200 237 | ``` 238 | 239 | In our example we use `ring.util.response/response` to create a HTTP 200. 240 | 241 | #### Configuration Options 242 | 243 | * There are all the [configuration options](https://ring-clojure.github.io/ring/ring.adapter.jetty.html) that Jetty supports, for example: 244 | 245 | ```clojure 246 | {:port 8081 247 | :cors-origin "*.zalando.de"} 248 | ``` 249 | 250 | ### DB Component 251 | 252 | DB component encapsulates JDBC connection pool and provides [Flyway](https://flywaydb.org/) to support schema migrations. 253 | 254 | When the component starts, it will have additional `:datasource` key that contains an implementation of `javax.sql.DataSource`. You can use it as you like. 255 | 256 | One of the examples is in friboo-ext-zalando: 257 | 258 | $ lein new friboo-ext-zalando db-example 259 | 260 | Take a look at the following files: 261 | 262 | ``` 263 | example 264 | ├── resources 265 | │   └── db 266 | │   ├── migration 267 | │   │   └── V1__initial_schema.sql 268 | │   └── queries.sql 269 | └── src 270 |    └── db_example 271 |    ├── api.clj 272 |    ├── core.clj 273 |    └── sql.clj 274 | ``` 275 | 276 | #### Configuration Options 277 | 278 | For available options please refer to `org.zalando.stups.friboo.system.db/start-component` 279 | 280 | ### Metrics Component 281 | 282 | The metrics component initializes a [Dropwizard MetricsRegistry](http://metrics.dropwizard.io) to measure 283 | frequency and performance of the Swagger API endpoints; see [HTTP component](#http-component). 284 | 285 | ### Management HTTP component 286 | 287 | This component starts another embedded Jetty at a different port (default 7979) and exposes endpoints used to monitor and manage the application: 288 | 289 | * `/metrics`: A JSON document containing all metrics, gathered by the metrics component 290 | * `/hystrix.stream`: The [Hystrix](https://github.com/Netflix/Hystrix) stream (can be aggregated by [Turbine](https://github.com/Netflix/Turbine)) 291 | * `/monitor/monitor.html`: The Hystrix dashboard 292 | 293 | #### Configuration Options 294 | 295 | All [Jetty configuration options](https://ring-clojure.github.io/ring/ring.adapter.jetty.html). 296 | 297 | ## Real-World Usage 298 | 299 | There are multiple examples of real-world usages of Friboo, including among Zalando's STUPS components: 300 | 301 | * [Pier One Docker registry](https://github.com/zalando-stups/pierone) (REST service with DB and S3 backend) 302 | * [Kio application registry](https://github.com/zalando-stups/kio) (REST service with DB) 303 | * [Even SSH access granting service](https://github.com/zalando-stups/even) (REST service with DB) 304 | * [Essentials](https://github.com/zalando-stups/essentials) (REST service with DB) 305 | 306 | TODO HINT: set java.util.logging.manager= org.apache.logging.log4j.jul.LogManager to have proper JUL logging. 307 | 308 | ## License 309 | 310 | Copyright © 2016 Zalando SE 311 | 312 | Licensed under the Apache License, Version 2.0 (the "License"); 313 | you may not use this file except in compliance with the License. 314 | You may obtain a copy of the License at 315 | 316 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 317 | 318 | Unless required by applicable law or agreed to in writing, software 319 | distributed under the License is distributed on an "AS IS" BASIS, 320 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 321 | See the License for the specific language governing permissions and 322 | limitations under the License. 323 | -------------------------------------------------------------------------------- /resources/hystrix-dashboard/components/hystrixCommand/hystrixCommand.js: -------------------------------------------------------------------------------- 1 | 2 | (function(window) { 3 | 4 | // cache the templates we use on this page as global variables (asynchronously) 5 | jQuery.get(getRelativePath("../components/hystrixCommand/templates/hystrixCircuit.html"), function(data) { 6 | hystrixTemplateCircuit = data; 7 | }); 8 | jQuery.get(getRelativePath("../components/hystrixCommand/templates/hystrixCircuitContainer.html"), function(data) { 9 | hystrixTemplateCircuitContainer = data; 10 | }); 11 | 12 | function getRelativePath(path) { 13 | var p = location.pathname.slice(0, location.pathname.lastIndexOf("/")+1); 14 | return p + path; 15 | } 16 | 17 | /** 18 | * Object containing functions for displaying and updating the UI with streaming data. 19 | * 20 | * Publish this externally as "HystrixCommandMonitor" 21 | */ 22 | window.HystrixCommandMonitor = function(containerId, args) { 23 | 24 | var self = this; // keep scope under control 25 | self.args = args; 26 | if(self.args == undefined) { 27 | self.args = {}; 28 | } 29 | 30 | this.containerId = containerId; 31 | 32 | /** 33 | * Initialization on construction 34 | */ 35 | // intialize various variables we use for visualization 36 | var maxXaxisForCircle="40%"; 37 | var maxYaxisForCircle="40%"; 38 | var maxRadiusForCircle="125"; 39 | 40 | // CIRCUIT_BREAKER circle visualization settings 41 | self.circuitCircleRadius = d3.scale.pow().exponent(0.5).domain([0, 400]).range(["5", maxRadiusForCircle]); // requests per second per host 42 | self.circuitCircleYaxis = d3.scale.linear().domain([0, 400]).range(["30%", maxXaxisForCircle]); 43 | self.circuitCircleXaxis = d3.scale.linear().domain([0, 400]).range(["30%", maxYaxisForCircle]); 44 | self.circuitColorRange = d3.scale.linear().domain([10, 25, 40, 50]).range(["green", "#FFCC00", "#FF9900", "red"]); 45 | self.circuitErrorPercentageColorRange = d3.scale.linear().domain([0, 10, 35, 50]).range(["grey", "black", "#FF9900", "red"]); 46 | 47 | /** 48 | * We want to keep sorting in the background since data values are always changing, so this will re-sort every X milliseconds 49 | * to maintain whatever sort the user (or default) has chosen. 50 | * 51 | * In other words, sorting only for adds/deletes is not sufficient as all but alphabetical sort are dynamically changing. 52 | */ 53 | setInterval(function() { 54 | // sort since we have added a new one 55 | self.sortSameAsLast(); 56 | }, 10000); 57 | 58 | 59 | /** 60 | * END of Initialization on construction 61 | */ 62 | 63 | /** 64 | * Event listener to handle new messages from EventSource as streamed from the server. 65 | */ 66 | /* public */ self.eventSourceMessageListener = function(e) { 67 | var data = JSON.parse(e.data); 68 | if(data) { 69 | // check for reportingHosts (if not there, set it to 1 for singleHost vs cluster) 70 | if(!data.reportingHosts) { 71 | data.reportingHosts = 1; 72 | } 73 | 74 | if(data && data.type == 'HystrixCommand') { 75 | if (data.deleteData == 'true') { 76 | deleteCircuit(data.escapedName); 77 | } else { 78 | displayCircuit(data); 79 | } 80 | } 81 | } 82 | }; 83 | 84 | /** 85 | * Pre process the data before displying in the UI. 86 | * e.g Get Averages from sums, do rate calculation etc. 87 | */ 88 | function preProcessData(data) { 89 | validateData(data); 90 | // escape string used in jQuery & d3 selectors 91 | data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1'); 92 | // do math 93 | converAllAvg(data); 94 | calcRatePerSecond(data); 95 | } 96 | 97 | /** 98 | * Since the stream of data can be aggregated from multiple hosts in a tiered manner 99 | * the aggregation just sums everything together and provides us the denominator (reportingHosts) 100 | * so we must divide by it to get an average per instance value. 101 | * 102 | * We want to do this on any numerical values where we want per instance rather than cluster-wide sum. 103 | */ 104 | function converAllAvg(data) { 105 | convertAvg(data, "errorPercentage", true); 106 | convertAvg(data, "latencyExecute_mean", false); 107 | convertAvg(data, "latencyTotal_mean", false); 108 | 109 | // the following will break when it becomes a compound string if the property is dynamically changed 110 | convertAvg(data, "propertyValue_metricsRollingStatisticalWindowInMilliseconds", false); 111 | } 112 | 113 | function convertAvg(data, key, decimal) { 114 | if (decimal) { 115 | data[key] = getInstanceAverage(data[key], data["reportingHosts"], decimal); 116 | } else { 117 | data[key] = getInstanceAverage(data[key], data["reportingHosts"], decimal); 118 | } 119 | } 120 | 121 | function getInstanceAverage(value, reportingHosts, decimal) { 122 | if (decimal) { 123 | return roundNumber(value/reportingHosts); 124 | } else { 125 | return Math.floor(value/reportingHosts); 126 | } 127 | } 128 | 129 | function calcRatePerSecond(data) { 130 | var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000; 131 | 132 | var totalRequests = data["requestCount"]; 133 | if (totalRequests < 0) { 134 | totalRequests = 0; 135 | } 136 | data["ratePerSecond"] = roundNumber(totalRequests / numberSeconds); 137 | data["ratePerSecondPerHost"] = roundNumber(totalRequests / numberSeconds / data["reportingHosts"]) ; 138 | } 139 | 140 | function validateData(data) { 141 | assertNotNull(data,"reportingHosts"); 142 | assertNotNull(data,"type"); 143 | assertNotNull(data,"name"); 144 | assertNotNull(data,"group"); 145 | // assertNotNull(data,"currentTime"); 146 | assertNotNull(data,"isCircuitBreakerOpen"); 147 | assertNotNull(data,"errorPercentage"); 148 | assertNotNull(data,"errorCount"); 149 | assertNotNull(data,"requestCount"); 150 | assertNotNull(data,"rollingCountCollapsedRequests"); 151 | assertNotNull(data,"rollingCountExceptionsThrown"); 152 | assertNotNull(data,"rollingCountFailure"); 153 | assertNotNull(data,"rollingCountFallbackFailure"); 154 | assertNotNull(data,"rollingCountFallbackRejection"); 155 | assertNotNull(data,"rollingCountFallbackSuccess"); 156 | assertNotNull(data,"rollingCountResponsesFromCache"); 157 | assertNotNull(data,"rollingCountSemaphoreRejected"); 158 | assertNotNull(data,"rollingCountShortCircuited"); 159 | assertNotNull(data,"rollingCountSuccess"); 160 | assertNotNull(data,"rollingCountThreadPoolRejected"); 161 | assertNotNull(data,"rollingCountTimeout"); 162 | assertNotNull(data,"currentConcurrentExecutionCount"); 163 | assertNotNull(data,"latencyExecute_mean"); 164 | assertNotNull(data,"latencyExecute"); 165 | assertNotNull(data,"latencyTotal_mean"); 166 | assertNotNull(data,"latencyTotal"); 167 | assertNotNull(data,"propertyValue_circuitBreakerRequestVolumeThreshold"); 168 | assertNotNull(data,"propertyValue_circuitBreakerSleepWindowInMilliseconds"); 169 | assertNotNull(data,"propertyValue_circuitBreakerErrorThresholdPercentage"); 170 | assertNotNull(data,"propertyValue_circuitBreakerForceOpen"); 171 | assertNotNull(data,"propertyValue_executionIsolationStrategy"); 172 | assertNotNull(data,"propertyValue_executionIsolationThreadTimeoutInMilliseconds"); 173 | assertNotNull(data,"propertyValue_executionIsolationThreadInterruptOnTimeout"); 174 | // assertNotNull(data,"propertyValue_executionIsolationThreadPoolKeyOverride"); 175 | assertNotNull(data,"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests"); 176 | assertNotNull(data,"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests"); 177 | assertNotNull(data,"propertyValue_requestCacheEnabled"); 178 | assertNotNull(data,"propertyValue_requestLogEnabled"); 179 | assertNotNull(data,"propertyValue_metricsRollingStatisticalWindowInMilliseconds"); 180 | } 181 | 182 | function assertNotNull(data, key) { 183 | if(data[key] == undefined) { 184 | throw new Error("Key Missing: " + key + " for " + data.name); 185 | } 186 | } 187 | 188 | /** 189 | * Method to display the CIRCUIT data 190 | * 191 | * @param data 192 | */ 193 | /* private */ function displayCircuit(data) { 194 | 195 | try { 196 | preProcessData(data); 197 | } catch (err) { 198 | log("Failed preProcessData: " + err.message); 199 | return; 200 | } 201 | 202 | // add the 'addCommas' function to the 'data' object so the HTML templates can use it 203 | data.addCommas = addCommas; 204 | // add the 'roundNumber' function to the 'data' object so the HTML templates can use it 205 | data.roundNumber = roundNumber; 206 | // add the 'getInstanceAverage' function to the 'data' object so the HTML templates can use it 207 | data.getInstanceAverage = getInstanceAverage; 208 | 209 | var addNew = false; 210 | // check if we need to create the container 211 | if(!$('#CIRCUIT_' + data.escapedName).length) { 212 | // args for display 213 | if(self.args.includeDetailIcon != undefined && self.args.includeDetailIcon) { 214 | data.includeDetailIcon = true; 215 | }else { 216 | data.includeDetailIcon = false; 217 | } 218 | 219 | // it doesn't exist so add it 220 | var html = tmpl(hystrixTemplateCircuitContainer, data); 221 | // remove the loading thing first 222 | $('#' + containerId + ' span.loading').remove(); 223 | // now create the new data and add it 224 | $('#' + containerId + '').append(html); 225 | 226 | // add the default sparkline graph 227 | d3.selectAll('#graph_CIRCUIT_' + data.escapedName + ' svg').append("svg:path"); 228 | 229 | // remember this is new so we can trigger a sort after setting data 230 | addNew = true; 231 | } 232 | 233 | 234 | // now update/insert the data 235 | $('#CIRCUIT_' + data.escapedName + ' div.monitor_data').html(tmpl(hystrixTemplateCircuit, data)); 236 | 237 | var ratePerSecond = data.ratePerSecond; 238 | var ratePerSecondPerHost = data.ratePerSecondPerHost; 239 | var ratePerSecondPerHostDisplay = ratePerSecondPerHost; 240 | var errorThenVolume = isNaN( ratePerSecond )? -1: (data.errorPercentage * 100000000) + ratePerSecond; 241 | // set the rates on the div element so it's available for sorting 242 | $('#CIRCUIT_' + data.escapedName).attr('rate_value', ratePerSecond); 243 | $('#CIRCUIT_' + data.escapedName).attr('error_then_volume', errorThenVolume); 244 | 245 | // update errorPercentage color on page 246 | $('#CIRCUIT_' + data.escapedName + ' a.errorPercentage').css('color', self.circuitErrorPercentageColorRange(data.errorPercentage)); 247 | 248 | updateCircle('circuit', '#CIRCUIT_' + data.escapedName + ' circle', ratePerSecondPerHostDisplay, data.errorPercentage); 249 | 250 | if(data.graphValues) { 251 | // we have a set of values to initialize with 252 | updateSparkline('circuit', '#CIRCUIT_' + data.escapedName + ' path', data.graphValues); 253 | } else { 254 | updateSparkline('circuit', '#CIRCUIT_' + data.escapedName + ' path', ratePerSecond); 255 | } 256 | 257 | if(addNew) { 258 | // sort since we added a new circuit 259 | self.sortSameAsLast(); 260 | } 261 | } 262 | 263 | /* round a number to X digits: num => the number to round, dec => the number of decimals */ 264 | /* private */ function roundNumber(num) { 265 | var dec=1; 266 | var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); 267 | var resultAsString = result.toString(); 268 | if(resultAsString.indexOf('.') == -1) { 269 | resultAsString = resultAsString + '.0'; 270 | } 271 | return resultAsString; 272 | }; 273 | 274 | 275 | 276 | 277 | /* private */ function updateCircle(variablePrefix, cssTarget, rate, errorPercentage) { 278 | var newXaxisForCircle = self[variablePrefix + 'CircleXaxis'](rate); 279 | if(parseInt(newXaxisForCircle) > parseInt(maxXaxisForCircle)) { 280 | newXaxisForCircle = maxXaxisForCircle; 281 | } 282 | var newYaxisForCircle = self[variablePrefix + 'CircleYaxis'](rate); 283 | if(parseInt(newYaxisForCircle) > parseInt(maxYaxisForCircle)) { 284 | newYaxisForCircle = maxYaxisForCircle; 285 | } 286 | var newRadiusForCircle = self[variablePrefix + 'CircleRadius'](rate); 287 | if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) { 288 | newRadiusForCircle = maxRadiusForCircle; 289 | } 290 | 291 | d3.selectAll(cssTarget) 292 | .transition() 293 | .duration(400) 294 | .attr("cy", newYaxisForCircle) 295 | .attr("cx", newXaxisForCircle) 296 | .attr("r", newRadiusForCircle) 297 | .style("fill", self[variablePrefix + 'ColorRange'](errorPercentage)); 298 | } 299 | 300 | /* private */ function updateSparkline(variablePrefix, cssTarget, newDataPoint) { 301 | var currentTimeMilliseconds = new Date().getTime(); 302 | var data = self[variablePrefix + cssTarget + '_data']; 303 | if(typeof data == 'undefined') { 304 | // else it's new 305 | if(typeof newDataPoint == 'object') { 306 | // we received an array of values, so initialize with it 307 | data = newDataPoint; 308 | } else { 309 | // v: VALUE, t: TIME_IN_MILLISECONDS 310 | data = [{"v":parseFloat(newDataPoint),"t":currentTimeMilliseconds}]; 311 | } 312 | self[variablePrefix + cssTarget + '_data'] = data; 313 | } else { 314 | if(typeof newDataPoint == 'object') { 315 | /* if an array is passed in we'll replace the cached one */ 316 | data = newDataPoint; 317 | } else { 318 | // else we just add to the existing one 319 | data.push({"v":parseFloat(newDataPoint),"t":currentTimeMilliseconds}); 320 | } 321 | } 322 | 323 | while(data.length > 200) { // 400 should be plenty for the 2 minutes we have the scale set to below even with a very low update latency 324 | // remove data so we don't keep increasing forever 325 | data.shift(); 326 | } 327 | 328 | if(data.length == 1 && data[0].v == 0) { 329 | //console.log("we have a single 0 so skipping"); 330 | // don't show if we have a single 0 331 | return; 332 | } 333 | 334 | if(data.length > 1 && data[0].v == 0 && data[1].v != 0) { 335 | //console.log("we have a leading 0 so removing it"); 336 | // get rid of a leading 0 if the following number is not a 0 337 | data.shift(); 338 | } 339 | 340 | var xScale = d3.time.scale().domain([new Date(currentTimeMilliseconds-(60*1000*2)), new Date(currentTimeMilliseconds)]).range([0, 140]); 341 | 342 | var yMin = d3.min(data, function(d) { return d.v; }); 343 | var yMax = d3.max(data, function(d) { return d.v; }); 344 | var yScale = d3.scale.linear().domain([yMin, yMax]).nice().range([60, 0]); // y goes DOWN, so 60 is the "lowest" 345 | 346 | sparkline = d3.svg.line() 347 | // assign the X function to plot our line as we wish 348 | .x(function(d,i) { 349 | // return the X coordinate where we want to plot this datapoint based on the time 350 | return xScale(new Date(d.t)); 351 | }) 352 | .y(function(d) { 353 | return yScale(d.v); 354 | }) 355 | .interpolate("basis"); 356 | 357 | d3.selectAll(cssTarget).attr("d", sparkline(data)); 358 | } 359 | 360 | /* private */ function deleteCircuit(circuitName) { 361 | $('#CIRCUIT_' + circuitName).remove(); 362 | } 363 | 364 | }; 365 | 366 | // public methods for sorting 367 | HystrixCommandMonitor.prototype.sortByVolume = function() { 368 | var direction = "desc"; 369 | if(this.sortedBy == 'rate_desc') { 370 | direction = 'asc'; 371 | } 372 | this.sortByVolumeInDirection(direction); 373 | }; 374 | 375 | HystrixCommandMonitor.prototype.sortByVolumeInDirection = function(direction) { 376 | this.sortedBy = 'rate_' + direction; 377 | $('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'rate_value'}); 378 | }; 379 | 380 | HystrixCommandMonitor.prototype.sortAlphabetically = function() { 381 | var direction = "asc"; 382 | if(this.sortedBy == 'alph_asc') { 383 | direction = 'desc'; 384 | } 385 | this.sortAlphabeticalInDirection(direction); 386 | }; 387 | 388 | HystrixCommandMonitor.prototype.sortAlphabeticalInDirection = function(direction) { 389 | this.sortedBy = 'alph_' + direction; 390 | $('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction}); 391 | }; 392 | 393 | 394 | HystrixCommandMonitor.prototype.sortByError = function() { 395 | var direction = "desc"; 396 | if(this.sortedBy == 'error_desc') { 397 | direction = 'asc'; 398 | } 399 | this.sortByErrorInDirection(direction); 400 | }; 401 | 402 | HystrixCommandMonitor.prototype.sortByErrorInDirection = function(direction) { 403 | this.sortedBy = 'error_' + direction; 404 | $('#' + this.containerId + ' div.monitor').tsort(".errorPercentage .value", {order: direction}); 405 | }; 406 | 407 | HystrixCommandMonitor.prototype.sortByErrorThenVolume = function() { 408 | var direction = "desc"; 409 | if(this.sortedBy == 'error_then_volume_desc') { 410 | direction = 'asc'; 411 | } 412 | this.sortByErrorThenVolumeInDirection(direction); 413 | }; 414 | 415 | HystrixCommandMonitor.prototype.sortByErrorThenVolumeInDirection = function(direction) { 416 | this.sortedBy = 'error_then_volume_' + direction; 417 | $('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'error_then_volume'}); 418 | }; 419 | 420 | HystrixCommandMonitor.prototype.sortByLatency90 = function() { 421 | var direction = "desc"; 422 | if(this.sortedBy == 'lat90_desc') { 423 | direction = 'asc'; 424 | } 425 | this.sortedBy = 'lat90_' + direction; 426 | this.sortByMetricInDirection(direction, ".latency90 .value"); 427 | }; 428 | 429 | HystrixCommandMonitor.prototype.sortByLatency99 = function() { 430 | var direction = "desc"; 431 | if(this.sortedBy == 'lat99_desc') { 432 | direction = 'asc'; 433 | } 434 | this.sortedBy = 'lat99_' + direction; 435 | this.sortByMetricInDirection(direction, ".latency99 .value"); 436 | }; 437 | 438 | HystrixCommandMonitor.prototype.sortByLatency995 = function() { 439 | var direction = "desc"; 440 | if(this.sortedBy == 'lat995_desc') { 441 | direction = 'asc'; 442 | } 443 | this.sortedBy = 'lat995_' + direction; 444 | this.sortByMetricInDirection(direction, ".latency995 .value"); 445 | }; 446 | 447 | HystrixCommandMonitor.prototype.sortByLatencyMean = function() { 448 | var direction = "desc"; 449 | if(this.sortedBy == 'latMean_desc') { 450 | direction = 'asc'; 451 | } 452 | this.sortedBy = 'latMean_' + direction; 453 | this.sortByMetricInDirection(direction, ".latencyMean .value"); 454 | }; 455 | 456 | HystrixCommandMonitor.prototype.sortByLatencyMedian = function() { 457 | var direction = "desc"; 458 | if(this.sortedBy == 'latMedian_desc') { 459 | direction = 'asc'; 460 | } 461 | this.sortedBy = 'latMedian_' + direction; 462 | this.sortByMetricInDirection(direction, ".latencyMedian .value"); 463 | }; 464 | 465 | HystrixCommandMonitor.prototype.sortByMetricInDirection = function(direction, metric) { 466 | $('#' + this.containerId + ' div.monitor').tsort(metric, {order: direction}); 467 | }; 468 | 469 | // this method is for when new divs are added to cause the elements to be sorted to whatever the user last chose 470 | HystrixCommandMonitor.prototype.sortSameAsLast = function() { 471 | if(this.sortedBy == 'alph_asc') { 472 | this.sortAlphabeticalInDirection('asc'); 473 | } else if(this.sortedBy == 'alph_desc') { 474 | this.sortAlphabeticalInDirection('desc'); 475 | } else if(this.sortedBy == 'rate_asc') { 476 | this.sortByVolumeInDirection('asc'); 477 | } else if(this.sortedBy == 'rate_desc') { 478 | this.sortByVolumeInDirection('desc'); 479 | } else if(this.sortedBy == 'error_asc') { 480 | this.sortByErrorInDirection('asc'); 481 | } else if(this.sortedBy == 'error_desc') { 482 | this.sortByErrorInDirection('desc'); 483 | } else if(this.sortedBy == 'error_then_volume_asc') { 484 | this.sortByErrorThenVolumeInDirection('asc'); 485 | } else if(this.sortedBy == 'error_then_volume_desc') { 486 | this.sortByErrorThenVolumeInDirection('desc'); 487 | } else if(this.sortedBy == 'lat90_asc') { 488 | this.sortByMetricInDirection('asc', '.latency90 .value'); 489 | } else if(this.sortedBy == 'lat90_desc') { 490 | this.sortByMetricInDirection('desc', '.latency90 .value'); 491 | } else if(this.sortedBy == 'lat99_asc') { 492 | this.sortByMetricInDirection('asc', '.latency99 .value'); 493 | } else if(this.sortedBy == 'lat99_desc') { 494 | this.sortByMetricInDirection('desc', '.latency99 .value'); 495 | } else if(this.sortedBy == 'lat995_asc') { 496 | this.sortByMetricInDirection('asc', '.latency995 .value'); 497 | } else if(this.sortedBy == 'lat995_desc') { 498 | this.sortByMetricInDirection('desc', '.latency995 .value'); 499 | } else if(this.sortedBy == 'latMean_asc') { 500 | this.sortByMetricInDirection('asc', '.latencyMean .value'); 501 | } else if(this.sortedBy == 'latMean_desc') { 502 | this.sortByMetricInDirection('desc', '.latencyMean .value'); 503 | } else if(this.sortedBy == 'latMedian_asc') { 504 | this.sortByMetricInDirection('asc', '.latencyMedian .value'); 505 | } else if(this.sortedBy == 'latMedian_desc') { 506 | this.sortByMetricInDirection('desc', '.latencyMedian .value'); 507 | } 508 | }; 509 | 510 | // default sort type and direction 511 | this.sortedBy = 'alph_asc'; 512 | 513 | 514 | // a temporary home for the logger until we become more sophisticated 515 | function log(message) { 516 | console.log(message); 517 | }; 518 | 519 | function addCommas(nStr){ 520 | nStr += ''; 521 | if(nStr.length <=3) { 522 | return nStr; //shortcut if we don't need commas 523 | } 524 | x = nStr.split('.'); 525 | x1 = x[0]; 526 | x2 = x.length > 1 ? '.' + x[1] : ''; 527 | var rgx = /(\d+)(\d{3})/; 528 | while (rgx.test(x1)) { 529 | x1 = x1.replace(rgx, '$1' + ',' + '$2'); 530 | } 531 | return x1 + x2; 532 | } 533 | })(window); 534 | 535 | 536 | --------------------------------------------------------------------------------