├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── debian ├── README.md ├── bin │ └── spotify-lyceum-riehab ├── changelog ├── compat ├── control ├── rules ├── spotify-lyceum-apidocs.install ├── spotify-lyceum-apidocs.links └── spotify-lyceum.install ├── doc └── intro.md ├── lyceum.conf ├── project.clj ├── resources └── riemann_plugin │ └── lyceum │ └── meta.edn ├── scripts └── lyceum-service ├── src ├── leiningen │ └── lyceum.clj └── lyceum │ ├── HttpException.java │ ├── core.clj │ ├── external.clj │ ├── external │ ├── email.clj │ ├── hipchat.clj │ ├── hipchat_backport.clj │ ├── logging.clj │ ├── pagerduty.clj │ └── tcp_forward.clj │ ├── mock.clj │ ├── plugin.clj │ ├── service.clj │ ├── service │ ├── config_file.clj │ ├── rules_loader.clj │ └── rules_loader │ │ ├── composite.clj │ │ ├── directory.clj │ │ └── github.clj │ └── test.clj ├── templates ├── rule.clj └── test.clj ├── test └── lyceum │ ├── core_test.clj │ ├── mock_test.clj │ ├── service │ └── rules_loader │ │ └── composite_test.clj │ └── test_test.clj └── tools └── license /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: clojure 3 | lein: lein2 4 | jdk: 5 | - openjdk7 6 | - oraclejdk7 7 | - oraclejdk8 8 | 9 | cache: 10 | directories: 11 | - $HOME/.m2 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lyceum 2 | [![Build Status](https://travis-ci.org/spotify/lyceum.svg)](https://travis-ci.org/spotify/lyceum) 3 | [![Clojars Project](http://clojars.org/lyceum/latest-version.svg)](http://clojars.org/lyceum) 4 | 5 | A Clojure library designed to help in the authoring and testing of modules 6 | riemann rules. 7 | 8 | Each rule lives and is distributed within a different namespace. 9 | Lyceum will take care to iterate the classpath and look for namespaces 10 | containing rules. 11 | 12 | Each namespace can be put under test using a test fixture provided by lyceum 13 | that takes care to setup and tear down all necessary state to simulate riemann 14 | operation. 15 | 16 | Tests are run with little overhead, it is not required to start a riemann 17 | core, instead lyceum takes the approach of completely _simulating_ riemann 18 | operation while imposing a little bit of structure on the way you write rules 19 | to make things manageable. 20 | 21 | Some key points. 22 | 23 | * When running tests, all external interaction is faked and verifiable (using 24 | `check-externals`). 25 | * The riemann schedules is replaced with a global override of 26 | `riemann.time/schedule!` that has a thread-local implementation without global 27 | state. 28 | With this comes also faked time (`riemann.time/unix-time` et. al.). 29 | * Provides a [restful HTTP service](#http-service) for evaluating rules on-the-fly. 30 | 31 | # Getting Started 32 | 33 | Start by initializing an empty leiningen project; 34 | 35 | ``` 36 | #> lein new my-rules 37 | #> cd my-rules 38 | #> rm src/my_rules/core.clj 39 | #> rm test/my_rules/core_test.clj 40 | ``` 41 | 42 | Add a testing scope dependency to lyceum in your project.clj 43 | 44 | ```clojure 45 | (defproject my-rules "0.1.0-SNAPSHOT" 46 | :description "FIXME: write description" 47 | :url "http://example.com/FIXME" 48 | :license {:name "Eclipse Public License" 49 | :url "http://www.eclipse.org/legal/epl-v10.html"} 50 | :dependencies [[org.clojure/clojure "1.5.1"] 51 | ; add both as a test dependency. 52 | [lyceum "0.1.0" :scope "test"]] 53 | ; ... and as a plugin. 54 | :plugins [[lyceum "0.1.0"]] 55 | ; define which namespace lyceum should initialize new rules in. 56 | :lyceum-namespace my-rules.rules) 57 | ``` 58 | 59 | Download templates and generate a new namespace skeleton. 60 | 61 | ``` 62 | #> mkdir templates 63 | #> wget https://raw.githubusercontent.com/spotify/lyceum/master/templates/rule.clj -O templates/rule.clj 64 | #> wget https://raw.githubusercontent.com/spotify/lyceum/master/templates/test.clj -O templates/test.clj 65 | ``` 66 | 67 | Now it's time to initialize rules for a group called __my-group__. 68 | 69 | ``` 70 | #> lein lyceum init my-group 71 | ``` 72 | 73 | Almost there, verify that your rules are working by running your skeleton 74 | test-cases! 75 | 76 | ``` 77 | #> lein test 78 | ``` 79 | 80 | Now you can start modifying *src/my\_rules/rules/my\_group.clj* to suit your 81 | needs. 82 | 83 | Any test-cases you come up with should be added to 84 | *test/my\_rules/rules/my_group_test.clj*, use the generated one as inspiration 85 | for writing more. 86 | 87 | When you are done, compile your rules. 88 | 89 | ``` 90 | #> lein uberjar 91 | ``` 92 | 93 | Add lyceum and the rules jar containing to the classpath of riemann and 94 | add the following declaration in your riemann.config. 95 | 96 | ```clojure 97 | ; load-plugins has to be present! 98 | (load-plugins) 99 | 100 | (streams 101 | ; load any namespaces containing rules under 'my-rules.rules'. 102 | ; blacklist any tutorial namespaces to avoid loading them. 103 | (lyceum/load-rules 104 | 'my-rules.rules 105 | :opts {:index index} 106 | :blacklist [ 107 | #"my-rules.rules.tutorial\d+" 108 | ])) 109 | ``` 110 | 111 | Make sure that you start the riemann service with the system property 112 | `-Dlyceum.mode=real` (pass this as an argument to _java_ like `java 113 | -Dlyceum.mode=real ... riemann.bin`). 114 | See the [externals section](#externals) for more details about this. 115 | 116 | # Time 117 | 118 | Time is controlled in a similar fashion to how `riemann.controlled.time` works, 119 | but it's done with a separate implementation in lyceum. 120 | 121 | Any input event is inspected for its `:time` field, and if present, the time 122 | specified is used as the current time for the simulation. 123 | 124 | This can bee seen in the example test-case that you generated if you followed 125 | the guide above. 126 | 127 | # Externals 128 | 129 | Lyceum uses __externals__ to interact with external systems, externals are thin 130 | wrappers around the riemann's external integrations (email, pagerduty, etc.). 131 | 132 | By default lyceum uses the __fake__ mode which will cause any external 133 | interactions to be logged instead of realized. 134 | This is what allows the test-cases to verify external effects with 135 | `check-externals`. 136 | 137 | However when running in production, the mode is set to __real__. This will 138 | cause the wrapping to be discarded and the real external interaction to be 139 | realized. 140 | 141 | The mode is set with the `-Dlyceum.mode=` system property that should 142 | be passed to your JVM. 143 | 144 | Valid modes are. 145 | 146 | * __fake (default)__ - Log any externals that are triggered to an internal 147 | data-structure, allowing for later verification. 148 | * __test__ - Write external interaction to a log file. 149 | * __real__ - Realize any externals that are triggered. 150 | 151 | Wrapping your own external is straight forward, you can use 152 | [the email external](src/lyceum/external/email.clj) as an example for how to do 153 | this. 154 | 155 | # HTTP Service 156 | 157 | You can start the HTTP service by running. 158 | 159 | ``` 160 | #> lein run [lyceum.conf] 161 | ``` 162 | 163 | Or the ___lyceum.service___ class in the resulting uberjar. 164 | 165 | ``` 166 | #> java -jar lyceum.service [lyceum.conf] 167 | ``` 168 | 169 | It expects to find a [lyceum.conf](lyceum.conf) in the current working directory, or one can be provided as an argument. 170 | 171 | The service is currently capable of loading rules the following ways. 172 | 173 | + Github through [github-rules](src/lyceum/service/rules_loader/github.clj) 174 | + Filesystem through [directory-rules](src/lyceum/service/rules_loader/directory.clj) 175 | 176 | #### POST /eval 177 | 178 | Will evaluate the received data (___VERY UNSAFE___) and apply the provided rules to them. 179 | 180 | + Response 200 (application/json) 181 | + Response 500 (application/json) 182 | 183 | ###### Request Structure 184 | ```javascript 185 | {"data": , "events": [, ..]} 186 | ``` 187 | 188 | ###### Response Structure 189 | ```javascript 190 | {/* contains any external events (like pagerduty) that happened */ 191 | "reports":[, ..], 192 | /* contains the events which was indexed during this evaluation. */ 193 | "index":[, ..] 194 | } 195 | ``` 196 | 197 | ###### Example CURL 198 | ``` 199 | #> curl http://localhost:8080/eval -H "Content-Type: application/json" 200 | -d '{"data": "(ns hello.world) (defn rules [{:keys [index]}] (fn [e] (index e)))", "events": [{"service": "foo"}]}' 201 | ``` 202 | 203 | #### GET /ns/ 204 | 205 | Will request a list of namespaces, see [lyceum.conf](lyceum.conf) for how this is configured. 206 | 207 | #### GET /ns/{ns} 208 | 209 | Will request the content of a specific namespace ___{ns}___, see [lyceum.conf](lyceum.conf) for how this is configured. 210 | -------------------------------------------------------------------------------- /debian/README.md: -------------------------------------------------------------------------------- 1 | # Build Instructions 2 | 3 | Since 'lein' currently does not exist as a debian package, this has to be built 4 | locally and subsequently uploaded to our debian repository. 5 | 6 | For a local build, make sure lein is available somewhere in your global path. 7 | -------------------------------------------------------------------------------- /debian/bin/spotify-lyceum-riehab: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | JVMARGS="/etc/spotify/default/spotify-lyceum-riehab.jvmargs" 4 | 5 | if [[ -f "$JVMARGS" ]]; then 6 | . $JVMARGS 7 | fi 8 | 9 | CLASSPATH="/usr/lib/spotify-lyceum/lyceum-standalone.jar" 10 | 11 | if [[ -n "$EXTRA_CLASSPATH" ]]; then 12 | CLASSPATH+=":${EXTRA_CLASSPATH}" 13 | fi 14 | 15 | exec java -cp $CLASSPATH lyceum.riehab "$@" 16 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | spotify-lyceum (0.1.1) unstable; urgency=low 2 | 3 | * Placeholder. 4 | 5 | -- Hero Squad Wed, 23 Oct 2013 17:40:35 +0200 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: spotify-lyceum 2 | Section: non-free/net 3 | Priority: extra 4 | Maintainer: Hero Squad 5 | Standards-Version: 3.8.3 6 | Build-Depends: 7 | debhelper, 8 | default-jdk 9 | 10 | Package: spotify-lyceum 11 | Depends: 12 | default-jre 13 | Architecture: all 14 | Description: Project to distribute riemann rules for spotify. 15 | 16 | Package: spotify-lyceum-apidocs 17 | Architecture: all 18 | Description: API documentation for lyceum. 19 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | 6 | LEIN=debian/lein 7 | LEIN_OPTS=with-profile user 8 | LEIN_HOME=debian/lein_home 9 | 10 | $(LEIN_HOME): 11 | mkdir -p $(LEIN_HOME)/.lein 12 | cp debian/profiles.clj $(LEIN_HOME)/.lein 13 | 14 | $(LEIN): $(LEIN_HOME) 15 | curl https://raw.github.com/technomancy/leiningen/stable/bin/lein -L -o $(LEIN) 16 | chmod +x $(LEIN) 17 | 18 | override_dh_clean: $(LEIN) 19 | LEIN_ROOT=1 LEIN_JVM_OPTS="-Duser.home=$(LEIN_HOME)" HOME=$(LEIN_HOME) $(LEIN) $(LEIN_OPTS) clean 20 | dh_clean 21 | 22 | override_dh_auto_test: $(LEIN) 23 | LEIN_ROOT=1 LEIN_JVM_OPTS="-Duser.home=$(LEIN_HOME)" HOME=$(LEIN_HOME) $(LEIN) $(LEIN_OPTS) test 24 | 25 | override_dh_auto_build: $(LEIN) 26 | LEIN_ROOT=1 LEIN_JVM_OPTS="-Duser.home=$(LEIN_HOME)" HOME=$(LEIN_HOME) $(LEIN) $(LEIN_OPTS) uberjar 27 | LEIN_ROOT=1 LEIN_JVM_OPTS="-Duser.home=$(LEIN_HOME)" HOME=$(LEIN_HOME) $(LEIN) $(LEIN_OPTS) marg -m 28 | 29 | override_dh_install: 30 | mkdir target/debian 31 | mv target/lyceum-*-standalone.jar target/debian/lyceum-standalone.jar 32 | mv target/lyceum-*.jar target/debian/lyceum.jar 33 | dh_install 34 | -------------------------------------------------------------------------------- /debian/spotify-lyceum-apidocs.install: -------------------------------------------------------------------------------- 1 | docs/* usr/share/spotify-lyceum/docs/api/lyceum 2 | -------------------------------------------------------------------------------- /debian/spotify-lyceum-apidocs.links: -------------------------------------------------------------------------------- 1 | usr/share/spotify-lyceum/docs/api/lyceum/toc.html usr/share/spotify-lyceum/docs/api/lyceum/index.html 2 | -------------------------------------------------------------------------------- /debian/spotify-lyceum.install: -------------------------------------------------------------------------------- 1 | target/debian/*.jar usr/lib/spotify-lyceum 2 | riehab-public/* usr/lib/spotify-lyceum/riehab-public 3 | debian/bin/* usr/bin 4 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to lyceum 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /lyceum.conf: -------------------------------------------------------------------------------- 1 | ; vim: filetype=clojure 2 | 3 | (rule-loaders 4 | (github-rules 5 | :url "https://api.github.com" 6 | :repo "aphyr/riemann" 7 | :prefix "src/" 8 | :id-prefix "riemann.") 9 | (directory-rules 10 | :path "../riemann-rules/src/" 11 | :id-prefix "spotify.rules.")) 12 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lyceum "0.1.1-SNAPSHOT" 2 | :description "A riemann plugin to build and deploy modular rules." 3 | :url "https://github.com/spotify/lyceum" 4 | :license {:name "Apache License 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | :dependencies 7 | [ 8 | [org.clojure/clojure "1.7.0"] 9 | [riemann/riemann "0.2.10"] 10 | [http-kit "2.1.16"] 11 | [compojure "1.1.6"] 12 | [javax.servlet/javax.servlet-api "3.1.0"] 13 | [base64-clj "0.1.1"]] 14 | :plugins [ 15 | [lein-marginalia "0.7.1"] 16 | ] 17 | :java-options ["-Dlyceum.mode=test"] 18 | :source-path "src/" 19 | :java-source-paths ["src/"] 20 | :test-selectors { 21 | :default (complement :integration) 22 | :integration :integration 23 | :all (constantly true) 24 | } 25 | :main lyceum.service 26 | ) 27 | -------------------------------------------------------------------------------- /resources/riemann_plugin/lyceum/meta.edn: -------------------------------------------------------------------------------- 1 | {:plugin "lyceum" 2 | :title "A plugin for loading and testing rules from separate namespaces." 3 | :git-repo "https://github.com/spotify/lyceum" 4 | :require lyceum.plugin} 5 | -------------------------------------------------------------------------------- /scripts/lyceum-service: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | JAR="/usr/lib/lyceum/lyceum-standalone.jar" 3 | 4 | if [[ -d target ]]; then 5 | JAR=$(find target -maxdepth 1 -name 'lyceum-*-standalone.jar') 6 | fi 7 | 8 | if [[ -f /etc/default/lyceum ]]; then 9 | source /etc/default/lyceum 10 | fi 11 | 12 | LYCEUM_CLASSPATH="$JAR" 13 | 14 | if [[ -n $CLASSPATH ]]; then 15 | LYCEUM_CLASSPATH="$CLASSPATH:$LYCEUM_CLASSPATH" 16 | fi 17 | 18 | echo "LYCEUM_CLASSPATH=$LYCEUM_CLASSPATH" 19 | exec java -cp "$LYCEUM_CLASSPATH" lyceum.service "$@" 20 | -------------------------------------------------------------------------------- /src/leiningen/lyceum.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns leiningen.lyceum 17 | (:require [clojure.java.io :as io] 18 | [clojure.pprint :as pprint] 19 | [clojure.string :as string])) 20 | 21 | (defn- path-replace 22 | [part] 23 | (string/replace part "-" "_")) 24 | 25 | (defmulti lyceum-action 26 | (fn [project action args] 27 | action)) 28 | 29 | (defmethod lyceum-action :init 30 | [project action args] 31 | 32 | (when (empty? args) 33 | (println "Usage: lyceum :init ") 34 | (System/exit 1)) 35 | 36 | (when-not (:lyceum-namespace project) 37 | (println "Missing :lyceum-namespace from project.clj") 38 | (System/exit 1)) 39 | 40 | (let [root (:root project) 41 | lyceum-namespace (:lyceum-namespace project) 42 | first-arg (first args) 43 | init-ns (symbol (str lyceum-namespace "." first-arg)) 44 | base-path (str (string/join "/" (map path-replace (string/split (str init-ns) #"\.")))) 45 | test-template (slurp (str root "/templates/test.clj")) 46 | rule-template (slurp (str root "/templates/rule.clj"))] 47 | 48 | (when-let [rule-path (first (:source-paths project))] 49 | (let [rule-path (str rule-path "/" base-path ".clj")] 50 | (io/make-parents rule-path) 51 | (with-open [out (io/writer rule-path)] 52 | (println (str "Writing rule: " rule-path)) 53 | (.write out (string/replace rule-template "__NS__" (str init-ns)))))) 54 | 55 | (when-let [test-path (first (:test-paths project))] 56 | (let [test-path (str test-path "/" base-path "_test.clj")] 57 | (io/make-parents test-path) 58 | (with-open [out (io/writer test-path)] 59 | (println (str "Writing test: " test-path)) 60 | (.write out (string/replace test-template "__NS__" (str init-ns)))))))) 61 | 62 | (defmethod lyceum-action :default 63 | [project action args] 64 | (throw (RuntimeException. (str "No such action: " action)))) 65 | 66 | (defn lyceum 67 | [project & args] 68 | 69 | (when (empty? args) 70 | (println "Usage: lyceum [action] [options]") 71 | (println "Available actions:") 72 | (println " init - Initialize a rules namespace, ex. :init my-team") 73 | (System/exit 1)) 74 | 75 | (lyceum-action project (keyword (first args)) (rest args))) 76 | -------------------------------------------------------------------------------- /src/lyceum/HttpException.java: -------------------------------------------------------------------------------- 1 | package lyceum; 2 | 3 | public class HttpException extends Exception { 4 | private final int status; 5 | private final String message; 6 | 7 | public HttpException(int status, String message) { 8 | this.status = status; 9 | this.message = message; 10 | } 11 | 12 | public int getStatus() { 13 | return this.status; 14 | } 15 | 16 | public String getMessage() { 17 | return this.message; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lyceum/core.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | ;; # Core components essential to lyceum 17 | (ns lyceum.core 18 | (:require [riemann.streams :refer [call-rescue]]) 19 | (:require [clojure.tools.logging :refer [info error]])) 20 | 21 | ;; A function that reads the current lyceum mode from system properties. 22 | ;; 23 | ;; The following modes are valid. 24 | ;; 25 | ;; * `:fake` - Prevents external interactions from being executed, just 26 | ;; appended to `lyceum.external/*external-reports*`. 27 | ;; * `:test` - Prevents external interactions from being executed, just logged 28 | ;; to standard logging facilities. 29 | ;; * `:real` - All external interactions will both be executed and logged. 30 | (defn get-lyceum-mode 31 | [] 32 | (keyword (System/getProperty "lyceum.mode" "fake"))) 33 | 34 | (defmacro def-rules 35 | "Define a rules function for the current namespace. 36 | 37 | This will define the entry point for lyceum. 38 | The `index` symbol will be defined within the block in def-rules and 39 | corresponds to the value that was passed into the `:index` key in 40 | `load-rules`." 41 | [& body] 42 | `(defn ~'rules 43 | [{:keys [~'index]}] 44 | (let [bodies# [~@body]] 45 | (fn [e#] (call-rescue e# bodies#))))) 46 | 47 | (defmacro require-depends 48 | "Requires all necessary dependencies and binds them to the current 49 | namespace. 50 | 51 | This should contain most dependencies that are available in a regular 52 | riemann.config file." 53 | [] 54 | (require '[riemann.streams :refer :all]) 55 | (require '[riemann.time :refer [unix-time linear-time once! every!]]) 56 | (require '[lyceum.external.email :refer :all]) 57 | (require '[lyceum.external.hipchat :refer :all]) 58 | (require '[lyceum.external.pagerduty :refer :all]) 59 | (require '[lyceum.external.logging :refer :all]) 60 | (require '[lyceum.external.tcp-forward :refer :all])) 61 | 62 | (def default-index 63 | (fn [e] (error "No indexing function set in lyceum/load-rules!"))) 64 | 65 | (defn rules-for-ns 66 | "Load rules for the `rule-ns` namespace passing in options `opts`. 67 | 68 | Returns `nil` if the specified rule cannot be found." 69 | [{:keys [index] :or {index default-index}} rule-ns] 70 | (when-let [rule-ns (find-ns rule-ns)] 71 | (when-let [rule-fn (ns-resolve rule-ns 'rules)] 72 | (rule-fn {:index index})))) 73 | 74 | (defn rules-for-nss 75 | "Load all specified rules in `rule-nss` with the option `opts`. 76 | 77 | Essentially a wrapped around `rules-for-ns` but `nil` results are ignored." 78 | [opts rule-nss] 79 | (let [map-fn (partial rules-for-ns opts)] 80 | (for [rule-ns rule-nss :let [f (map-fn rule-ns)] :when f] 81 | f))) 82 | 83 | (defn rules-for-all 84 | "Create a stream function that corresponds to the rules specified in 85 | `rule-nss` with the options `opts`. 86 | 87 | Essentially a wrapper for `rules-for-nss` but defines a stream function out 88 | of the results." 89 | [opts rule-nss] 90 | (let [functions (doall (rules-for-nss opts rule-nss))] 91 | (fn [e] (call-rescue e functions)))) 92 | -------------------------------------------------------------------------------- /src/lyceum/external.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | ;; # External integrations 17 | ;; `externals` are lyceum's way of interacting with the outside world. 18 | ;; 19 | ;; In essence they are wrappers around other types of external integrations to 20 | ;; provide a convenience layer to enable integration testing. 21 | (ns lyceum.external 22 | (:require [riemann.time] 23 | [riemann.pubsub :as pubsub] 24 | [riemann.core :as core]) 25 | (:require [lyceum.core :refer [get-lyceum-mode]]) 26 | (:require [riemann.common :as common] 27 | [riemann.config :as config] 28 | [riemann.streams :as streams]) 29 | (:require [clojure.tools.logging :refer [info error]])) 30 | 31 | ;; If bound, will contain a list of _all_ external interactions that has 32 | ;; occured as formatted by `format-report`. 33 | (def ^:dynamic *external-reports*) 34 | 35 | (defn- format-report 36 | "Format a single external the external named by `external` containing an 37 | `event`, a `message` and `extra` parameters." 38 | [external message extra event] 39 | (let [now (riemann.time/unix-time)] 40 | (list external {:message message :event event 41 | :extra extra :time now}))) 42 | 43 | (defn fake-report 44 | "Build a stream function that causes an a message as formatted by 45 | `format-report` to be appended to `*external-reports*` when it is 46 | called." 47 | [external message extra] 48 | (fn [e] 49 | (if (bound? #'*external-reports*) 50 | (swap! *external-reports* conj (format-report external message extra e)) 51 | (error "*external-reports* is not bound, you probably want to run riemann with -Dlyceum.mode=real or -Dlyceum.mode=test")))) 52 | 53 | (defn real-report 54 | [external message opts & children] 55 | (let [external-s (name external)] 56 | (fn real-report-fn [e] 57 | (info (str external " " message ": " opts " - " (common/event-to-json e))) 58 | (when-let [core @config/core] 59 | (pubsub/publish! 60 | (:pubsub core) 61 | "lyceum.external" 62 | (common/event (merge opts 63 | {:external_name external :external_message message} 64 | e)))) 65 | (streams/call-rescue e children)))) 66 | 67 | (defn test-report 68 | [external message opts] 69 | (fn test-report-fn [e] 70 | (info (str external " (TEST) " message ": " opts " - " (common/event-to-json e))))) 71 | 72 | (defmacro report 73 | [external message opts & children] 74 | (let [m (get-lyceum-mode)] 75 | (case m 76 | :real `(real-report ~external ~message ~opts ~@children) 77 | :test `(test-report ~external ~message ~opts) 78 | :fake `(fake-report ~external ~message ~opts) 79 | (throw (RuntimeException. (str "Unknown lyceum mode: " m)))))) 80 | 81 | (defn report-external 82 | "Dispatch all accumulated external reports to the specified `reporter` 83 | function." 84 | [reporter] 85 | (let [reports @*external-reports*] 86 | (doseq [report reports] 87 | (reporter report)))) 88 | -------------------------------------------------------------------------------- /src/lyceum/external/email.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | ;; # External e-mail integration. 17 | ;; Wraps riemann's [email](http://riemann.io/api/riemann.email.html) 18 | ;; implementation for sending e-mails. 19 | (ns lyceum.external.email 20 | (:require [lyceum.external :refer [report]]) 21 | (:require [riemann.email :as email])) 22 | 23 | ;; ## Send an e-mail on events. 24 | ;; Send e-mails using `mailer-opts` to the specified `recipients`. 25 | 26 | ;; Options (`mailer-opts`) is of the same form as provided to 27 | ;; [`riemann.email/mailer`](http://riemann.io/api/riemann.email.html#var-mailer). 28 | (defn email 29 | "Returns a stream function that emails events it receives" 30 | 31 | [opts recipients] 32 | (report 33 | :email "Send E-Mail" (merge opts {:recipients recipients}) 34 | (apply (email/mailer opts) recipients))) 35 | -------------------------------------------------------------------------------- /src/lyceum/external/hipchat.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.external.hipchat 17 | (:require [lyceum.external :refer [report]]) 18 | (:require [lyceum.external.hipchat-backport :as hipchat])) 19 | 20 | (defn hipchat 21 | [opts] 22 | (report :hipchat "Send HipChat Message" opts 23 | (hipchat/hipchat opts))) 24 | -------------------------------------------------------------------------------- /src/lyceum/external/hipchat_backport.clj: -------------------------------------------------------------------------------- 1 | ;; # A backport and extension of the official hipchat plugin 2 | ;; This came to be because the version we were running at the time (0.2.2) did 3 | ;; not support hipchat. 4 | ;; 5 | ;; Original: https://github.com/aphyr/riemann/blob/master/src/riemann/hipchat.clj 6 | ;; 7 | ;; $LICENSE 8 | ;; The original source did not have a copyright statement, so we assume it was 9 | ;; released under the same license as the rest of Riemann which is the 10 | ;; Eclipse Public License - v 1.0, see 11 | ;; https://github.com/aphyr/riemann/blob/master/LICENSE for the copyright 12 | ;; statement. 13 | ;; 14 | ;; This code has been extended to add slightly more sane HTML bodies. 15 | (ns ^{:doc "Forwards events to HipChat" 16 | :author "Hubert Iwaniuk"} 17 | lyceum.external.hipchat-backport 18 | (:require [clj-http.client :as client] 19 | [clojure.string :refer [join]] 20 | [cheshire.core :as json])) 21 | 22 | (def ^:private chat-url 23 | "https://api.hipchat.com/v1/rooms/message?format=json") 24 | 25 | (defn- escape-html 26 | "Change special characters into HTML character entities." 27 | [text] 28 | (.. ^String (str text) 29 | (replace "&" "&") 30 | (replace "<" "<") 31 | (replace ">" ">") 32 | (replace "\"" """))) 33 | 34 | (defn- format-message 35 | [{:keys [host service state metric description tags]}] 36 | (let [description (if (nil? description) 37 | "(no description)" 38 | (if (< (count description) 150) 39 | description 40 | (subs description 0 150))) 41 | service (if-not (nil? service) 42 | service 43 | "(no service)") 44 | host (if-not (nil? host) 45 | host 46 | "(no host)") 47 | tags (join ", " (map escape-html tags)) 48 | parts [(str "[" (escape-html state) "]") " " 49 | (escape-html host) " " 50 | (str "" (escape-html service) "") "
" 51 | (escape-html description) "
" 52 | (str "Tags: " tags) 53 | (if-not (nil? metric) 54 | (str "
" "Metric: " (escape-html metric)) 55 | "")]] 56 | (join parts))) 57 | 58 | (defn- format-event 59 | [params event] 60 | (merge {:color (condp = (:state event) 61 | "ok" "green" 62 | "critical" "red" 63 | "error" "red" 64 | "yellow")} 65 | params 66 | (when-not (:message params) 67 | (prn (format-message event)) 68 | {:message (format-message event)}))) 69 | 70 | (defn- post 71 | "POST to the HipChat API." 72 | [token params event] 73 | (let [form-params (format-event (assoc params :message_format "html") event)] 74 | (client/post (str chat-url "&auth_token=" token) 75 | {:form-params form-params 76 | :socket-timeout 5000 77 | :conn-timeout 5000 78 | :accept :json 79 | :throw-entire-message? true}))) 80 | 81 | (defn hipchat 82 | "Creates a HipChat adapter. Takes your HipChat authentication token, 83 | and returns a function which posts a message to a HipChat. 84 | 85 | (let [hc (hipchat {:token \"...\" 86 | :room 12345 87 | :from \"Riemann reporting\" 88 | :notify 0})] 89 | (changed-state hc))" 90 | [{:keys [token room from notify color]}] 91 | (fn [e] (post token 92 | {:room_id room 93 | :from from 94 | :notify notify} 95 | e))) 96 | -------------------------------------------------------------------------------- /src/lyceum/external/logging.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.external.logging 17 | (:require [lyceum.external :refer [report]])) 18 | 19 | (defn logging 20 | [data] 21 | (report :logging "Logging" data)) 22 | -------------------------------------------------------------------------------- /src/lyceum/external/pagerduty.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.external.pagerduty 17 | (:require [lyceum.external :refer [report]]) 18 | (:require [riemann.common :refer [event-to-json]] 19 | [riemann.pagerduty :as pagerduty])) 20 | 21 | (def ^:private map-keys [:trigger :resolve :acknowledge]) 22 | 23 | (defn- pagerduty-make-map 24 | "Helper function to build a pagerduty map using a filter function for each 25 | action. 26 | The supplied function will be applied to all available keys. 27 | 28 | The map will be of the form: 29 | {:action (make-fn :action) ...} 30 | 31 | Note that the map keys are defines statically as the global var 'map-keys'." 32 | [make-fn] 33 | (apply hash-map (flatten 34 | (interleave map-keys 35 | (map make-fn map-keys))))) 36 | 37 | (defn pagerduty 38 | [api-key] 39 | (pagerduty-make-map 40 | (fn [action] 41 | (report 42 | :pagerduty "Send Event to PagerDuty" {:api_key api-key :action action} 43 | (let [pd (riemann.pagerduty/pagerduty api-key)] 44 | (pd action)))))) 45 | -------------------------------------------------------------------------------- /src/lyceum/external/tcp_forward.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.external.tcp-forward 17 | (:require [lyceum.external :refer [report]]) 18 | (:require [riemann.streams :refer [forward]] 19 | [riemann.client :refer [tcp-client]])) 20 | 21 | (defn tcp-forward 22 | [& {:keys [host]}] 23 | (report :tcp-forward {:host host} 24 | (let [client (tcp-client :host host)] 25 | (forward client)))) 26 | -------------------------------------------------------------------------------- /src/lyceum/mock.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | ;; # A simple mocking library 17 | (ns lyceum.mock 18 | (:require clojure.test)) 19 | 20 | ;; Record all mock calls. 21 | (def ^:dynamic *mocks-db*) 22 | 23 | (defmacro with-mocks [& body] 24 | "Sets up the mocking framework by binding `*mocks-db*` in the scope of the 25 | specified body." 26 | `(binding [*mocks-db* (atom {})] 27 | ~@body)) 28 | 29 | (defn make-mock-fn 30 | "Builds a mock function by `callee-fn-name` that will always return the value 31 | `return-value`. 32 | 33 | Will return a function that once called, logs the call to `*mocks-db*` 34 | (if bound) and return `return-value`. 35 | 36 | The `calee-fn-name` parameter exists to give a better error when 37 | pre-conditions fail since it contains the name of _who_ requested the mock 38 | function." 39 | [callee-fn-name return-value] 40 | (assert (bound? #'*mocks-db*) 41 | (str "Function must be inside a with-mocks block: " callee-fn-name)) 42 | (let [mocked-fn 43 | (fn mocked-fn [& args] 44 | (swap! *mocks-db* update-in [mocked-fn] #(concat % [args])) 45 | return-value)] 46 | (swap! *mocks-db* assoc-in [mocked-fn] (list)) 47 | mocked-fn)) 48 | 49 | (defn make-mocks 50 | "Build mock functions 51 | 52 | Create a list of forms containing definition of mock functions compatible 53 | with with-redefs, let and binding. 54 | 55 | `mock-defs` should be a sequence containing sequences of the form 56 | `(name[ return-value])`. 57 | 58 | * The `name` part for each element will be used as the local variable where 59 | the mock function is bound. 60 | * The `return-value` corresponds to what invocations of this function should 61 | return." 62 | [callee-fn-name mock-defs] 63 | {:pre [(vector? mock-defs) 64 | ; Make sure every mock definition is a sequence. 65 | (every? seq? mock-defs) 66 | ; make sure every mock definition has at at least the first 67 | ; value. Second (return-value) will default to nil. 68 | (every? first mock-defs)]} 69 | (let [make-mock-expr #(list 'make-mock-fn 70 | callee-fn-name 71 | (second %)) 72 | mocks (map make-mock-expr mock-defs) 73 | fn-names (map #(first %) mock-defs)] 74 | (interleave fn-names mocks))) 75 | 76 | (defn lookup-calls 77 | "Lookup all calls to a mocked function (`mocked-fn`) 78 | 79 | If any calls could be found, filters the result using `filter-fn`. 80 | 81 | Returns `nil` if no calls could be found and reports the problem using 82 | clojure.test/report." 83 | [mocked-fn filter-fn] 84 | (let [calls (@*mocks-db* mocked-fn)] 85 | (if (nil? calls) 86 | (do 87 | (clojure.test/report 88 | {:type :fail 89 | :message (str "Expected '" mocked-fn "' to be a mocked function") }) 90 | nil) 91 | (filter-fn calls)))) 92 | 93 | (defmacro binding-mocks 94 | "Setup a binding of mocked functions using `with-redefs`. 95 | 96 | For the structure of `mock-defs`, see [`make-mocks`](#make-mocks). 97 | 98 | Evaluates `body` with the defined binding in effect." 99 | [mock-defs & body] 100 | (let [mocks (make-mocks ''binding-mocks mock-defs)] 101 | `(with-redefs [~@mocks] 102 | ~@body))) 103 | 104 | (defmacro let-mocks 105 | "Define new mock functions which did not exist before." 106 | [mock-defs & body] 107 | (let [mocks (make-mocks ''let-mocks mock-defs)] 108 | `(let [~@mocks] ~@body))) 109 | 110 | (defn is-called-nth 111 | "Check that `mocked-fn` call number `n` matches `args`." 112 | [mocked-fn n args] 113 | (let [msg (str "Expected '" mocked-fn "' call number " n " to have parameters: " args)] 114 | (let [actual (lookup-calls mocked-fn #(get % n))] 115 | (when-not (nil? actual) 116 | (clojure.test/is (= args actual) msg))))) 117 | 118 | (defn is-called-count 119 | "Macro to check that `mocked-fn` has `n` calls." 120 | [mocked-fn n] 121 | (let [msg (str "Expected '" mocked-fn "' to be called " n " time(s)")] 122 | (let [actual (lookup-calls mocked-fn #(count %))] 123 | (when-not (nil? actual) 124 | (clojure.test/is (= n actual) msg))))) 125 | 126 | (defn is-not-called [mocked-fn] 127 | "Macro to check that `mocked-fn` has not been called." 128 | (let [msg (str "Expected '" mocked-fn "' to not be called")] 129 | (let [actual (lookup-calls mocked-fn #(count %))] 130 | (when-not (nil? actual) 131 | (clojure.test/is (= 0 actual) msg))))) 132 | 133 | (defmacro is-called 134 | "Helper macro to check all call arguments. 135 | 136 | Uses [`is-called-count`](#is-called-count) and 137 | [`is-called-nth`](#is-called-nth) to verify that all specified arguments 138 | match." 139 | [mocked-fn & args] 140 | (let [c (count args) 141 | expr (for [i (range 0 c)] 142 | (list 'is-called-nth mocked-fn i (nth args i)))] 143 | `(do 144 | (is-called-count ~mocked-fn ~c) 145 | ~@expr))) 146 | -------------------------------------------------------------------------------- /src/lyceum/plugin.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.plugin 17 | (:require [clojure.java.classpath :as cp]) 18 | (:require [clojure.tools.logging :refer [info warn error]]) 19 | (:require [lyceum.core :as core])) 20 | 21 | (defn- fn-to-ns 22 | [path] 23 | (let [n (.substring path 0 (- (.length path) 4))] 24 | (symbol (.replace n "/" ".")))) 25 | 26 | (defn- blacklist-matcher 27 | [blacklist] 28 | (fn [s] 29 | (let [n (name s)] 30 | (not 31 | (every? 32 | nil? 33 | (map #(re-matches % n) blacklist)))))) 34 | 35 | (defn- load-namespaces 36 | "Loads all namespaces not matching blacklist. 37 | 38 | Namespaces that should not be loaded" 39 | [namespaces matches-blacklist?] 40 | (for [n namespaces 41 | :when 42 | (do 43 | (if (matches-blacklist? n) 44 | (do 45 | (warn (str "Not loading blacklisted namespace: " n)) 46 | false) 47 | (do 48 | (info (str "Loading namespace: " n)) 49 | true)))] 50 | (try 51 | (do 52 | (require n) 53 | n) 54 | (catch Exception e 55 | (error e (str "Failed to require namespace: " n)) 56 | nil)))) 57 | 58 | (defn load-rules 59 | [base-ns & {:keys [opts blacklist] 60 | :or {opts {} blacklist []}}] 61 | (let [files (mapcat cp/filenames-in-jar (cp/classpath-jarfiles)) 62 | path-prefix (str (.replace (name base-ns) "." "/") "/") 63 | matches-blacklist? (blacklist-matcher blacklist) 64 | namespaces (for [file files 65 | :when (and (.startsWith file path-prefix) 66 | (.endsWith file ".clj")) 67 | :let [file-ns (fn-to-ns file)]] 68 | file-ns)] 69 | (let [ns-names (load-namespaces namespaces matches-blacklist?)] 70 | (core/rules-for-all opts (filter (comp not nil?) ns-names))))) 71 | -------------------------------------------------------------------------------- /src/lyceum/service.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | ;; # Experimental http server for testing rules. 17 | ;; 18 | ;; * Start: __lein run__ 19 | ;; * Surf to http://localhost:8080 20 | (ns lyceum.service 21 | (:import 22 | java.io.InputStreamReader 23 | java.io.ByteArrayInputStream 24 | java.io.PushbackReader 25 | lyceum.HttpException) 26 | (:require 27 | [lyceum.service.rules-loader :as loader] 28 | [lyceum.service.config-file :as config-file]) 29 | (:require 30 | riemann.logging 31 | riemann.common 32 | riemann.time) 33 | (:require 34 | [lyceum.test :refer [with-mocked-riemann-time make-send-events-fn]] 35 | [lyceum.external :refer [*external-reports*]] 36 | [clojure.tools.logging :refer [info error]] 37 | [cheshire.core :as json] 38 | [compojure.route :refer [files not-found]] 39 | [compojure.handler :refer [site]] 40 | [compojure.core :refer [routes GET POST DELETE ANY context]] 41 | [org.httpkit.server :refer [run-server]]) 42 | (:gen-class :name lyceum.service)) 43 | 44 | (defn handle-json 45 | [handler & args] 46 | (let [headers {"Content-Type" "application/json"} 47 | serialize json/generate-string 48 | respond (fn [status body] 49 | {:status status 50 | :headers headers 51 | :body (serialize body)})] 52 | (fn [req] 53 | (try 54 | (respond 200 (apply handler (concat [req] args))) 55 | (catch HttpException e 56 | (respond (.getStatus e) {:message (.getMessage e)})) 57 | (catch Exception e 58 | (error e "An error happened in the request handler") 59 | (respond 500 {:message (str "An error occured: " e)})))))) 60 | 61 | (defn- http-list-rules [req rules-loader] 62 | {:rules (loader/list-rules rules-loader)}) 63 | 64 | (defn- http-get-rule [req rules-loader] 65 | (let [p-ns (-> req :params :ns) 66 | rule (loader/get-rule rules-loader p-ns)] 67 | (if (nil? rule) 68 | (throw (HttpException. 404 (str "No such rule: " p-ns))) 69 | {:data rule}))) 70 | 71 | (defn read-expressions 72 | [reader] 73 | (binding [*read-eval* false] 74 | (loop [l []] 75 | (let [result (read reader false nil)] 76 | (if (nil? result) 77 | l 78 | (recur (conj l result))))))) 79 | 80 | (defn- index-add 81 | [index e] 82 | (conj index {:time (riemann.time/unix-time) :event e})) 83 | 84 | (defn eval-expressions 85 | [expressions events current-time] 86 | (let [temp-ns-name (gensym "lyceum-eval-ns___") 87 | current-ns (create-ns temp-ns-name) 88 | evaled-tasks (atom [])] 89 | (try 90 | (with-mocked-riemann-time evaled-tasks current-time 91 | (binding [*ns* current-ns] 92 | (doseq [expr expressions] 93 | (if (= (first expr) 'ns) 94 | (eval (cons 'ns (cons temp-ns-name (rest (rest expr))))) 95 | (eval expr))))) 96 | 97 | (let [reports (atom []) 98 | index (atom []) 99 | make-rules-fn (ns-resolve current-ns 'rules)] 100 | (when-not make-rules-fn 101 | (throw (Exception. "No 'rules' function in evaluated namespace."))) 102 | 103 | (let [index-fn (fn [e] (swap! index index-add e)) 104 | rules-fn (make-rules-fn {:index index-fn})] 105 | (when-not (and rules-fn (fn? rules-fn)) 106 | (throw (Exception. "Calling 'rules' did not return a stream function."))) 107 | 108 | (let [send-events (make-send-events-fn rules-fn)] 109 | (binding [*external-reports* reports] 110 | (send-events events)) 111 | {:reports @reports :index @index}))) 112 | 113 | (finally 114 | (remove-ns temp-ns-name))))) 115 | 116 | (defn- make-pushback-reader 117 | [data] 118 | (let [input-stream (ByteArrayInputStream. (.getBytes data)) 119 | input-stream-reader (InputStreamReader. input-stream)] 120 | (PushbackReader. input-stream-reader) )) 121 | 122 | (defn- http-eval-rule [req] 123 | (let [json-body-string (slurp (:body req)) 124 | json-body (or (json/parse-string json-body-string) {}) 125 | data (json-body "data" "") 126 | events (json-body "events" []) 127 | current-time (int (json-body "current-time" 0))] 128 | (when (or (nil? data) (empty? data)) 129 | (throw (HttpException. 400 "'data' must not be empty"))) 130 | (when (or (nil? events) (empty? events)) 131 | (throw (HttpException. 400 "'events' must not be empty"))) 132 | (let [pushback-reader (make-pushback-reader data)] 133 | (try 134 | (let [expressions (read-expressions pushback-reader)] 135 | (eval-expressions expressions events current-time)) 136 | (catch RuntimeException e 137 | (error e "Failed to handle request") 138 | (throw (HttpException. 400 (str "Invalid body: " e)))))))) 139 | 140 | 141 | (defn make-routes 142 | [c] 143 | (let [rules-loader (:rules-loader c)] 144 | 145 | (when (nil? (:rules-loader c)) 146 | (throw (Exception. "No rules-loader specified in lyceum configuration"))) 147 | 148 | (routes 149 | (GET "/rules" [] (handle-json http-list-rules rules-loader)) 150 | (GET "/rules/:ns" [] (handle-json http-get-rule rules-loader)) 151 | (POST "/eval" [] (handle-json http-eval-rule)) 152 | (not-found "not found")))) 153 | 154 | (defn http-server 155 | [listen-port site-routes] 156 | (info (str "Listening on port " listen-port)) 157 | (run-server (site site-routes) {:port listen-port})) 158 | 159 | (defn -main 160 | [& argv] 161 | (riemann.logging/init) 162 | (let [config-path (or (first argv) "lyceum.conf") 163 | c (config-file/setup config-path) 164 | listen-port (:listen-port c) 165 | site-routes (make-routes c)] 166 | (http-server listen-port site-routes))) 167 | -------------------------------------------------------------------------------- /src/lyceum/service/config_file.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.service.config-file 17 | (:require [clojure.tools.logging :refer [info error]]) 18 | (:require [lyceum.service.rules-loader.composite :as composite] 19 | [lyceum.service.rules-loader.github :as github] 20 | [lyceum.service.rules-loader.directory :as directory])) 21 | 22 | (def ^:dynamic *config*) 23 | 24 | (defn assoc-function 25 | [assoc-key] 26 | (fn [value] 27 | (swap! *config* assoc assoc-key value))) 28 | 29 | (def listen-port (assoc-function :listen-port)) 30 | (def rules-loader (assoc-function :rules-loader)) 31 | 32 | (defn rule-loaders 33 | [& loaders] 34 | (rules-loader (composite/setup loaders))) 35 | 36 | (defn github-rules 37 | [& opts] 38 | (github/setup (apply hash-map opts))) 39 | 40 | (defn directory-rules 41 | [& opts] 42 | (directory/setup (apply hash-map opts))) 43 | 44 | (defn setup 45 | [path] 46 | (binding [*config* (atom {:listen-port 8080}) 47 | *ns* (find-ns 'lyceum.service.config-file)] 48 | (load-file path) 49 | @*config*)) 50 | -------------------------------------------------------------------------------- /src/lyceum/service/rules_loader.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.service.rules-loader 17 | (:require [clojure.string :as string])) 18 | 19 | (defn path-to-id 20 | [prefix path] 21 | (let [r1 (.substring path (.length prefix) (- (.length path) 4)) 22 | r2 (string/split r1 #"/") 23 | r3 (string/join "." r2)] 24 | (string/replace r3 #"_" "-"))) 25 | 26 | (defn id-to-path 27 | [prefix id] 28 | (let [r1 (string/replace id #"-" "_") 29 | r2 (string/split r1 #"\.") 30 | r3 (string/join "/" r2)] 31 | (str prefix r3 ".clj"))) 32 | 33 | (defprotocol RulesLoader 34 | (list-rules [this] "List all available rules, should return a list of string.") 35 | (get-rule [this id] "Get the specified set of rules.")) 36 | -------------------------------------------------------------------------------- /src/lyceum/service/rules_loader/composite.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.service.rules-loader.composite 17 | (:require [clojure.tools.logging :refer [info error]]) 18 | (:require [lyceum.service.rules-loader :refer :all])) 19 | 20 | (defn real-get-rule 21 | [[loader & loaders] id] 22 | (if (nil? loader) 23 | nil 24 | (let [data (get-rule loader id)] 25 | (if-not (nil? data) 26 | data 27 | (real-get-rule loaders id))))) 28 | 29 | (defrecord CompositeRulesLoader [loaders] 30 | RulesLoader 31 | (list-rules [this] 32 | (mapcat list-rules loaders)) 33 | (get-rule [this id] 34 | (real-get-rule loaders id))) 35 | 36 | (defn setup 37 | "Build a rules loader that composes multiple loaders. 38 | 39 | Returns a `rules-loader` based of this." 40 | [loaders] 41 | (CompositeRulesLoader. loaders)) 42 | -------------------------------------------------------------------------------- /src/lyceum/service/rules_loader/directory.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.service.rules-loader.directory 17 | (:require [clojure.tools.logging :refer [info error]]) 18 | (:require [lyceum.service.rules-loader :refer :all] 19 | [clojure.string :as string])) 20 | 21 | (defn- node-type 22 | [node] 23 | (let [file-name (.getName node)] 24 | (if (and (.isFile node) 25 | (.endsWith file-name ".clj") 26 | (not (.startsWith file-name "."))) 27 | :file 28 | (if (and (.isDirectory node) 29 | (not (.startsWith file-name "."))) 30 | :directory 31 | nil)))) 32 | 33 | (defn- real-list-rules 34 | [directory id-prefix] 35 | (let [make-nodes (fn [nodes path] (map (fn [c] [c path]) nodes)) 36 | dir (clojure.java.io/file directory)] 37 | (loop [nodes (make-nodes (.listFiles dir) []) 38 | result []] 39 | (if (empty? nodes) 40 | result 41 | (let [[node path] (first nodes) 42 | node-name (.getName node)] 43 | (case (node-type node) 44 | :file 45 | (let [id (path-to-id "" (string/join "/" (conj path node-name))) 46 | next-nodes (rest nodes)] 47 | (if (or (nil? id-prefix) 48 | (.startsWith id id-prefix)) 49 | (recur next-nodes (conj result id)) 50 | (recur next-nodes result))) 51 | :directory 52 | (let [next-nodes 53 | (make-nodes (.listFiles node) (conj path node-name))] 54 | (recur (concat (rest nodes) next-nodes) result)) 55 | nil 56 | (recur (rest nodes) result))))))) 57 | 58 | (defn- real-get-rule 59 | [directory id] 60 | (let [path (id-to-path directory id)] 61 | (let [f (clojure.java.io/file path)] 62 | (if (.isFile f) 63 | (slurp path) 64 | nil)))) 65 | 66 | (defrecord DirectoryRulesLoader [path id-prefix] 67 | RulesLoader 68 | (list-rules [this] 69 | (real-list-rules path id-prefix)) 70 | (get-rule [this id] 71 | (real-get-rule path id))) 72 | 73 | (defn setup 74 | "Will iterate through `path` assuming that that is the root namespace of the 75 | rules and load any *.clj files found, transforming their names appropriately. 76 | 77 | Returns a `rules-loader` based of this." 78 | [{:keys [path id-prefix]}] 79 | (DirectoryRulesLoader. path id-prefix)) 80 | -------------------------------------------------------------------------------- /src/lyceum/service/rules_loader/github.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | (ns lyceum.service.rules-loader.github 17 | (:require [clojure.tools.logging :refer [info error]]) 18 | (:require [lyceum.service.rules-loader :refer :all] 19 | [org.httpkit.client :as http-kit] 20 | [cheshire.core :as json] 21 | [clojure.string :as string] 22 | [base64-clj.core :as b64])) 23 | 24 | (defn- http-request-get 25 | [url http-options] 26 | (info (str "GET: " url)) 27 | (let [request (http-kit/get url http-options) 28 | result @request] 29 | (when (= (result :status) 200) 30 | (json/parse-string (result :body))))) 31 | 32 | (defprotocol GithubClientProtocol 33 | (get-contents [this path ref]) 34 | (get-git-refs [this ref]) 35 | (get-git-trees-rec [this sha])) 36 | 37 | (defrecord GithubClient [http-options base-url] 38 | GithubClientProtocol 39 | (get-contents [this path ref] 40 | (http-request-get 41 | (str base-url "/contents/" path "?ref=" ref) 42 | http-options)) 43 | (get-git-refs [this ref] 44 | (http-request-get 45 | (str base-url "/git/refs/" ref) 46 | http-options)) 47 | (get-git-trees-rec [this sha] 48 | (http-request-get 49 | (str base-url "/git/trees/" sha "?recursive=1") 50 | http-options))) 51 | 52 | (defn- github-list-rules 53 | [client branch prefix] 54 | (let [] 55 | (when-let [body (get-git-refs client (str "heads/" branch))] 56 | (let [object-sha ((body "object") "sha") 57 | result (get-git-trees-rec client object-sha)] 58 | (for [entry (result "tree") 59 | :let [path (entry "path")] 60 | :when (and (.startsWith path prefix) 61 | (.endsWith path ".clj"))] 62 | (path-to-id prefix path)))))) 63 | 64 | (defn- real-list-rules 65 | [client branch prefix id-prefix] 66 | (for [id (github-list-rules client branch prefix) 67 | :when (or (nil? id-prefix) 68 | (.startsWith id id-prefix))] 69 | id)) 70 | 71 | (defn- real-get-rule 72 | [client branch prefix id] 73 | (let [path (id-to-path prefix id) 74 | body (get-contents client path branch) 75 | split-content (body "content") 76 | content (string/replace split-content #"\n" "")] 77 | (b64/decode content "UTF-8"))) 78 | 79 | (defrecord GithubRulesLoader [client branch prefix id-prefix] 80 | RulesLoader 81 | (list-rules [this] 82 | (real-list-rules client branch prefix id-prefix)) 83 | (get-rule [this id] 84 | (real-get-rule client branch prefix id))) 85 | 86 | (defn setup 87 | [{:keys [url branch prefix repo id-prefix http-options] 88 | :or {url "https://api.github.com" 89 | branch "master" 90 | prefix "test/" 91 | http-options {}}}] 92 | {:pre [(not (nil? repo))]} 93 | (let [client (GithubClient. http-options (str url "/repos/" repo))] 94 | (GithubRulesLoader. client branch prefix id-prefix))) 95 | -------------------------------------------------------------------------------- /src/lyceum/test.clj: -------------------------------------------------------------------------------- 1 | ;; $LICENSE 2 | ;; Copyright 2013-2014 Spotify AB. All rights reserved. 3 | ;; 4 | ;; The contents of this file are licensed under the Apache License, Version 2.0 5 | ;; (the "License"); you may not use this file except in compliance with the 6 | ;; License. You may obtain a copy of the License at 7 | ;; 8 | ;; http://www.apache.org/licenses/LICENSE-2.0 9 | ;; 10 | ;; Unless required by applicable law or agreed to in writing, software 11 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | ;; License for the specific language governing permissions and limitations under 14 | ;; the License. 15 | 16 | ;;

Lyceum Test Framework

17 | ;; 18 | ;; This namespace contains function designed to put lyceum rules under test. 19 | ;; 20 | ;; It contains [utility functions](#utility-functions) to simulate riemann 21 | ;; operations as fast as your CPU can handle it without spinning up a core. 22 | ;; 23 | ;; You are probably looking for [`with-rules`](#with-rules), which is the 24 | ;; helper macro used when writing test-cases. 25 | ;; 26 | ;; It is typically used like the following. 27 | ;; 28 | ;;
 29 | ;; (ns my-awesome.namespace-test
 30 | ;;   (require [lyceum.test :refer :all]))
 31 | ;;
 32 | ;; (deftest my-awesome-test
 33 | ;;   (with-rules 'my-awesome.namespace
 34 | ;;     (send-events {:time 1000 :host :foo :state "critical"})
 35 | ;;     (check-externals
 36 | ;;       :email {:state "critical"})))
 37 | ;; 
38 | (ns lyceum.test 39 | (:require clojure.test) 40 | (:require 41 | [lyceum.core :refer [rules-for-ns]] 42 | [lyceum.mock :refer [with-mocks let-mocks make-mock-fn]] 43 | [lyceum.external :refer [*external-reports* report-external]]) 44 | (:require 45 | lyceum.external) 46 | (:require 47 | riemann.common 48 | riemann.logging)) 49 | 50 | ;; Maintains a list of all events sent into the rules 51 | ;; function under test. 52 | (def ^:dynamic *test-events*) 53 | 54 | (defn check-bound-with-rules 55 | "Builds a function that fails with an 56 | assertion error unless [`*test-events*`](#*test-events*) has been bound" 57 | [fn-name] 58 | (fn [& args] 59 | (assert (bound? #'*test-events*) 60 | (str "Function must be inside a with-rules block: " fn-name)))) 61 | 62 | ;;

Unbound Functions

63 | ;; 64 | ;; These functions are bound to a placeholder through 65 | ;; [`check-bound-with-rules`](#check-bound-with-rules) until a real 66 | ;; implementation is provided by the [`with-rules`](#with-rules) macro. 67 | 68 | ;; The unbound implementation of 69 | ;; `send-event-fn` which is built by 70 | ;; [`make-send-event-fn`](#make-send-event-fn) 71 | (def ^:dynamic send-event (check-bound-with-rules 'send-event)) 72 | 73 | ;; The unbound implementation of 74 | ;; `send-events-fn` which is built by 75 | ;; [`make-send-events-fn`](#make-send-events-fn) 76 | (def ^:dynamic send-events (check-bound-with-rules 'send-events)) 77 | 78 | ;; The unbound implementation of 79 | ;; [`nth-event-fn`](#nth-event-fn) 80 | (def ^:dynamic nth-event (check-bound-with-rules 'nth-event)) 81 | 82 | ;; The unbound implementation of 83 | ;; [`check-externals-fn`](#check-externals-fn) 84 | (def ^:dynamic check-externals (check-bound-with-rules 'check-externals)) 85 | 86 | ;; A helper variable that is bound by 87 | ;; [`rule-fixture`](#rule-fixture) for simplifying common test fixtures. 88 | (def ^:dynamic index) 89 | 90 | ;;

Utility Functions

91 | 92 | (defn- make-event-maps-filter 93 | "Makes the function that converts a map 94 | (`event-map`) into a riemann event. 95 | 96 | Takes a `config` parameter which is what was passed to 97 | [`send-events`](#send-events) and family functions. 98 | 99 | All keys will be converted to keywords" 100 | [{:keys [event-base]}] 101 | (fn [event-map] 102 | (riemann.common/event 103 | (into 104 | {:time 0} 105 | (for [[k v] (merge event-base event-map)] 106 | [(keyword k) v]))))) 107 | 108 | (defmacro with-mocked-riemann-time 109 | "Mock all important riemann time functions 110 | for predictive operations." 111 | [tasks now & body] 112 | `(with-redefs [riemann.time/next-tick (fn [a# dt#] (+ ~now dt#)) 113 | riemann.time/unix-time (fn [] ~now) 114 | riemann.time/schedule! 115 | (fn [task#] 116 | (swap! ~tasks conj task#) 117 | task#)] 118 | ~@body)) 119 | 120 | (defn- sort-tasks 121 | [tasks] 122 | (sort (fn [a b] (compare (:t a) (:t b))) tasks)) 123 | 124 | (defn- run-task 125 | [task task-time current-tasks] 126 | (let [new-tasks (atom []) 127 | i-task-time (int task-time)] 128 | (with-mocked-riemann-time new-tasks i-task-time 129 | (riemann.time/run task)) 130 | (let [next-task (riemann.time/succ task) 131 | rest-tasks (rest current-tasks) 132 | tail-tasks (if (nil? next-task) 133 | rest-tasks 134 | (conj rest-tasks next-task))] 135 | (sort-tasks (concat @new-tasks tail-tasks))))) 136 | 137 | (defn run-tasks 138 | "Recursively execute all viable tasks." 139 | [now tasks] 140 | (loop [current-tasks (sort-tasks tasks)] 141 | (if (empty? current-tasks) 142 | current-tasks 143 | (let [task (first current-tasks) 144 | task-time (:t task)] 145 | (if (> task-time now) 146 | current-tasks 147 | (recur (run-task task task-time current-tasks))))))) 148 | 149 | (defn- compare-events 150 | "Compare events by time first, then by index to make sure sorting is 151 | consistent." 152 | [[i-a a] [i-b b]] 153 | (compare [(:time a) i-a] [(:time b) i-b])) 154 | 155 | (defn send-events-fn 156 | "Function to simulate riemann operation. 157 | Will submit events to the specified rules function and run any tasks 158 | which pop up at the appropriate time." 159 | ([config rules-fn event-maps initial-tasks] 160 | (let [set-defaults (partial merge {:host :default-host :service :default-service}) 161 | event-filter (make-event-maps-filter config) 162 | flat-event-maps (flatten event-maps) 163 | events (into [] (map event-filter flat-event-maps)) 164 | indexed-events (map-indexed list events) 165 | events (map second (sort compare-events indexed-events)) 166 | tasks (atom initial-tasks)] 167 | 168 | ; Send in the sequence of events, and run tasks required accordingly. 169 | ; We have to run tasks both before and after the rule under test. 170 | ; * Tasks can have been pre-emptively scheduled before the rule and be 171 | ; viable for run at `now`. 172 | ; * The rule function can schedule tasks. 173 | (doseq [e (map set-defaults events) :let [now (:time e)]] 174 | (reset! tasks (run-tasks now @tasks)) 175 | (with-mocked-riemann-time tasks now 176 | (rules-fn e)) 177 | (reset! tasks (run-tasks now @tasks))) 178 | 179 | ; If :end-time specified, run the current tasks up until specified time. 180 | (when-let [end-time (:end-time config)] 181 | (run-tasks end-time @tasks)) 182 | 183 | (when (bound? #'*test-events*) 184 | (reset! *test-events* events)))) 185 | ([config rules-fn event-maps] 186 | (send-events-fn config rules-fn event-maps []))) 187 | 188 | 189 | (defn make-send-events-fn 190 | "Build the send-events-fn used by the 191 | [unbound function](#unbound-functions) [`send-events`](#send-events) 192 | 193 | This is essentially a wrapper for [`send-events-fn`](#send-events-fn) 194 | " 195 | [rules-fn] 196 | (fn [& args] 197 | {:pre [(> (count args) 0)]} 198 | (let [two-args? (> (count args) 1) 199 | config (if two-args? (first args) {}) 200 | event-maps (if two-args? (rest args) (first args))] 201 | (send-events-fn config rules-fn event-maps)))) 202 | 203 | 204 | (defn make-send-event-fn 205 | "Build the send-event-fn used by the 206 | [unbound function](#unbound-functions) [`send-event`](#send-event) 207 | 208 | This is essentially a wrapper for [`send-events-fn`](#send-events-fn)." 209 | [rules-fn] 210 | (fn [& event-maps] 211 | (send-events-fn {} rules-fn event-maps))) 212 | 213 | 214 | (defn external-reporter 215 | "Function used to generate one external report 216 | (`report`) through `lyceum.external/report-external`" 217 | [report] 218 | (let [external-name (:external report) 219 | message (:message report)] 220 | (clojure.test/report 221 | {:type :fail 222 | :message (str "This should not happen: " external-name ": " message 223 | " - did you forget to mock streams or check the external " 224 | "calls (check-externals) in your test case?")}))) 225 | 226 | 227 | (defn nth-event-fn 228 | "Return event number `n` that has been processed from 229 | [`*test-events*`](#*test-events*)" 230 | [n] 231 | (get @*test-events* n)) 232 | 233 | 234 | (defn- match-map 235 | "Recursively check that all items in `ref-val` equals to 236 | the ones in `actual-val`. 237 | 238 | Keys existing in `actual-val` but missing in `ref-val` will be ignored. 239 | 240 | Ex: `(match-map {:foo :bar} {:foo :bar :baz :biz})` would be `true`, even 241 | though `:baz` only exists in `actual-val`." 242 | [ref-val actual-val] 243 | (if (map? ref-val) 244 | (every? 245 | true? 246 | (for [[k v] (seq ref-val)] 247 | (match-map v (k actual-val)))) 248 | (= ref-val actual-val))) 249 | 250 | 251 | (defn- match-actual 252 | "Generate a readable reference of a map that did 253 | not match. 254 | 255 | Create a readable representation of what the actual match looked like 256 | compared to the reference object." 257 | [ref-val actual-val] 258 | (if (and actual-val (map? ref-val)) 259 | (apply 260 | hash-map 261 | (flatten 262 | (for [[k v] (seq ref-val)] 263 | [k (match-actual v (k actual-val))]))) 264 | actual-val)) 265 | 266 | 267 | (defn check-externals-fn 268 | "The real implementation of 269 | [`check-externals`](#check-externals) 270 | 271 | Check that all externals that is specified in `call-defs` has been called 272 | in the order specified. 273 | 274 | This is a flexible assertion method that uses the specified calls 275 | (`call-defs`) as template to the actual calls being made, and will report 276 | when-ever there is a mis-match through `clojure.test/report` 277 | 278 | The structure of the external call is matched using 279 | [`match-map`](#match-map). 280 | 281 | Resets `*external-reports*` after each call." 282 | [& call-defs] 283 | {:pre [(even? (count call-defs))]} 284 | (let [reports @*external-reports* 285 | calls (partition 2 call-defs) 286 | indexed-reports (map-indexed vector reports) 287 | count-matches? (= (count calls) (count reports))] 288 | 289 | (clojure.test/report 290 | {:type (if count-matches? :pass :fail) 291 | :message (str "Amount of external calls should match") 292 | :expected (count calls) 293 | :actual (count reports)}) 294 | 295 | (doseq [[i [external-actual report]] indexed-reports] 296 | (let [[external-ref value-ref] (nth calls i [nil, nil])] 297 | (let [external-matches? (= external-ref external-actual) 298 | value-matches? (match-map value-ref report) 299 | all-matches? (and external-matches? value-matches?) 300 | report-message (if (nil? external-ref) 301 | (str "Did not expect external call #" (inc i)) 302 | (str "Expected external call #" (inc i))) 303 | report-type (if all-matches? 304 | :pass 305 | :fail) 306 | reduced-actual (match-actual value-ref report) 307 | print-reduced? (and (not (nil? value-ref)) 308 | (not (empty? reduced-actual))) 309 | value-actual (if print-reduced? reduced-actual report)] 310 | 311 | (clojure.test/report 312 | {:type report-type 313 | :message report-message 314 | :expected (list external-ref value-ref) 315 | :actual (list external-actual value-actual)})))) 316 | 317 | (reset! *external-reports* []))) 318 | 319 | ;;

Public Functions

320 | 321 | (defmacro with-test-bindings 322 | [rules-fn & body] 323 | `(binding [*test-events* (atom []) 324 | *external-reports* (atom []) 325 | send-event (make-send-event-fn ~rules-fn) 326 | send-events (make-send-events-fn ~rules-fn) 327 | nth-event nth-event-fn 328 | check-externals check-externals-fn] 329 | ~@body 330 | (report-external external-reporter))) 331 | 332 | (defmacro with-rules 333 | "Helper macro to setup test framework 334 | 335 | __This is probably what you are looking for.__ 336 | 337 | Will bind all [unbound functions](#unbound-functions) with options (`opts`) 338 | and load the namespace `under-test` and evalute `body` in this scope." 339 | [opts under-test & body] 340 | {:pre [(not (nil? opts)) 341 | (symbol? under-test)]} 342 | `(do 343 | (require '~under-test :reload) 344 | 345 | (let [rules-fn# (rules-for-ns ~opts '~under-test)] 346 | (when (nil? rules-fn#) 347 | (throw (Exception. (str "Could not load rules for: " '~under-test)))) 348 | (riemann.logging/init) 349 | (with-test-bindings rules-fn# 350 | ~@body) 351 | (remove-ns '~under-test)))) 352 | 353 | (defmacro rule-fixture 354 | "A helper function to setup a clojure.test fixture for 355 | a namespace `under-test`. 356 | 357 | Initializes a rule namespace with a bound and mocked [index](#index)." 358 | [under-test] 359 | `(fn [f#] 360 | (with-mocks 361 | (let-mocks [(index#)] 362 | (with-rules {:index index#} ~under-test 363 | (binding [index index#] 364 | (f#))))))) 365 | 366 | (defmacro with-rule-fn 367 | "A helper function to setup lyceum testing for a 368 | immediate `rule-fn` rules function." 369 | [rule-fn & body] 370 | `(with-test-bindings ~rule-fn 371 | ~@body)) 372 | -------------------------------------------------------------------------------- /templates/rule.clj: -------------------------------------------------------------------------------- 1 | (ns __NS__ 2 | (:require [lyceum.core :refer :all])) 3 | 4 | (require-depends) 5 | 6 | (def alert 7 | (email {:from "riemann@example.com"} ["me@example.com"])) 8 | 9 | ;; STEP 4: Add rules here! 10 | (def-rules 11 | (where (tagged-any ["role::example"]) 12 | (changed-state {:init "ok"} 13 | alert))) 14 | -------------------------------------------------------------------------------- /templates/test.clj: -------------------------------------------------------------------------------- 1 | (ns __NS__-test 2 | (:require [clojure.test :refer :all] 3 | [lyceum.mock :refer :all] 4 | [lyceum.test :refer :all])) 5 | 6 | (use-fixtures :each (rule-fixture __NS__)) 7 | 8 | (def test-tags ["role::example"]) 9 | 10 | (deftest test-default-streams 11 | (testing "Monitoring hooks should send a single e-mail when toggling back and forth between 'critical' and 'ok'" 12 | (send-events 13 | ; stop the simulation at 100 seconds. 14 | ; use an event base to add the specified tags to all events. 15 | {:end-time 100 :event-base {:tags test-tags}} 16 | ; send a critical event at second 0. 17 | [{:time 0 :state "ok"} 18 | {:time 10 :state "critical"} 19 | {:time 20 :state "critical"} 20 | {:time 30 :state "ok"}]) 21 | ; verify externals. 22 | (check-externals 23 | :email {:event {:state "critical" :time 10}} 24 | :email {:event {:state "ok" :time 30}}))) 25 | -------------------------------------------------------------------------------- /test/lyceum/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns lyceum.core-test 2 | (:require [clojure.test :refer :all] 3 | [lyceum.core :refer :all] 4 | [lyceum.mock :refer :all])) 5 | 6 | (deftest test-rules-for-all 7 | (with-mocks 8 | (binding-mocks 9 | [(rules-for-ns (list :rules-for-ns-return))] 10 | (rules-for-all :opts [:foo :bar]) 11 | (is-called rules-for-ns (list :opts :foo) (list :opts :bar))))) 12 | 13 | (deftest test-rules-for-one 14 | (with-mocks 15 | (let-mocks 16 | [(ns-resolve-ret :ns-resolve-ret)] 17 | (binding-mocks 18 | [(find-ns :find-ns) (ns-resolve ns-resolve-ret)] 19 | (is (= :ns-resolve-ret (rules-for-ns :opts :foo))) 20 | (is-called find-ns (list :foo)) 21 | (is-called ns-resolve (list :find-ns 'rules)) 22 | (is-called ns-resolve-ret (list :opts)))))) 23 | -------------------------------------------------------------------------------- /test/lyceum/mock_test.clj: -------------------------------------------------------------------------------- 1 | (ns lyceum.mock-test 2 | (:require [clojure.test :refer :all]) 3 | (:require [lyceum.mock :refer :all])) 4 | 5 | (deftest test-with-mocks 6 | (testing "Should rebind *mocks-db* to a map" 7 | (binding [*mocks-db* (atom :incorrect)] 8 | (with-mocks 9 | (is (= {} @*mocks-db*)))))) 10 | 11 | (deftest test-make-mock-fn 12 | (testing "Should support multiple calls" 13 | (binding [*mocks-db* (atom {})] 14 | (let [mock-fn (make-mock-fn :callee-fn-name :make-mock-fn-return)] 15 | (is (= :make-mock-fn-return (mock-fn :arg-a1 :arg-a2)) 16 | "Mock function should return specified value") 17 | (is (= :make-mock-fn-return (mock-fn :arg-b1 :arg-b2)) 18 | "Mock function should return specified value") 19 | (is (= {mock-fn '((:arg-a1 :arg-a2) (:arg-b1 :arg-b2))} @*mocks-db*) 20 | "Mock function call should have been recorded"))))) 21 | 22 | (deftest test-make-mocks 23 | (is (= '(:func1 (make-mock-fn :callee-fn-name :ret1) 24 | :func1 (make-mock-fn :callee-fn-name :ret2)) 25 | (make-mocks :callee-fn-name ['(:func1 :ret1) '(:func1 :ret2)])))) 26 | 27 | (deftest test-binding-mocks 28 | (with-redefs [make-mocks (fn [& _] '((foo :foo-fn)))] 29 | (let [f (macroexpand-1 `(binding-mocks [(fn-name :ret-value)]))] 30 | (is (= `(with-redefs [(foo :foo-fn)])) f)))) 31 | 32 | (deftest test-let-mocks 33 | (with-redefs [make-mocks (fn [& _] '((foo :foo-fn)))] 34 | (let [f (macroexpand-1 `(let-mocks [(fn-name :ret-value)]))] 35 | (is (= `(let [(foo :foo-fn)])) f)))) 36 | -------------------------------------------------------------------------------- /test/lyceum/service/rules_loader/composite_test.clj: -------------------------------------------------------------------------------- 1 | (ns lyceum.service.rules-loader.composite-test 2 | (:require [lyceum.service.rules-loader :refer :all]) 3 | (:require [lyceum.service.rules-loader.composite :refer :all]) 4 | (:require [clojure.test :refer :all])) 5 | 6 | (defn fake 7 | [ret nss] 8 | (reify 9 | RulesLoader 10 | (list-rules [this] nss) 11 | (get-rule [this id] ret))) 12 | 13 | (deftest test-recursive-loader 14 | (is (= :second (real-get-rule [(fake nil []) (fake :second [])] :x))) 15 | (is (= :only (real-get-rule [(fake :only [])] :x))) 16 | (is (= nil (real-get-rule [] :x)))) 17 | -------------------------------------------------------------------------------- /test/lyceum/test_test.clj: -------------------------------------------------------------------------------- 1 | (ns lyceum.test-test 2 | (:require [clojure.test :refer :all]) 3 | (:require [lyceum.test :refer :all])) 4 | -------------------------------------------------------------------------------- /tools/license: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check that all files has a license header. 3 | # 4 | # Modifies the file to have the specified license header if it does not exist. 5 | 6 | LICENSE_PLACEHOLDER=";; \$LICENSE" 7 | 8 | license_header() { 9 | cat < /dev/null; then 49 | echo "No license header found in file: $file" 50 | 51 | if [[ $verify_only -ne 1 ]]; then 52 | ( 53 | echo "$libdir: $file: Missing license header, could not find placeholder '$LICENSE_PLACEHOLDER'" 54 | echo "Note: If you are the contributor of this file, make sure to add a license header that suits you!" 55 | echo " You can apply the default license by running:" 56 | echo 57 | echo " #> tools/license" 58 | echo 59 | ) 1>&2 60 | exit_status=1 61 | continue 62 | fi 63 | 64 | if [[ $force -ne 1 ]]; then 65 | read -p "Apply default (Spotify) license? [Y/n]: " q 66 | else 67 | echo "Changed forced (-f)!" 68 | q="Y" 69 | fi 70 | 71 | if [[ $q == "Y" ]]; then 72 | ( 73 | license_header 74 | cat $file 75 | ) > $license_file 76 | 77 | mv $license_file $file 78 | echo "$libdir: $file: modified" 79 | else 80 | echo "$libdir: $file: no action" 81 | fi 82 | fi 83 | done 4< <(find $libdir -type f -name '*.clj') 84 | done 3< <(find -name vendor -prune -o -type d -name src -print) 85 | 86 | while read -u 3 gemspec; do 87 | dir=$(dirname $gemspec) 88 | license_path=$dir/LICENSE 89 | if [[ ! -f $license_path ]]; then 90 | echo "Missing LICENSE for project: $gemspec (expected $license_path)" 91 | exit_status=1 92 | fi 93 | done 3< <(find -name vendor -prune -o -type f -name '*.gemspec' -print) 94 | 95 | exit $exit_status 96 | --------------------------------------------------------------------------------