Repository: otto-de/tesla-microservice Branch: master Commit: b40ff9ba3c30 Files: 46 Total size: 92.6 KB Directory structure: gitextract_11uzdn14/ ├── .gitignore ├── .tool-versions ├── .travis.yml ├── CHANGES.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS ├── OSSMETADATA ├── README.md ├── dev-resources/ │ └── logback.xml ├── project.clj ├── resources/ │ ├── default.edn │ └── default.properties ├── src/ │ ├── data_readers.clj │ └── de/ │ └── otto/ │ └── tesla/ │ ├── middleware/ │ │ └── exceptions.clj │ ├── stateful/ │ │ ├── app_status.clj │ │ ├── auth.clj │ │ ├── configuring.clj │ │ ├── handler.clj │ │ ├── health.clj │ │ ├── keep_alive.clj │ │ ├── metering.clj │ │ └── scheduler.clj │ ├── system.clj │ └── util/ │ ├── env_var_reader.clj │ ├── keyword.clj │ └── sanitize.clj ├── test/ │ └── de/ │ └── otto/ │ └── tesla/ │ ├── reporter/ │ │ └── prometheus_test.clj │ ├── stateful/ │ │ ├── app_status_test.clj │ │ ├── configuring_test.clj │ │ ├── handler_test.clj │ │ ├── health_test.clj │ │ ├── keep_alive_test.clj │ │ └── scheduler_test.clj │ ├── system_test.clj │ └── util/ │ ├── env_var_reader_test.clj │ ├── keyword_test.clj │ ├── sanitize_test.clj │ └── test_utils_test.clj ├── test-resources/ │ ├── local.edn │ ├── local.properties │ ├── logback-test.xml │ ├── private.edn │ ├── test.edn │ └── version.properties └── test-utils/ └── de/ └── otto/ └── tesla/ └── util/ └── test_utils.clj ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea .lein* target .nrepl-port *.iml pom.xml pom.xml.asc /.clj-kondo/ /.lsp/ ================================================ FILE: .tool-versions ================================================ terraform 1.8.5 awscli 2.16.10 tfenv 0.4.0 ================================================ FILE: .travis.yml ================================================ language: clojure ================================================ FILE: CHANGES.md ================================================ ## Changes _tesla-microservice_ is used for a number of different services now. Still it is a work in progress. This section will document changes and give instructions on breaking ones. Likely you will find corresponding changes in [tesla-examples](https://github.com/otto-de/tesla-examples). ### 0.15.0 Changed securing of internal endpoints. Moved from providing separate auth-functions to components app-status and metering to an auth-middleware provided to the base-system, see [Securing internal info endpoints](https://github.com/otto-de/tesla-microservice#securing-internal-info-endpoints) ### 0.14.0 This release cleans up remnants of past eperiments and unused functionality. This leads to breaking changes. - Remove ```register-timed-handler``` from ```handler``` namespace. Use ```goo/timing-middleware``` instead. - Remove ```register-response-fn``` from ```handler``` namespace. This was only used internally. - Move internally used middlewares to separate namespaces. - Remove support for reporting via graphite from ```metering``` namespace. Only support prometheus reporting via goo and iapetos for the moment. - Remove ```SchedulerPool``` protocol from ```scheduler``` namespace. Use ```(:pool scheduler)``` instead of ```(SchedulerPool/pool scheduler)``` ### 0.11.0 Utilize the iapetos library as main metrics library. Tesla-Microservice is now able to report to graphite as well as prometheus. For configuration of graphite and prometheus reporters please see the updated README. de.otto.tesla.metrics.prometheus.core now provides some useful instrumentation functions/macros. ### 0.8.0 You are now able to override the name of the base config file via the runtime config. The following example will make the configuring component disgregard ```default.edn``` and use ```not-default.edn``` instead. This might be useful when deploying several applications from one repo. ```edn { :default-cfg-file-name "not-default" } ``` ### 0.6.0 The behaviour of loading configuration changed. * When using configuration via `properties` files, system properties and environment variables are not loaded by default any more. Use `:merge-env-to-properties-config true` in runtime config to achieve prior behaviour. * For the config-file `application.edn`/`application.properties` (name can be overriden by env-var `$CONFIG_FILE`) is now with preference loaded as a resource from classpath. If the resource is not found, it is tried to load it as a file. ### 0.5.0 The scheduler is now part of the tesla-base-system. Per default no threads are kept in the thread-pool it manages. ### 0.4.0 The scheduler does not have the `de.otto.tesla.stateful.scheduler/schedule` function anymore. Instead it only wraps the overtone pool and provides it via `de.otto.tesla.stateful.scheduler/pool`. The pool then can be used with the overtone API like that: ```clj (overtone.at-at/every 100 #(println "Hello world") (de.otto.tesla.stateful.scheduler/pool scheduler) :desc "HelloWord Task") ``` ### 0.1.24 Config can be provided via EDN-files. Those files are looked up and merged: * `default.edn` * `{your-custom}.edn` * `local.edn` The `{your-custom.edn}` can be specified via a ENV-variable named `$CONFIG_FILE`. All EDN-config-files have to be located somewhere in the class path. Even though the old properties-files are considered deprecated and will go away with future releases, you can still use them, if you specify `:property-file-preferred` in the runtime-config of your system: ```edn { :property-file-preferred true } ``` ### 0.1.17 Fix wrapping of middleware to not apply to all routes in the application, which created problems with POST-request. ### 0.1.16 Speedup of unit-tests (and possibly runtime behaviour) by simpler implmentation of the `:keep-alive`-component. ### 0.1.15 The function ```de.otto.tesla.system/start-system``` is renamed to ```start```, ```de.otto.tesla.system/empty-system``` is renamed to ```base-system```. _tesla-microservice_ does not come with an embedded jetty server out of the box anymore. To go on with jetty as before, add the new dependency in ```project.clj```: ```clojure [de.otto/tesla-microservice "0.1.15"] [de.otto/tesla-jetty "0.1.0"] ``` Add the server to your system before you start it. Pass any additional dependencies of the server (```:example-page``` in this case). ```clojure (system/start (serving-with-jetty/add-server (example-system {}) :example-page)) ``` A working example for this is in the [simple-example](https://github.com/otto-de/tesla-examples/tree/master/simple-example). You can also use the ```->```-threading macro as demonstrated in the [mongo-example](https://github.com/otto-de/tesla-examples/tree/master/mongo-example). ### 0.1.14 The `routes`-component was abandoned in favour of the `handler`-component. In the ring library, handlers are the thing to push around (wrapping routes and middleware). You can choose your routing library now. Instead of [compojure](https://github.com/weavejester/compojure) you could also use e.g. [bidi](https://github.com/juxt/bidi). Change components relying on the old ```routes```-component should be trivial: Instead of adding a vector of (compojure)-routes using ```de.otto.tesla.stateful.routes/register-routes```, ```clojure (routes/register-routes routes [(c/GET "/test" [] (test-fn))]) ``` just add a single ring handler using ```de.otto.tesla.stateful.handler/register-handler``` like this: ```clojure (handlers/register-handler handler (c/routes (c/GET "/test" [] (test-fn)))) ``` Add multiple routes like this: ```clojure (handlers/register-handler handler (c/routes (c/GET "/route1" [] (test-fn)) (c/GET "/route1" [] (test-fn2)))) ``` Note that the keyword for the dependency changed from ```:routes``` to ```:handler``` in the base system. ### 0.1.13 Specific logging-dependencies and the escaping-messageconverter have been removed. You now have to (read: you are able to) configure logging yourself in your app. To exactly restore the old behaviour add these dependencies to you own application: ```clojure [org.slf4j/slf4j-api "1.7.12"] [ch.qos.logback/logback-core "1.1.3"] [ch.qos.logback/logback-classic "1.1.3"] [de.otto/escaping-messageconverter "0.1.1"] ``` in your ```logback.xml``` replace ```xml ``` with ```xml ``` ================================================ FILE: Dockerfile ================================================ # This is an example Dockerfile for running a tesla-microservice app in a docker container. # # Instructions: # 1. build uber jar: # ./lein.sh clean # ./lein.sh uberjar # 2. build docker image # docker build -t tesla-example:latest . # 3. run docker container # docker run -d -p 8080:8080 tesla-example:latest FROM centos:6 MAINTAINER Felix Bechstein EXPOSE 8080 # prepare image RUN yum install -y java-1.8.0-openjdk-headless USER daemon # set command line CMD ["java", "-Dlog_level=info", "-jar", "/tesla-microservice-standalone.jar"] # instead of logging to stdout, you may log to file in /log. create volume or mount host volume to /log # RUN mkdir /log && chown daemon /log # CMD ["java", "-Dlog_level=info", "-Dlog_appender=fileAppender", "-Dlog_location=/log", "-jar", "/tesla-microservice-standalone.jar"] # drop in uber jar ADD target/tesla-microservice-*-standalone.jar /tesla-microservice-standalone.jar ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS ================================================ Team Tesla ================================================ FILE: OSSMETADATA ================================================ osslifecycle=active ================================================ FILE: README.md ================================================ # tesla-microservice > "If Edison had a needle to find in a haystack, he would proceed at once with the diligence of the bee to examine straw after straw until he found the object of his search." - Nikola Tesla This is the common basis for some of otto.de's microservices. It is written in clojure using the [component framework](https://github.com/stuartsierra/component). [![Clojars Project](http://clojars.org/de.otto/tesla-microservice/latest-version.svg)](http://clojars.org/de.otto/tesla-microservice) [![Build Status](https://travis-ci.org/otto-de/tesla-microservice.svg)](https://travis-ci.org/otto-de/tesla-microservice) [![Dependencies Status](http://jarkeeper.com/otto-de/tesla-microservice/status.svg)](http://jarkeeper.com/otto-de/tesla-microservice) ## Breaking changes _tesla-microservice_ is used for a number of different services now. Still it is a work in progress. See [CHANGES.md](./CHANGES.md) for instructions on breaking changes. ## Features included * Load configuration from filesystem. * Aggregate a status. * Execute functions with a scheduler * Reply to a health check. * Deliver a json status report. * Report to graphite using the metrics library. * Manage handlers using ring. * Optional auto-hot-reloading of changed source files * Shutdown gracefully. If necessary delayed, so load-balancers have time to notice. ## Examples * A growing set of example applications can be found at [tesla-examples](https://github.com/otto-de/tesla-examples). * David & Germán created an example application based, among other, on tesla-microservice. They wrote a very instructive [blog post about it](http://blog.agilityfeat.com/2015/03/clojure-walking-skeleton/) * Moritz created [tesla-pubsub-service](https://bitbucket.org/DerGuteMoritz/tesla-pubsub-service). It showcases how to connect components via core.async channels. Also the embedded jetty was replaced by immutant. ### Scheduler The scheduler wraps a thread-pool which can be used for scheduling tasks. It is based on [overtones at-at](https://github.com/overtone/at-at) project. To actually use it you have to pass the `:scheduler` as a dependency to the component in which it should be used. Afterwards you can schedule tasks using the overtone api like this: ```clj (overtone.at-at/every 100 #(println "Hello world") (de.otto.tesla.stateful.scheduler/pool scheduler) :desc "HelloWord Task") ``` The overtone-pool wrapped by the scheduler can be configured by the config-entry `:scheduler`. (See `overtone.at-at/mk-pool`) By default the pool holds no threads. ### app-status The app-status indicates the current status of your microservice. To use it you can register a status function to it. Here is a simple example for a function that checks if an atom is empty or not. ```clj (de.otto.tesla.stateful.app-status/register-status-fun app-status #(status atom)) ``` The `app-status` is injected under the keyword :app-status from the base system. ```clj (defn status [atom] (let [status (if @atom :error :ok) message (if @atom "Atom is empty" "Atom is not empty")] (de.otto.status/status-detail :status-id status message))) ``` For further information and usages take a look at the: [status library](https://github.com/otto-de/status) ## Choosing a server As of version ```0.1.15``` there is no server included any more directly in _tesla-microservice_. This gives you the freedom to a) not use any server at all (e.g. for embedded use) b) choose another server e.g. a non-blocking one like httpkit or immutant. The available options are: * [tesla-jetty](https://github.com/otto-de/tesla-jetty): The tried and tested embedded jetty. * [tesla-httpkit](https://github.com/otto-de/tesla-httpkit): The non-blocking httpkit. ## Configuring Applications build with `tesla-microservices` can be configured via `edn`-files, that have to be located in the class path. For backwards compatibility, it is also possible to load config from `properties`-files. See below for noteworthy differences. ### Order of loading and merging 1. A file named `default.edn` is loaded as a resource from classpath if present. 2. A file either named `application.edn` or overridden by the ENV-variable `$CONFIG_FILE` is loaded as a resource or, if that is not possible, from the filesystem. 3. A file name `local.edn` is loaded from classpath if present. The configuration hash-map in those files is loaded and merged in the specified order. Which mean configurations for the same key is overridden by the latter occurrence. ### ENV-variables In contrast to former versions of `tesla-microservice` ENV-variables are not merged into the configuration. But you can easily specify ENV-variables, that should be accessible in your configuration: ```edn { :my-app-secret #ts/env [:my-env-dep-app-secret "default"] } ``` ENV-variables are read with [environ](https://github.com/weavejester/environ). To see which keyword represents which ENV-var have a look in their docs. ### Configuring via properties files For backwards compatibility, it is also possible to load config from `properties`-files. You'll have to pass `{:property-file-preferred true}` as a runtime config to the base-system. It is not possible to load individual environment variables when using properties config. Adding `:merge-env-to-properties-config true` to the runtime config will add all system properties and environment variables, overiding any config from files. ### Reporters Applications utilizing Tesla-Microservice can use [iapetos prometheus client](https://github.com/xsc/iapetos) for monitoring. Metrics are send by reporters which can be configured using the `:metrics` keyword. Each configured reporter will start at system startup automatically. See example configuration below for all supported reporters. ```clojure :metrics {:graphite {:host "localhost" :port "2003" :prefix "my.prefix" :interval-in-s 60 :include-hostname :first-part} :prometheus {:metrics-path "/metrics"}} ``` ## Automatic hot-reloading of changed source files Restarting the whole system after a small change can be cumbersome. A _tesla-microservice_ can detect changes to your source files and load them into a running server. Add this to your config, to check for changes on each request to your system: ```edn {:handler {:hot-reload? true}} ``` _Note_: This should only be enabled in development mode. Use your `local.edn` to enable this feature safely. You can add a `private.edn` as well for personal configurations. This file should be added to your `.gitignore`. ## Securing internal info endpoints The Tesla-Microservice comes with endpoints that hold information about the internal state of your application. Those endpoints can be the app-status or even metrics (Prometheus, see above). To secure those endpoints you can provide an authentication-middleware to the base-system. E.g.: ```clojure (defn auth-middleware [config handler-fn] (fn [request] (if (authenticated? config request) (handler-fn request) {:status 401 :body "access denied"}))) (defn example-system [runtime-config] (-> (de.otto.tesla.system/base-system runtime-config auth-middleware))) ``` ## Addons The basis included is stripped to the very minimum. Additional functionality is available as addons: * [tesla-zookeeper-observer](https://github.com/otto-de/tesla-zookeeper-observer): Read only access to zookeeper. * [tesla-mongo-connect](https://github.com/otto-de/tesla-mongo-connect): Read/write access to mongodb. * [tesla-cachefile](https://github.com/otto-de/tesla-cachefile): Read and write a cachefile. Locally or in hdfs. More features will be released at a later time as separate addons. ## FAQ **Q:** Is it any good? **A:** Yes. **Q:** Why tesla? **A:** It's a reference to the ingenious scientist and inventor. **Q:** Are there alternatives? **A:** Yes. You might want to look at [modularity.org](https://modularity.org/), [system](https://github.com/danielsz/system) and [duct](https://github.com/weavejester/duct). ## Initial Contributors Christian Stamm, Felix Bechstein, Ralf Sigmund, Kai Brandes, Florian Weyandt ## License Released under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) license. ================================================ FILE: dev-resources/logback.xml ================================================ tesla true %d{ISO8601} %-5p logger=%c thread=%t msg="%m"%n ${log_location}/de.otto.tesla.log true ${log_location}/de.otto.tesla.log.%i 1 5 10MB %d{ISO8601} %-5p logger=%c thread=%t msg="%m"%n ================================================ FILE: project.clj ================================================ (defproject de.otto/tesla-microservice "0.17.9-SNAPSHOT" :description "basic microservice." :url "https://github.com/otto-de/tesla-microservice" :license {:name "Apache License 2.0" :url "http://www.apache.org/license/LICENSE-2.0.html"} :scm {:name "git" :url "https://github.com/otto-de/tesla-microservice"} :repositories [["releases" {:url "https://repo.clojars.org" :creds :gpg}]] :dependencies [[org.clojure/data.json "2.5.1"] [org.clojure/tools.logging "1.3.0"] [de.otto/status "0.1.3"] [de.otto/goo "1.2.12"] [clojure.java-time "1.4.3"] [clojurewerkz/propertied "1.3.0"] [com.stuartsierra/component "1.1.0"] [compojure "1.7.1"] [environ "1.2.0"] [overtone/at-at "1.4.65"] [ring/ring-core "1.13.0"] [ring/ring-devel "1.13.0"] [ring-basic-authentication "1.2.0"]] :exclusions [org.clojure/clojure org.slf4j/slf4j-nop org.slf4j/slf4j-log4j12 log4j commons-logging/commons-logging] :lein-release {:deploy-via :clojars} :filespecs [{:type :path :path "test-utils"}] :test-selectors {:default (constantly true) :integration :integration :unit :unit :all (constantly true)} :profiles {:uberjar {:aot :all} :dev {:dependencies [[org.clojure/clojure "1.12.0"] [org.slf4j/slf4j-api "2.0.16"] [ch.qos.logback/logback-core "1.5.14"] [ch.qos.logback/logback-classic "1.5.14"] [ring-mock "0.1.5"] [org.clojure/data.codec "0.2.0"]] :plugins [[lein-release/lein-release "1.0.9"]]}} :test-paths ["test" "test-resources" "test-utils"]) ================================================ FILE: resources/default.edn ================================================ { :status-url "/status" :health-url "/health" ; metering ; take a look at de.otto.tesla.stateful.metering ; for all available metrics reporters and their configs :metric {:console {:interval-in-s 120}} :scheduler {:cpu-count 0} :server-port "8080" :jetty-options {:send-server-version? false} ; use this if you need a grace-time during which ; the system reports unhealthy before shutting down. ;:wait-ms-on-stop "10000" } ================================================ FILE: resources/default.properties ================================================ status.url=/status health.url=/health ### metering metering.reporter=console console.interval.seconds=100 ### activate graphite like this: #metering.reporter=graphite #graphite.host=localhost #graphite.port=2003 #graphite.prefix=my-app-prefix #graphite.interval.seconds=60 server.port=8080 ## use this if you need a grace-time during which ## the system reports unhealthy before shutting down. #wait.ms.on.stop=10000 ================================================ FILE: src/data_readers.clj ================================================ { ts/env de.otto.tesla.util.env_var_reader/read-env-var } ================================================ FILE: src/de/otto/tesla/middleware/exceptions.clj ================================================ (ns de.otto.tesla.middleware.exceptions (:require [clojure.tools.logging :as log])) (defn exceptions-to-500 [handler] (fn [request] (try (handler request) (catch Exception e (log/error e "Will return 500 to client because of this error.") {:status 500 :body (.getMessage e)})))) ================================================ FILE: src/de/otto/tesla/stateful/app_status.clj ================================================ (ns de.otto.tesla.stateful.app-status "This component renders a status page consisting of instance- and configuration-info as well as dynamic status of other components" (:require [com.stuartsierra.component :as component] [compojure.core :as c] [clojure.data.json :as json :only [write-str]] [clojure.tools.logging :as log] [clojure.string :as str] [java-time.api :as jt] [environ.core :as env] [de.otto.tesla.stateful.handler :as handlers] [de.otto.status :as s] [ring.middleware.basic-authentication :as ba] [de.otto.tesla.util.sanitize :as san] [de.otto.tesla.stateful.configuring :as configuring] [de.otto.goo.goo :as goo] [de.otto.tesla.stateful.auth :as auth])) (defn keyword-to-status [kw] (str/upper-case (name kw))) (defn status-details-to-json [details] (into {} (map (fn [[k v]] {k (update-in v [:status] keyword-to-status)}) details))) (defn system-infos [config] {:systemTime (jt/format :iso-date (jt/local-date-time)) :hostname (configuring/external-hostname config) :port (configuring/external-port config)}) (defn aggregation-strategy [config] (if (= (get-in config [:status-aggregation]) "forgiving") s/forgiving-strategy s/strict-strategy)) (defn create-complete-status [self] (let [config (get-in self [:config :config]) version-info (get-in self [:config :version]) aggregate-strategy (:status-aggregation self) extra-info {:name (:name config) :version (:version version-info) :git (:commit version-info) :configuration (san/hide-passwds config)}] (assoc (s/aggregate-status :application aggregate-strategy @(:status-functions self) extra-info) :system (system-infos (:config self))))) (defn status-response-body [self] (-> (create-complete-status self) (update-in [:application :statusDetails] status-details-to-json) (update-in [:application :status] keyword-to-status))) ;; This should apply to the specification at ;; http://spec.otto.de/media_types/application_vnd_otto_monitoring_status_json.html . ;; Right now it applies only partially. (defn status-response [self _] {:status 200 :headers {"Content-Type" "application/json"} :body (json/write-str (status-response-body self))}) (defn register-status-fun [self fun] (swap! (:status-functions self) #(conj % fun))) (defn path-filter [self handler] (let [status-path (get-in self [:config :config :status-url] "/status")] (c/GET status-path request (handler request)))) (defn mk-handler [{:keys [auth] :as self}] ((->> (partial status-response self) (goo/timing-middleware) (auth/wrap-auth auth) (partial path-filter self)))) (defrecord ApplicationStatus [config handler auth] component/Lifecycle (start [self] (log/info "-> starting Application Status") (let [new-self (assoc self :status-aggregation (aggregation-strategy (:config config)) :status-functions (atom []))] (handlers/register-handler handler (mk-handler new-self)) (goo/register-gauge! :build/info {:labels [:version :revision] :description "Constant '1' value labeled by version and revision of the service."}) (goo/inc! :build/info {:version (-> config :version :version) :revision (-> config :version :commit)}) new-self)) (stop [self] (log/info "<- stopping Application Status") self)) (defn new-app-status ([] (map->ApplicationStatus {}))) ================================================ FILE: src/de/otto/tesla/stateful/auth.clj ================================================ (ns de.otto.tesla.stateful.auth "This component handles authentication." (:require [clojure.tools.logging :as log] [com.stuartsierra.component :as component])) (defn no-auth-middleware [_config handler-fn] (log/warn "You are using no authentication...Is this desired?") (fn [request] (handler-fn request))) (defn wrap-auth [self handler-fn] ((:auth-mw self) handler-fn)) (defrecord Auth [config auth-mw] component/Lifecycle (start [self] (log/info "-> starting AuthMiddleware") (let [auth-mw (partial (or auth-mw no-auth-middleware) (:config config)) new-self (assoc self :auth-mw auth-mw)] new-self)) (stop [self] (log/info "<- stopping AuthMiddleware") self)) (defn new-auth ([auth-mw] (map->Auth {:auth-mw auth-mw}))) ================================================ FILE: src/de/otto/tesla/stateful/configuring.clj ================================================ (ns de.otto.tesla.stateful.configuring "This component is responsible for loading the configuration." (:require [com.stuartsierra.component :as component] [clojurewerkz.propertied.properties :as p] [clojure.tools.logging :as log] [clojure.java.io :as io] [de.otto.tesla.util.keyword :as kwutil] [environ.core :as env :only [env]] [de.otto.tesla.util.env_var_reader :only [read-env-var]] [de.otto.tesla.util.sanitize :as san]) (:import (java.io PushbackReader))) (defn deep-merge "Recursively merges maps. If vals are not maps, the last value wins." [& vals] (if (every? map? vals) (apply merge-with deep-merge vals) (last vals))) (defn- load-properties-from-resource [resource] (kwutil/sanitize-keywords (p/properties->map (p/load-from resource) false))) (defn resolve-file [name type] (cond (= :resource type) (io/resource name) (and (= :file type) (.exists (io/file name))) (io/file name) (and (= :resource-or-file type) (io/resource name)) (io/resource name) (and (= :resource-or-file type) (.exists (io/file name))) (io/file name))) (defn- load-properties [name type] (when-let [resource (resolve-file name type)] (load-properties-from-resource resource))) (defn- load-edn [name type] (when-let [resource (resolve-file name type)] (log/debugf "Reading %s" name) (-> resource (io/reader) (PushbackReader.) (read)))) (defn load-merge [load-fn merge-fn ending runtime-config] (let [default-cfg-name (or (:default-cfg-file-name runtime-config) "default") defaults (load-fn (str default-cfg-name ending) :resource) application (load-fn (or (:config-file env/env) (str "application" ending)) :resource-or-file) local (load-fn (str "local" ending) :resource) private (load-fn (str "private" ending) :resource) configs (filter some? [defaults application local private runtime-config])] (apply merge-fn configs))) (defn load-config-from-edn-files [runtime-config] (load-merge load-edn deep-merge ".edn" runtime-config)) (defn load-config-from-properties-files [runtime-config] (let [loaded (load-merge load-properties merge ".properties" runtime-config)] (if (:merge-env-to-properties-config runtime-config) (merge loaded env/env) loaded))) (defn load-and-merge [runtime-config] (if-not (:property-file-preferred runtime-config) (load-config-from-edn-files runtime-config) (load-config-from-properties-files runtime-config))) ;; Load config on startup. (defrecord Configuring [runtime-config] component/Lifecycle (start [self] (log/info "-> loading configuration.") (log/info runtime-config) (let [config (load-and-merge runtime-config)] (log/info "-> using configuration:\n" (with-out-str (clojure.pprint/pprint (san/hide-passwds config)))) (assoc self :config config :version (load-properties "version.properties" :resource)))) (stop [self] (log/info "<- stopping configuration.") self)) (defn new-config [runtime-config] (map->Configuring {:runtime-config runtime-config})) ;; The hostname and port visble from the outside are different for ;; different environments. ;; These methods default to Marathon defaults. (defn external-hostname [{:keys [config]}] (or (:host-name config) (:host env/env) (:host-name env/env) (:hostname env/env) "localhost")) (defn server-port [config] (:server-port config)) ;; see above (defn external-port [{:keys [config]}] (or (server-port config) (:port0 env/env) (:host-port env/env) (:server-port env/env))) ================================================ FILE: src/de/otto/tesla/stateful/handler.clj ================================================ (ns de.otto.tesla.stateful.handler "This component is responsible for collecting HTTP handlers in order to provide them to a server component." (:require [com.stuartsierra.component :as component] [de.otto.tesla.middleware.exceptions :as ex] [clojure.tools.logging :as log] [ring.middleware.reload :refer [wrap-reload]])) (defn- single-handler-fn [{:keys [registered-handlers]}] (fn [request] (some (fn [h] (h request)) @registered-handlers))) (defn register-handler [{:keys [registered-handlers]} new-handler-fn] (swap! registered-handlers conj (ex/exceptions-to-500 new-handler-fn))) (defn handler [{:keys [config] :as self}] (if (get-in config [:config :handler :hot-reload?]) (wrap-reload (single-handler-fn self)) (single-handler-fn self))) (defrecord Handler [config] component/Lifecycle (start [self] (log/info "-> starting Handler") (assoc self :registered-handlers (atom []))) (stop [self] (log/info "<- stopping Handler") self)) (defn new-handler [] (map->Handler {})) ================================================ FILE: src/de/otto/tesla/stateful/health.clj ================================================ (ns de.otto.tesla.stateful.health "This component provides a health-check endpoint which can be used to orchestrate app shutdown with load balancers." (:require [com.stuartsierra.component :as component] [compojure.core :as c] [clojure.tools.logging :as log] [de.otto.tesla.stateful.handler :as handler] [de.otto.goo.goo :as goo])) (def healthy-response {:status 200 :headers {"Content-Type" "text/plain"} :body "HEALTHY"}) (def unhealthy-response {:status 423 :headers {"Content-Type" "text/plain"} :body "UNHEALTHY"}) (defn health-response [self _] (if @(:locked self) unhealthy-response healthy-response)) (defn path-filter [self handler] (let [health-path (get-in self [:config :config :health-url] "/health")] (c/GET health-path request (handler request)))) (defn make-handler [self] (->> (partial health-response self) goo/timing-middleware (path-filter self))) (defn lock-application [self] (goo/update! :health/locked 0) (reset! (:locked self) true)) (defrecord Health [config handler] component/Lifecycle (start [self] (log/info "-> Starting healthcheck.") (let [new-self (assoc self :locked (atom false))] (handler/register-handler handler (make-handler new-self)) ;; TODO: use config directly (goo/register-gauge! :health/locked {}) (goo/update! :health/locked 1) new-self)) (stop [self] (log/info "<- Stopping Healthcheck") self)) (defn new-health [] (map->Health {})) ================================================ FILE: src/de/otto/tesla/stateful/keep_alive.clj ================================================ (ns de.otto.tesla.stateful.keep-alive "This component is responsible for keeping the system alive by creating a non-deamonized noop thread." (:require [clojure.tools.logging :as log] [com.stuartsierra.component :as component]) (:import (java.util.concurrent CountDownLatch))) (defn exit-keep-alive [] (log/info "<- stopping keepalive thread: " (.getName (Thread/currentThread)))) (defn enter-keep-alive [] (log/info "-> starting keepalive thread: " (.getName (Thread/currentThread)))) (defn wait-for-count-down-latch [cdl] (enter-keep-alive) (.await cdl) (exit-keep-alive)) (defn start-keep-alive-thread [cd-latch] (doto (Thread. ^Runnable (partial wait-for-count-down-latch cd-latch) "tesla-ms-keep-alive") (.start))) (defrecord KeepAlive [cd-latch] component/Lifecycle (start [self] (log/info "-> starting keepalive") (let [cd-latch (CountDownLatch. 1)] (assoc self :thread (start-keep-alive-thread cd-latch) :cd-latch cd-latch))) (stop [self] (log/info "<- stopping keepalive") (.countDown cd-latch) (dissoc self :thread))) (defn new-keep-alive [] (map->KeepAlive {})) ================================================ FILE: src/de/otto/tesla/stateful/metering.clj ================================================ (ns de.otto.tesla.stateful.metering "This component handles exporting of (prometheus) metrics" (:require [com.stuartsierra.component :as component] [clojure.tools.logging :as log] [de.otto.goo.goo :as goo] [de.otto.tesla.stateful.handler :as handlers] [compojure.core :as c] [overtone.at-at :as at] [de.otto.tesla.stateful.auth :as auth])) (defn write-to-console [] (log/info "Metrics Reporting:\n" (goo/text-format))) (defn start-console-reporter [console-config scheduler] (let [interval-in-ms (* 1000 (:interval-in-s console-config))] (log/info "Starting metrics console reporter") (at/every interval-in-ms write-to-console (:pool scheduler) :desc "Console-Reporter"))) (defn metrics-response [_] (fn [_request] {:status 200 :headers {"Content-Type" "text/plain"} :body (goo/text-format)})) (defn- path-filter [metrics-path handler] (c/GET metrics-path request (handler request))) (defn register-metrics-endpoint [{metrics-path :metrics-path} {:keys [handler auth]}] (log/info "Register metrics prometheus endpoint") (handlers/register-handler handler ((->> (metrics-response handler) (goo/timing-middleware) (auth/wrap-auth auth) (partial path-filter metrics-path))))) (defn- start-reporter! [{:keys [scheduler] :as self} [reporter-type reporter-config]] (case reporter-type :console (start-console-reporter reporter-config scheduler) :prometheus (register-metrics-endpoint reporter-config self))) (defn- start-reporters! [{:keys [config] :as self}] (let [available-reporters (get-in config [:config :metrics])] (run! (partial start-reporter! self) available-reporters))) (defrecord Metering [config handler scheduler auth] component/Lifecycle (start [self] (log/info "-> starting metering.") (goo/register-counter! :metering/errors {:labels [:error :metric-name]}) (assoc self :reporters (start-reporters! self))) (stop [self] (log/info "<- stopping metering") self)) (defn new-metering ([] (map->Metering {}))) ================================================ FILE: src/de/otto/tesla/stateful/scheduler.clj ================================================ (ns de.otto.tesla.stateful.scheduler "This components maintains a thread pool which can be used to schedule activities." (:require [com.stuartsierra.component :as c] [clojure.tools.logging :as log] [overtone.at-at :as ot] [de.otto.tesla.stateful.app-status :as app-status]) (:import (java.util.concurrent ScheduledThreadPoolExecutor))) (defn- as-seq [v] (apply concat v)) (defn- new-ot-pool [config] (let [pool-config (get-in config [:config :scheduler])] (apply ot/mk-pool (as-seq pool-config)))) (defn- as-readable-time [l] (.format (java.text.SimpleDateFormat. "EEEE HH':'mm':'ss's'") l)) (defn- job-details [{:keys [id created-at initial-delay desc ms-period scheduled?]}] [(keyword (str id)) {:desc desc :createdAt (as-readable-time created-at) :initialDelay initial-delay :msPeriod ms-period :scheduled? @scheduled?}]) (defn- pool-details [{:keys [pool-atom]}] (when pool-atom (let [^ScheduledThreadPoolExecutor thread-pool (:thread-pool @pool-atom)] {:active (.getActiveCount thread-pool) :queueSize (.size (.getQueue thread-pool)) :poolSize (.getPoolSize thread-pool)}))) (defn- scheduler-app-status [{:keys [pool]}] {:scheduler {:status :ok :poolInfo (pool-details pool) :scheduledJobs (into {} (map job-details (ot/scheduled-jobs pool)))}}) (defn exception-to-log [desc f] (try (f) (catch Exception e (log/error e (str "Exception during scheduled job: " desc))))) (defn every "Calls fun every ms-period, and takes an optional initial-delay for the first call in ms. Returns a scheduled-fn which may be cancelled with cancel. All exceptions are catched and logged. Default options are {:initial-delay 0 :desc \"\"}" [{:keys [pool]} ms-period fun & {:keys [initial-delay desc] :or {initial-delay 0 desc ""}}] (ot/every ms-period (partial exception-to-log desc fun) pool :initial-delay initial-delay :desc desc)) (defn interspaced "Calls fun repeatedly with an interspacing of ms-period, i.e. the next call of fun will happen ms-period milliseconds after the completion of the previous call. Also takes an optional initial-delay for the first call in ms. Returns a scheduled-fn which may be cancelled with cancel. All exceptions are catched and logged. Default options are {:initial-delay 0 :desc \"\"}" [{:keys [pool]} ms-period fun & {:keys [initial-delay desc] :or {initial-delay 0 desc ""}}] (ot/interspaced ms-period (partial exception-to-log desc fun) pool :initial-delay initial-delay :desc desc)) (defrecord Scheduler [config app-status] c/Lifecycle (start [self] (log/info "-> Start Scheduler") (let [new-self (assoc self :pool (new-ot-pool config))] (app-status/register-status-fun app-status (partial scheduler-app-status new-self)) new-self)) (stop [self] (log/info "<- Stop Scheduler") (when-let [pool (:pool self)] (ot/stop-and-reset-pool! pool)) self)) (defn new-scheduler [] (map->Scheduler {})) ================================================ FILE: src/de/otto/tesla/system.clj ================================================ (ns de.otto.tesla.system (:require [com.stuartsierra.component :as c] [de.otto.tesla.stateful.app-status :as app-status] [de.otto.tesla.stateful.health :as health] [de.otto.goo.goo :as goo] [de.otto.tesla.stateful.configuring :as configuring] [de.otto.tesla.stateful.metering :as metering] [de.otto.tesla.stateful.keep-alive :as keep-alive] [de.otto.tesla.stateful.scheduler :as scheduler] [clojure.tools.logging :as log] [environ.core :as env :only [env]] [de.otto.tesla.stateful.handler :as handler] [de.otto.tesla.stateful.auth :as auth]) (:import (clojure.lang ExceptionInfo))) (defn wait! [system] (when-let [wait-time (get-in system [:config :config :wait-ms-on-stop])] (try (log/info "<- Waiting " wait-time " milliseconds.") (Thread/sleep (Long/parseLong wait-time)) (catch Exception e (log/error e))))) (defn- exit [code] (System/exit code)) (defn- try-stop [system] (try (c/stop system) (log/info "System stopped. Bye.") (catch Exception ex (log/error ex "Error on stopping the system.") (exit 1)))) (defn stop [system] (when-let [sdt (:sdt system)] (.removeShutdownHook (Runtime/getRuntime) sdt)) (when-let [health (:health system)] (log/info "<- System will be stopped. Setting lock.") (health/lock-application health) (wait! system)) (log/info "<- Stopping system.") (try-stop system)) (defn- try-start [system] (try (c/start system) (catch ExceptionInfo e (log/error (c/ex-without-components e) "Going to shut down because of this error.") (-> e (ex-data) :system (try-stop))))) (defn start [system] (log/info "-> Starting system.") (let [start-timestamp (System/currentTimeMillis) started (try-start system)] (log/info "-> System completely started.") (goo/register-counter! :system-startups {:description "Counts startups."}) (goo/register-counter! :system-startup-duration-seconds {:description "Measures the startup duration of the system."}) (goo/inc! :system-startups) (goo/inc! :system-startup-duration-seconds (/ (- (System/currentTimeMillis) start-timestamp) 1000)) (if (map? started) (let [sdt (Thread. ^Runnable (partial stop started))] (.addShutdownHook (Runtime/getRuntime) sdt) (assoc started :sdt sdt)) started))) (map? []) (defn base-system [runtime-config & [auth-mw]] (c/system-map :keep-alive (keep-alive/new-keep-alive) :config (c/using (configuring/new-config runtime-config) [:keep-alive]) :handler (c/using (handler/new-handler) [:config]) :metering (c/using (metering/new-metering) [:config :handler :scheduler :auth]) :health (c/using (health/new-health) [:config :handler]) :app-status (c/using (app-status/new-app-status) [:config :handler :auth]) :scheduler (c/using (scheduler/new-scheduler) [:config :app-status]) :auth (c/using (auth/new-auth auth-mw) [:config]))) ================================================ FILE: src/de/otto/tesla/util/env_var_reader.clj ================================================ (ns de.otto.tesla.util.env_var_reader (:require [environ.core :as env])) (defn read-env-var ([[env-var-key fallback]] (or (get env/env env-var-key fallback) ""))) ================================================ FILE: src/de/otto/tesla/util/keyword.clj ================================================ (ns de.otto.tesla.util.keyword (:require [clojure.string :as str])) ;implementation is copied from environ 1.0.0 (defn- keywordize [s] (-> (str/lower-case s) (str/replace "_" "-") (str/replace "." "-") (keyword))) (defn sanitize-keywords [m] (->> m (map (fn [[k v]] [(keywordize k) v])) (into {}))) ;; removed keywordize-keys, provided by clojure.walk/keywordize-keys ================================================ FILE: src/de/otto/tesla/util/sanitize.clj ================================================ (ns de.otto.tesla.util.sanitize) (def checklist ["password" "pw" "passwd" "private" "secret" "token"]) (defn hide-passwd [k v] (if (some true? (map #(.contains (name k) %) checklist)) "***" v)) (defn hide-passwds [map] (reduce (fn [new-map [k v]] (assoc new-map k (if (map? v) (hide-passwds v) (hide-passwd k v)))) {} map)) ================================================ FILE: test/de/otto/tesla/reporter/prometheus_test.clj ================================================ (ns de.otto.tesla.reporter.prometheus-test (:require [clojure.test :refer :all] [de.otto.tesla.system :as system] [com.stuartsierra.component :as c] [clojure.data.codec.base64 :as b64] [de.otto.tesla.stateful.metering :as metering] [de.otto.tesla.stateful.handler :as handler] [ring.mock.request :as mock] [ring.middleware.basic-authentication :as ba])) (defn- to-base64 [original] (String. ^bytes (b64/encode (.getBytes original)) "UTF-8")) (defn- auth-header [request user password] (mock/header request "authorization" (str "Basic " (to-base64 (str user ":" password))))) (defn system [runtime-config auth-middleware] (-> (system/base-system runtime-config auth-middleware) (dissoc :server))) (defn- handlers [runtime-config & [auth-middleware]] (let [system (system runtime-config auth-middleware) started-system (c/start-system system)] (handler/handler (:handler started-system)))) (defn- rc-metrics-request [system-handler user password] (-> (mock/request :get "/metrics") (auth-header user password) (system-handler) :status)) (deftest authentication (let [config {:metrics {:prometheus {:metrics-path "/metrics"}}} auth-fun (fn [usr pw] (and (= "some-user" usr) (= "some-password" pw))) auth-middleware (fn [_config handler] (ba/wrap-basic-authentication handler auth-fun)) system-handler (handlers config auth-middleware)] (testing "it should allow access if authentication succeeds" (is (= 200 (rc-metrics-request system-handler "some-user" "some-password")))) (testing "it should deny access if authentication fails" (is (= 401 (rc-metrics-request system-handler "some-user" "wrong password")))))) ================================================ FILE: test/de/otto/tesla/stateful/app_status_test.clj ================================================ (ns de.otto.tesla.stateful.app-status-test (:require [clojure.test :refer :all] [de.otto.tesla.stateful.app-status :as app-status] [com.stuartsierra.component :as c] [environ.core :as env] [clojure.data.json :as json] [clojure.tools.logging :as log] [de.otto.tesla.util.test-utils :as u] [de.otto.tesla.system :as system] [de.otto.tesla.stateful.handler :as handler] [ring.mock.request :as mock] [de.otto.status :as s] [de.otto.goo.goo :as goo] [ring.middleware.basic-authentication :as ba])) (defn- serverless-system [runtime-config & [auth-middleware]] (dissoc (system/base-system runtime-config auth-middleware) :server)) (deftest ^:unit should-have-system-status-for-runtime-config (u/with-started [system (serverless-system {:host-name "bar" :server-port "0123"})] (let [status (:app-status system) system-status (:system (app-status/status-response-body status))] (is (= (:hostname system-status) "bar")) (is (= (:port system-status) "0123")) (is (not (nil? (:systemTime system-status))))))) (deftest ^:unit host-name-and-port-on-app-status (with-redefs [env/env {:host-name "foo" :server-port "1234"}] (testing "should add host and port from env to app-status in property-file case" (u/with-started [system (serverless-system {:property-file-preferred true :merge-env-to-properties-config true})] (let [status (:app-status system) system-status (:system (app-status/status-response-body status))] (is (= (:hostname system-status) "foo")) (is (= (:port system-status) "1234")) (is (not (nil? (:systemTime system-status))))))) (testing "should add host and port from env to app-status in edn-file case" (u/with-started [system (serverless-system {} nil)] (let [status (:app-status system) system-status (:system (app-status/status-response-body status))] (is (= (:hostname system-status) "foo")) (is (= (:port system-status) "9991")) (is (not (nil? (:systemTime system-status))))))))) (defrecord MockStatusSource [response] c/Lifecycle (start [self] (app-status/register-status-fun (:app-status self) #(:response self)) self) (stop [self] self)) (defn- mock-status-system [response] (assoc (serverless-system {}) :mock-status (c/using (map->MockStatusSource {:response response}) [:app-status]))) (deftest ^:unit should-show-applicationstatus (u/with-started [started (mock-status-system {:mock {:status :ok :message "nevermind"}})] (let [status (:app-status started) page (app-status/status-response status {}) _ (log/info page) application-body (get (json/read-str (:body page)) "application")] (testing "it shows OK as application status" (is (= (get application-body "status") "OK"))) (testing "it shows the substatus" (is (= (get application-body "statusDetails") {"mock" {"message" "nevermind" "status" "OK"} "scheduler" {"poolInfo" {"active" 0 "poolSize" 0 "queueSize" 0} "scheduledJobs" {} "status" "OK"}})))))) (deftest ^:unit should-show-warning-as-application-status (u/with-started [started (mock-status-system {:mock {:status :warning :message "nevermind"}})] (let [status (:app-status started) page (app-status/status-response status {}) applicationStatus (get (get (json/read-str (:body page)) "application") "status")] (is (= applicationStatus "WARNING"))))) (deftest ^:integration should-serve-status-under-configured-url (testing "use the default url" (u/with-started [started (serverless-system {})] (let [handlers (handler/handler (:handler started))] (is (= (:status (handlers (mock/request :get "/status"))) 200))))) (testing "use the configuration url" (u/with-started [started (serverless-system {:status-url "/my-status"})] (let [handlers (handler/handler (:handler started))] (is (= (:status (handlers (mock/request :get "/my-status"))) 200))))) (testing "default should be overridden" (u/with-started [started (serverless-system {:status-url "/my-status"})] (let [handlers (handler/handler (:handler started))] (is (= (handlers (mock/request :get "/status")) nil))))) (testing "response should be metered" (goo/clear-default-registry!) (u/with-started [started (serverless-system {})] (let [handlers (handler/handler (:handler started))] (handlers (mock/request :get "/status")) (u/eventually (= 1.0 (last (.-buckets (.get ((goo/snapshot) :http/duration_in_s {:path "/status" :method :get :rc 200})))))))))) (deftest should-add-version-properties-to-status (testing "it should add the version properties" (u/with-started [started (serverless-system {})] (let [handlers (handler/handler (:handler started)) request (mock/request :get "/status") status-map (json/read-json (:body (handlers request)))] (is (= (get-in status-map [:application :version]) "test.version")) (is (= (get-in status-map [:application :git]) "test.githash")))))) (deftest determine-status-strategy (testing "it should use strict stategy if none is configured" (let [config {:status-aggregation nil}] (is (= (app-status/aggregation-strategy config) s/strict-strategy)))) (testing "it should use forgiving stategy if forgiving is configured" (let [config {:status-aggregation "forgiving"}] (is (= (app-status/aggregation-strategy config) s/forgiving-strategy)))) (testing "it should use strict stategy if something else is configured" (let [config {:status-aggregation "unknown"}] (is (= (app-status/aggregation-strategy config) s/strict-strategy))))) (defn start-authenticated-system [user password] (let [config {} auth-fn (fn [usr pw] (and (= user usr) (= password pw))) auth-middleware (fn [_config handler] (ba/wrap-basic-authentication handler auth-fn)) system (serverless-system config auth-middleware) started-system (c/start-system system)] (handler/handler (:handler started-system)))) (deftest authentication (let [handlers (start-authenticated-system "some-user" "some-password")] (testing "it should allow access if authentication succeeds" (is (= 200 (:status (handlers (mock/header (mock/request :get "/status") "authorization" "Basic c29tZS11c2VyOnNvbWUtcGFzc3dvcmQ=")))))) (testing "it should deny access if authentication fails" (is (= 401 (:status (handlers (mock/header (mock/request :get "/status") "authorization" "Basic c29tZS11c2VyOnNvbWUtcGEzc3dvcmQ=")))))))) ================================================ FILE: test/de/otto/tesla/stateful/configuring_test.clj ================================================ (ns de.otto.tesla.stateful.configuring-test (:require [clojure.test :refer :all] [de.otto.tesla.stateful.configuring :as configuring] [com.stuartsierra.component :as component] [clojure.java.io :as io] [de.otto.tesla.util.test-utils :as u] [environ.core :as env])) (defn- test-system [rt-conf] (-> (component/system-map :conf (configuring/new-config rt-conf)))) (deftest referencing-env-properties (testing "should return env-property if referenced in edn-config" (with-redefs [env/env {:prop-without-fallback "prop-value"}] (u/with-started [started (test-system {}) conf (get-in started [:conf :config])] (is (= (:prop-without-fallback conf) "prop-value"))))) (testing "should return empty if env prop does not exist and fallback not provided" (with-redefs [env/env {}] (u/with-started [started (test-system {}) conf (get-in started [:conf :config])] (is (= (:prop-without-fallback conf) "")))))) (deftest ^:unit should-read-property-from-default-config (testing "should be possible to prefer reading configs from property files" (u/with-started [started (test-system {:property-file-preferred true})] (let [conf (get-in started [:conf :config])] (is (= (:foo-prop conf) "baz")) (is (= (get-in conf [:foo :edn]) nil)))))) (deftest ^:unit should-read-property-from-default-edn-file (u/with-started [started (test-system {})] (let [edn-conf (get-in started [:conf :config])] (is (= (:foo-prop edn-conf) nil)) (is (= (get-in edn-conf [:foo :edn]) "baz"))))) (deftest ^:unit should-read-property-from-private-edn-file (u/with-started [started (test-system {})] (let [conf (get-in started [:conf :config])] (is (= (:very conf) :private))))) (deftest ^:unit should-read-property-from-custom-edn-file (with-redefs [env/env {:config-file "./test-resources/test.edn"}] (u/with-started [started (test-system {})] (let [edn-conf (get-in started [:conf :config])] (is (= (get-in edn-conf [:health-url]) "/test/health")) (is (= (get-in edn-conf [:foo :local]) true)) (is (= (get-in edn-conf [:foo :edn]) "baz")))))) (deftest ^:unit should-ignore-missing-custom-edn-file (with-redefs [env/env {:config-file "non-existing.edn"}] (u/with-started [started (test-system {:runtime 123})] (let [edn-conf (get-in started [:conf :config])] (is (= (get-in edn-conf [:runtime]) 123)) (is (= (get-in edn-conf [:health-url]) "/health")) (is (= (get-in edn-conf [:foo :edn]) "baz")))))) (deftest ^:unit should-read-property-from-runtime-config (u/with-started [started (test-system {:foo-rt "bat" :foo {:nested 123}})] (let [edn-conf (get-in started [:conf :config])] (is (= (:foo-prop edn-conf) nil)) (is (= (:foo-rt edn-conf) "bat")) (is (= (get-in edn-conf [:foo :edn]) "baz")) (is (= (get-in edn-conf [:foo :nested]) 123))))) (deftest ^:unit should-read-default-properties (testing "should read default properties from property-files" (let [loaded-properties (configuring/load-config-from-properties-files {})] (is (not (nil? (:server-port loaded-properties)))) (is (not (nil? (:metering-reporter loaded-properties)))))) (testing "should read default properties from edn-property-files" (let [loaded-properties (configuring/load-config-from-edn-files {})] (is (= "9991" (:server-port loaded-properties))) (is (nil? (:metering-reporter loaded-properties)))))) (deftest ^:unit determine-hostname-from-config-and-env-with-defined-precedence (testing "it prefers a explicitly configured :host-name" (with-redefs [env/env {:host "host" :host-name "host-name" :hostname "hostname"}] (u/with-started [started (test-system {:host-name "configured"})] (is (= "configured" (configuring/external-hostname (:conf started))))))) (testing "it falls back to env-vars and prefers $HOST" (with-redefs [env/env {:host "host" :host-name "host-name" :hostname "hostname"}] (u/with-started [started (test-system {})] (is (= "host" (configuring/external-hostname (:conf started))))))) (testing "it falls back to env-vars and prefers $HOST_NAME" (with-redefs [env/env {:host-name "host-name" :hostname "hostname"}] (u/with-started [started (test-system {})] (is (= "host-name" (configuring/external-hostname (:conf started))))))) (testing "it falls back to env-vars and looks finally for $HOSTNAME" (with-redefs [env/env {:hostname "hostname"}] (u/with-started [started (test-system {})] (is (= "hostname" (configuring/external-hostname (:conf started))))))) (testing "it eventually falls back to localhost" (with-redefs [env/env {:host-name nil :hostname nil :host nil}] (u/with-started [started (test-system {})] (is (= "localhost" (configuring/external-hostname (:conf started)))))))) (deftest ^:unit determine-hostport-from-config-and-env-with-defined-precedence (testing "it prefers a explicitly configured :hostname" (with-redefs [configuring/server-port (constantly "configured")] (with-redefs [env/env {:port0 "0" :host-port "1" :server-port "2"}] (is (= "configured" (configuring/external-port {})))))) (with-redefs [configuring/server-port (constantly nil)] (testing "it falls back to env-vars and prefers $PORT0" (with-redefs [env/env {:port0 "0" :host-port "1" :server-port "2"}] (u/with-started [started (test-system {})] (is (= "0" (configuring/external-port {})))))) (testing "it falls back to env-vars and prefers $HOST_PORT" (with-redefs [env/env {:host-port "1" :server-port "2"}] (u/with-started [started (test-system {})] (is (= "1" (configuring/external-port {}))))))) (testing "it falls back to env-vars and finally takes $SERVER_PORT" (with-redefs [env/env {:server-port "2"}] (u/with-started [started (test-system {})] (is (= "2" (configuring/external-port {}))))))) (deftest ^:integration should-read-properties-from-file (spit "application.properties" "foooo=barrrr") (is (= (:foooo (configuring/load-config-from-properties-files {})) "barrrr")) (io/delete-file "application.properties")) (deftest ^:integration should-prefer-configured-conf-file (spit "application.properties" "foooo=value") (spit "other.properties" "foooo=other-value") (with-redefs-fn {#'env/env {:config-file "other.properties"}} #(is (= (:foooo (configuring/load-config-from-properties-files {})) "other-value"))) (io/delete-file "other.properties") (io/delete-file "application.properties")) (deftest ^:unit deep-merge-test (testing "simple cases" (is (= {:a 1 :b 2} (configuring/deep-merge {:a 1} {:b 2}))) (is (= {:a 1 :b 2} (configuring/deep-merge {:a 1 :b 1} {:b 2}))) (is (= {:a 2 :b 2} (configuring/deep-merge {:a 1 :b 1} {:a 2 :b 2}))) (is (= {:a 3 :b 2 :c 3} (configuring/deep-merge {:a 1 :b 1} {:b 2} {:a 3 :c 3})))) (testing "nested maps" (is (= {:a {:b {:c 1 :d 2}}} (configuring/deep-merge {:a {:b {:c 1}}} {:a {:b {:d 2}}}))) (is (= {:a {:b {:c 2 :d 2} :f 3}} (configuring/deep-merge {:a {:b {:c 1}}} {:a {:b {:c 2 :d 2}}} {:a {:f 3}})))) (testing "collections as values" (is (= {:a [4 5 6]} (configuring/deep-merge {:a [1 2 3]} {:a [4 5 6]})))) (testing "reseting values" (is (= {:a nil} (configuring/deep-merge {:a 1} {:a nil})))) (testing "missing things" (is (= {:a 1} (configuring/deep-merge {:a 1} {}))) (is (= nil (configuring/deep-merge {:a 1} nil))) (is (= {:a 1} (apply configuring/deep-merge (filter some? [{:a 1} nil])))))) ================================================ FILE: test/de/otto/tesla/stateful/handler_test.clj ================================================ (ns de.otto.tesla.stateful.handler-test (:require [clojure.test :refer :all] [de.otto.tesla.stateful.handler :as handler] [com.stuartsierra.component :as component])) (deftest registering-handlers (testing "should register a handler and return a single handling-function" (let [handler (-> (handler/new-handler) (component/start))] (handler/register-handler handler (fn [r] (when (= r :ping) :pong))) (handler/register-handler handler (fn [r] (when (= r :pong) :ping))) (is (= 2 (count @(:registered-handlers handler)))) (is (= :pong ((handler/handler handler) :ping))) (is (= :ping ((handler/handler handler) :pong)))))) ================================================ FILE: test/de/otto/tesla/stateful/health_test.clj ================================================ (ns de.otto.tesla.stateful.health-test (:require [clojure.test :refer :all] [de.otto.tesla.stateful.health :as health] [de.otto.tesla.util.test-utils :as u] [de.otto.tesla.system :as system] [de.otto.tesla.stateful.handler :as handler] [ring.mock.request :as mock] [de.otto.goo.goo :as goo])) (defn- serverless-system [runtime-config] (dissoc (system/base-system runtime-config) :server)) (deftest ^:unit should-turn-unhealthy-when-locked (u/with-started [started (serverless-system {})] (testing "it is still healthy when not yet locked" (let [handlers (handler/handler (:handler started)) response (handlers (mock/request :get "/health"))] (are [key value] (= value (get response key)) :body "HEALTHY" :status 200) (is (= 1.0 (-> :health/locked ((goo/snapshot)) (.get)))))) (testing "when locked, it is unhealthy" (let [handlers (handler/handler (:handler started)) _ (health/lock-application (:health started)) response (handlers (mock/request :get "/health"))] (are [key value] (= value (get response key)) :body "UNHEALTHY" :status 423) (is (= 0.0 (-> :health/locked ((goo/snapshot)) (.get)))))))) (deftest ^:integration should-serve-health-under-configured-url (testing "use the default url" (u/with-started [started (serverless-system {})] (let [handlers (handler/handler (:handler started))] (is (= (:body (handlers (mock/request :get "/health"))) "HEALTHY" ))))) (testing "use the configuration url" (u/with-started [started (serverless-system {:health-url "/my-health"})] (let [handlers (handler/handler (:handler started))] (is (= (:body (handlers (mock/request :get "/my-health"))) "HEALTHY")))))) ================================================ FILE: test/de/otto/tesla/stateful/keep_alive_test.clj ================================================ (ns de.otto.tesla.stateful.keep-alive-test (:require [clojure.test :refer [deftest testing is]] [de.otto.tesla.stateful.keep-alive :as kalive] [de.otto.tesla.util.test-utils :refer [eventually with-started]] [clojure.tools.logging :as log])) (deftest starting-and-stopping-the-keepalive-component (testing "should start and stop keepalive-thread" (let [state (atom :not-started)] (with-redefs [kalive/enter-keep-alive (fn [] (log/info "ENTER test keepalive") (reset! state :entered)) kalive/exit-keep-alive (fn [] (log/info "EXIT test keepalive") (reset! state :exited))] (is (= :not-started @state)) (with-started [_ (kalive/new-keep-alive)] (eventually (= :entered @state))) (eventually (= :exited @state)))))) ================================================ FILE: test/de/otto/tesla/stateful/scheduler_test.clj ================================================ (ns de.otto.tesla.stateful.scheduler-test (:require [clojure.test :refer :all] [de.otto.tesla.system :as system] [de.otto.tesla.stateful.scheduler :as schedule] [de.otto.tesla.util.test-utils :as u] [overtone.at-at :as at] [de.otto.tesla.util.test-utils :refer [eventually]] [com.stuartsierra.component :as c] [de.otto.tesla.stateful.handler :as handler] [ring.mock.request :as mock] [clojure.data.json :as json] [de.otto.tesla.stateful.scheduler :as scheduler]) (:import (java.util.concurrent ScheduledThreadPoolExecutor))) (defn- serverless-system [runtime-config] (-> (system/base-system runtime-config) (dissoc :server) (assoc :scheduler (c/using (schedule/new-scheduler) [:config :app-status])))) (deftest ^:unit should-call-function-at-scheduled-rate (testing "Function gets called every 20 ms" (u/with-started [system (serverless-system {:scheduler {:cpu-count 1}})] (let [scheduler (:scheduler system) calls (atom 0)] (at/every 20 #(swap! calls inc) (:pool scheduler)) (eventually (= @calls 3))))) (testing "Function gets called every 20 ms with initial delay" (u/with-started [system (serverless-system {:scheduler {:cpu-count 1}})] (let [scheduler (:scheduler system) calls (atom 0)] (at/every 20 #(swap! calls inc) (:pool scheduler) :initial-delay 20) (eventually (= @calls 2))))) (testing "Function gets called every 20 ms AFTER the function last returned" (u/with-started [system (serverless-system {:scheduler {:cpu-count 1}})] (let [scheduler (:scheduler system) calls (atom 0)] (at/interspaced 20 #((Thread/sleep 10) (swap! calls inc)) (:pool scheduler)) (eventually (= @calls 1)))))) (defn assert-map-args! [args-assert-val] (fn [& {:as args}] (is (= args args-assert-val)))) (deftest ^:unit configuring-the-schedule (testing "should pass nr cpus to pool if specified" (let [config {:scheduler {:cpu-count 2 :stop-delayed? false :stop-periodic? true}}] (with-redefs [at/stop-and-reset-pool! (constantly nil) at/mk-pool (assert-map-args! {:cpu-count 2 :stop-delayed? false :stop-periodic? true})] (u/with-started [system (serverless-system config)] (is ()))))) (testing "should pass nothing to pool if nothing is specified" (let [config {:some-other :property}] (with-redefs [at/stop-and-reset-pool! (constantly nil) at/mk-pool (assert-map-args! {:cpu-count 0})] (u/with-started [system (serverless-system config)] (is ())))))) (deftest ^:unit scheduler-app-status (with-redefs [schedule/as-readable-time (constantly "mock-time")] (u/with-started [system (serverless-system {:host-name "bar" :server-port "0123" :scheduler {:cpu-count 2}})] (let [{:keys [scheduler handler]} system handler-fn (handler/handler handler)] (testing "should register and return status-details in app-status" (at/every 20 #(Thread/sleep 10) (:pool scheduler) :desc "Job 1") (at/interspaced 20 #(Thread/sleep 10) (:pool scheduler) :desc "Job 2") (eventually (= {:poolInfo {:active 0 :poolSize 2 :queueSize 2} :scheduledJobs {:1 {:createdAt "mock-time" :desc "Job 1" :initialDelay 0 :msPeriod 20 :scheduled? true} :2 {:createdAt "mock-time" :desc "Job 2" :initialDelay 0 :msPeriod 20 :scheduled? true}} :status "OK"} (-> (mock/request :get "/status") (handler-fn) :body (json/read-str :key-fn keyword) (get-in [:application :statusDetails :scheduler]))))))))) (deftest ^:unit scheduler-default-conf (testing "should startup pool with core-pool-size of 0 if nothing else is configured" (u/with-started [system (serverless-system {})] (let [{:keys [scheduler]} system ^ScheduledThreadPoolExecutor thread-pool (:thread-pool @(:pool-atom (:pool scheduler)))] (is (= 0 (.getCorePoolSize thread-pool)))))) (testing "should startup pool with configured core-pool-size" (u/with-started [system (serverless-system {:scheduler {:cpu-count 2}})] (let [{:keys [scheduler]} system ^ScheduledThreadPoolExecutor thread-pool (:thread-pool @(:pool-atom (:pool scheduler)))] (is (= 2 (.getCorePoolSize thread-pool))))))) ================================================ FILE: test/de/otto/tesla/system_test.clj ================================================ (ns de.otto.tesla.system-test (:require [clojure.test :refer :all] [com.stuartsierra.component :as c] [de.otto.tesla.util.test-utils :as u :refer [eventually]] [de.otto.tesla.system :as system] [ring.mock.request :as mock] [de.otto.tesla.stateful.handler :as handler] [de.otto.tesla.stateful.configuring :as configuring] [environ.core :as env] [overtone.at-at :as at])) (deftest ^:unit should-start-base-system-and-shut-it-down (testing "start then shutdown using own method" (let [system-stop-calls (atom 0) started (system/start (system/base-system {}))] (with-redefs [c/stop-system (fn [_] (swap! system-stop-calls inc))] (system/stop started) (is (= 1 @system-stop-calls)))))) (defrecord BombOnStartup [] c/Lifecycle (start [_self] (throw (Exception. "boom!"))) (stop [self] self)) (defn new-bomb-on-startup [] (map->BombOnStartup {})) (deftest ^:unit should-shutdown-on-error-while-starting (let [exploding-system (assoc (system/base-system {}) :bomb (c/using (new-bomb-on-startup) [:health])) system-stop-calls (atom 0)] (with-redefs [c/stop-system (fn [_] (swap! system-stop-calls inc))] (system/start exploding-system) (is (= 1 @system-stop-calls))))) (defrecord BombEverytime [] c/Lifecycle (start [_self] (throw (Exception. "boom!"))) (stop [_self] (throw (Exception. "boom!")))) (defn new-bomb-on-everytime [] (map->BombEverytime {})) (deftest should-exit-jvm-if-stopping-system-fails (let [exploding-system (assoc (system/base-system {}) :bomb (c/using (new-bomb-on-everytime) [:health])) system-exit-calls (atom [])] (with-redefs [system/exit #(swap! system-exit-calls conj %)] (system/start exploding-system) (is (= [1] @system-exit-calls))))) (defrecord BombOnShutdown [] c/Lifecycle (start [self] self) (stop [_self] (throw (Exception. "boom!")))) (defn new-bomb-on-shutdown [] (map->BombOnShutdown {})) (deftest ^:unit should-shutdown-on-error-while-stopping (let [exploding-system-on-stop (assoc (system/base-system {}) :bomb (c/using (new-bomb-on-shutdown) [:health])) system-exit-calls (atom [])] (with-redefs [system/exit #(swap! system-exit-calls conj %)] (system/stop (system/start exploding-system-on-stop)) (is (= [1] @system-exit-calls))))) (deftest should-lock-application-on-shutdown (testing "the lock is set" (u/with-started [started (system/base-system {:wait-ms-on-stop 10})] (let [healthcomp (:health started)] (with-redefs [system/exit #(println "System exit would be called with code " %)] (system/stop started)) (is (= @(:locked healthcomp) true))))) (testing "it waits on stop" (u/with-started [started (system/base-system {:wait-seconds-on-stop 1})] (let [has-waited (atom false)] (with-redefs [system/wait! (fn [_] (reset! has-waited true)) system/exit #(println "System exit would be called with code " %)] (let [healthcomp (:health started) _ (system/stop started)] (is (= @(:locked healthcomp) true)) (is (= @has-waited true)))))))) (deftest ^:integration should-substitute-env-variables-while-reading (with-redefs [env/env {:my-custom-status-url "/custom/status/path" :prop-without-fallback "some prop value"}] (u/with-started [started (system/base-system {})] (testing "should load the status-path property from edn" (is (= "/custom/status/path" (:status-url (configuring/load-config-from-edn-files {}))))) (testing "should point to edn-configured custom status url" (let [handlers (handler/handler (:handler started)) response (handlers (mock/request :get "/custom/status/path"))] (is (= 200 (:status response))))))) (u/with-started [started (system/base-system {})] (testing "should fallback to default for status path" (is (= "/status" (:status-url (configuring/load-config-from-edn-files {}))))))) (deftest the-scheduler-in-the-base-system (testing "should schedule and execute task NOW" (u/with-started [started (system/base-system {})] (let [work-done (atom :no-work-done) {:keys [scheduler]} started] (at/after 0 #(reset! work-done :work-done!) (:pool scheduler)) (eventually (= :work-done! @work-done)))))) ================================================ FILE: test/de/otto/tesla/util/env_var_reader_test.clj ================================================ (ns de.otto.tesla.util.env-var-reader-test (:require [de.otto.tesla.util.env_var_reader :as env-reader] [clojure.test :refer :all] [environ.core :as env])) (deftest ^:unit should-map-keys-to-sanitized-keywords (with-redefs [env/env {"my-var-key" "my-var-value"}] (is (= "my-var-value" (env-reader/read-env-var ["my-var-key"]))))) (deftest ^:unit should-use-fallback-if-env-does-not-have-key (with-redefs [env/env {}] (is (= "default" (env-reader/read-env-var ["my-var-key" "default"]))))) (deftest ^:unit should-return-empty-if-no-fallback-and-key-not-in-env (with-redefs [env/env {}] (is (= "" (env-reader/read-env-var ["my-var-key"]))))) ================================================ FILE: test/de/otto/tesla/util/keyword_test.clj ================================================ (ns de.otto.tesla.util.keyword-test (:require [clojure.test :refer :all] [de.otto.tesla.util.keyword :as kwutil])) (deftest ^:unit should-map-keys-to-sanitized-keywords (is (= (kwutil/sanitize-keywords {"thats.a.property" "a value"}) {:thats-a-property "a value"}))) ================================================ FILE: test/de/otto/tesla/util/sanitize_test.clj ================================================ (ns de.otto.tesla.util.sanitize-test (:require [clojure.test :refer :all] [de.otto.tesla.util.sanitize :as san])) (deftest ^:unit should-sanitize-passwords (is (= (san/hide-passwds {:somerandomstuff "not-so-secret" :somerandomstuff-passwd-somerandomstuff "secret" :somerandomstuff-pw-somerandomstuff "longersecret" :my-private-stuff "sonotpublic" :my-secret-stuff "psstsecret" :nested {:some-passwd "secret"}}) {:somerandomstuff "not-so-secret" :somerandomstuff-passwd-somerandomstuff "***" :somerandomstuff-pw-somerandomstuff "***" :my-private-stuff "***" :my-secret-stuff "***" :nested {:some-passwd "***"} }))) ================================================ FILE: test/de/otto/tesla/util/test_utils_test.clj ================================================ (ns de.otto.tesla.util.test-utils-test (:require [clojure.test :refer :all] [de.otto.tesla.util.test-utils :as u] [com.stuartsierra.component :as c])) (deftest eventually (testing "should eventually get the right random number" (u/eventually (= 1 (rand-int 2)))) (testing "should eventually return the right response" (let [start-time (System/currentTimeMillis)] (u/eventually (= :expected-response (#(let [waited-200msec? (> (- (System/currentTimeMillis) start-time) 200)] (if waited-200msec? :expected-response :wrong-response)))))))) (defrecord StartableComponent [state] c/Lifecycle (start [self] (reset! state :started) self) (stop [self] (reset! state :stopped) self)) (deftest with-started (testing "should start and stop something" (let [state (atom :not-running)] (is (= :not-running @state)) (u/with-started [started (->StartableComponent state)] (is (= :started @state))) (is (= :stopped @state))))) (deftest testing-the-mock-request (testing "should create mock-request" (is (= (u/mock-request :get "url" {}) {:headers {"host" "localhost"} :query-string nil :remote-addr "localhost" :request-method :get :scheme :http :server-name "localhost" :server-port 80 :uri "url"}))) (testing "should create mock-request" (is (= (u/mock-request :get "url" {:headers {"content-type" "application/json"}}) {:headers {"host" "localhost" "content-type" "application/json"} :query-string nil :remote-addr "localhost" :request-method :get :scheme :http :server-name "localhost" :server-port 80 :uri "url"})))) ================================================ FILE: test-resources/local.edn ================================================ {:server-port "9991" :hostname #ts/env [:host-name "localhost"] :status-url #ts/env [:my-custom-status-url "/status"] :prop-without-fallback #ts/env [:prop-without-fallback] :metric {:console {:interval-in-s 120}} :foo {:edn "baz"} :very :local} ================================================ FILE: test-resources/local.properties ================================================ foo.prop=baz server.port=9991 ================================================ FILE: test-resources/logback-test.xml ================================================ tesla true %d{HH:mm:ss.SSS} %-5p %c{5} %t "%m"%n ================================================ FILE: test-resources/private.edn ================================================ ; this is checked in for testing reasons. ; In your projects you should add 'private.edn' to .gitignore. {:very :private} ================================================ FILE: test-resources/test.edn ================================================ {:health-url "/test/health" :foo {:local true}} ================================================ FILE: test-resources/version.properties ================================================ version=test.version commit=test.githash ================================================ FILE: test-utils/de/otto/tesla/util/test_utils.clj ================================================ (ns de.otto.tesla.util.test-utils (:require [com.stuartsierra.component :as comp] [ring.mock.request :as mock])) (defmacro eventually "Generic assertion macro, that waits for a predicate test to become true. 'form' is a predicate test, that clojure.test/is can understand 'timeout': optional; in ms; how long to wait in total (defaults to 5000) 'interval' optional; in ms; how long to pause between tries (defaults to 10) Example: Since this will fail half of the time ... (is (= 1 (rand-int 2))) ... use this: (eventually (= 1 (rand-int 2)))" [form & {:keys [timeout interval] :or {timeout 5000 interval 10}}] `(let [start-time# (System/currentTimeMillis)] (loop [] (let [last-stats# (atom nil) pass?# (with-redefs [clojure.test/do-report (fn [s#] (reset! last-stats# s#))] (clojure.test/is ~form)) took-too-long?# (> (- (System/currentTimeMillis) start-time#) ~timeout)] (if (or pass?# took-too-long?#) (clojure.test/do-report @last-stats#) (do (Thread/sleep ~interval) (recur))))))) (defmacro with-started "bindings => [name init ...] Evaluates body in a try expression with names bound to the values of the inits after (.start init) has been called on them. Finally a clause calls (.stop name) on each name in reverse order." [bindings & body] (if (and (vector? bindings) "a vector for its binding" (even? (count bindings)) "an even number of forms in binding vector") (cond (= (count bindings) 0) `(do ~@body) (symbol? (bindings 0)) `(let [~(bindings 0) (comp/start ~(bindings 1))] (try (with-started ~(subvec bindings 2) ~@body) (finally (comp/stop ~(bindings 0))))) :else (throw (IllegalArgumentException. "with-started-system only allows Symbols in bindings"))) (throw (IllegalArgumentException. "not a vector or bindings-count is not even")))) (defn mock-request "merges additional arguments into a mock-request" [method url args] (let [request (mock/request method url)] (merge-with merge request args)))