├── .lein-classpath ├── test ├── data │ └── tls │ │ ├── demoCA │ │ ├── crlnumber │ │ ├── serial │ │ ├── serial.old │ │ ├── index.txt.attr │ │ ├── index.txt.attr.old │ │ ├── index.txt.old │ │ ├── index.txt │ │ ├── careq.pem │ │ ├── private │ │ │ └── cakey.pem │ │ ├── cacert.pem │ │ └── newcerts │ │ │ └── 94FD3EFB7A3D153E.pem │ │ ├── client.pkcs8 │ │ └── server.pkcs8 └── riemann │ ├── test_utils.clj │ ├── campfire_test.clj │ ├── pushover_test.clj │ ├── nagios_test.clj │ ├── kairosdb_test.clj │ ├── opentsdb_test.clj │ ├── keenio_test.clj │ ├── opsgenie_test.clj │ ├── user_test.clj │ ├── instrumentation_test.clj │ ├── datadog_test.clj │ ├── stackdriver_test.clj │ ├── test_test.clj │ ├── time │ └── controlled_test.clj │ ├── cloudwatch_test.clj │ ├── blueflood_test.clj │ ├── client_test.clj │ ├── email_test.clj │ ├── hipchat_test.clj │ ├── pubsub_test.clj │ ├── xymon_test.clj │ ├── shinken_test.clj │ ├── logentries_test.clj │ ├── twilio_test.clj │ ├── mailgun_test.clj │ ├── transport │ ├── opentsdb_test.clj │ └── graphite_test.clj │ ├── influxdb_test.clj │ ├── graphite_test.clj │ ├── time_test.clj │ ├── common_test.clj │ └── slack_test.clj ├── resources ├── log4j.properties └── query.g4 ├── pkg ├── deb │ ├── prerm.sh │ ├── postrm.sh │ ├── preinst.sh │ ├── postinst.sh │ ├── riemann.config │ ├── riemann │ └── init.sh ├── riemann-default ├── tar │ ├── riemann.config │ └── riemann └── rpm │ ├── riemann.config │ ├── riemann │ └── init.sh ├── .travis.yml ├── README.markdown ├── .gitignore ├── tasks └── leiningen │ ├── pkg.clj │ └── tar.clj ├── riemann.config ├── src └── riemann │ ├── pushover.clj │ ├── keenio.clj │ ├── repl.clj │ ├── campfire.clj │ ├── nagios.clj │ ├── shinken.clj │ ├── cloudwatch.clj │ ├── datadog.clj │ ├── stackdriver.clj │ ├── pagerduty.clj │ ├── time │ └── controlled.clj │ ├── opsgenie.clj │ ├── xymon.clj │ ├── deps.clj │ ├── hipchat.clj │ ├── bin.clj │ ├── instrumentation.clj │ ├── blueflood.clj │ ├── plugin.clj │ ├── twilio.clj │ ├── email.clj │ ├── logentries.clj │ ├── mailgun.clj │ ├── transport │ └── debug.clj │ ├── pubsub.clj │ ├── logstash.clj │ ├── boundary.clj │ ├── index.clj │ ├── pool.clj │ ├── kairosdb.clj │ ├── opentsdb.clj │ ├── librato.clj │ └── slack.clj └── project.clj /.lein-classpath: -------------------------------------------------------------------------------- 1 | :tasks 2 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/crlnumber: -------------------------------------------------------------------------------- 1 | 01 2 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/serial: -------------------------------------------------------------------------------- 1 | 94FD3EFB7A3D1541 2 | -------------------------------------------------------------------------------- /resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO 2 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/serial.old: -------------------------------------------------------------------------------- 1 | 94FD3EFB7A3D1540 2 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/index.txt.attr: -------------------------------------------------------------------------------- 1 | unique_subject = yes 2 | -------------------------------------------------------------------------------- /pkg/deb/prerm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | invoke-rc.d riemann stop 4 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/index.txt.attr.old: -------------------------------------------------------------------------------- 1 | unique_subject = yes 2 | -------------------------------------------------------------------------------- /pkg/deb/postrm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ "$1" = "purge" ] ; then 4 | update-rc.d riemann remove >/dev/null 5 | fi 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/index.txt.old: -------------------------------------------------------------------------------- 1 | V 22890427214119Z 94FD3EFB7A3D153E unknown /C=US/ST=CA/O=Puppy Bureaucracy/OU=Distributed Systems Department/CN=test.riemann.io 2 | V 22890427215023Z 94FD3EFB7A3D153F unknown /C=US/ST=CA/L=San Francisco/O=Puppy Bureaucracy/OU=Riemann Testing Dept/CN=arf.riemann.io 3 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Riemann monitors distributed systems. 2 | 3 | Riemann aggregates events from your servers and applications with a powerful stream processing language. 4 | 5 | Find out more at [http://riemann.io](http://riemann.io) 6 | 7 | === 8 | 9 | [![Build Status](https://travis-ci.org/aphyr/riemann.png)](https://travis-ci.org/aphyr/riemann) 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cake 2 | .attach_pid* 3 | pom.xml 4 | pom.xml.asc 5 | *.jar 6 | *.tar 7 | *.tar.bz2 8 | *.war 9 | *.deb 10 | *~ 11 | .*.swp 12 | *.log 13 | .lein-repl-history 14 | .nrepl-port 15 | lib 16 | classes 17 | build 18 | .lein-deps-sum 19 | .lein-failures 20 | protosrc/ 21 | reimann-*.zip 22 | /site 23 | site/** 24 | bench/** 25 | target/** 26 | /target/ 27 | -------------------------------------------------------------------------------- /pkg/deb/preinst.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # Create riemann user and group 3 | USERNAME="riemann" 4 | GROUPNAME="riemann" 5 | getent group "$GROUPNAME" >/dev/null || groupadd -r "$GROUPNAME" 6 | getent passwd "$USERNAME" >/dev/null || \ 7 | useradd -r -g "$GROUPNAME" -d /usr/share/riemann -s /bin/false \ 8 | -c "Riemann monitoring system" "$USERNAME" 9 | exit 0 10 | -------------------------------------------------------------------------------- /tasks/leiningen/pkg.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.pkg 2 | (:use [leiningen.uberjar :only [uberjar]] 3 | [leiningen.fatdeb :only [fatdeb]] 4 | [leiningen.fatrpm :only [fatrpm]] 5 | [leiningen.tar :only [tar]])) 6 | 7 | (defn pkg [project] 8 | (doto project 9 | (uberjar) 10 | (tar false) 11 | (fatrpm false) 12 | (fatdeb false))) 13 | -------------------------------------------------------------------------------- /pkg/riemann-default: -------------------------------------------------------------------------------- 1 | # Optionally add classes to the classpath for additional functionality 2 | # EXTRA_CLASSPATH= 3 | 4 | # Optional JAVA_OPTS 5 | # EXTRA_JAVA_OPTS= 6 | 7 | # Alternative path to config directory 8 | # RIEMANN_PATH_CONF= 9 | 10 | # Alternative path to config file 11 | # RIEMANN_CONFIG= 12 | 13 | # Additional options to pass to riemann on startup 14 | # RIEMANN_OPTS= 15 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/index.txt: -------------------------------------------------------------------------------- 1 | V 22890427214119Z 94FD3EFB7A3D153E unknown /C=US/ST=CA/O=Puppy Bureaucracy/OU=Distributed Systems Department/CN=test.riemann.io 2 | V 22890427215023Z 94FD3EFB7A3D153F unknown /C=US/ST=CA/L=San Francisco/O=Puppy Bureaucracy/OU=Riemann Testing Dept/CN=arf.riemann.io 3 | V 22890427215130Z 94FD3EFB7A3D1540 unknown /C=US/ST=CA/L=San Francisco/O=Puppy Bureaucracy/OU=Riemann Clients Dept/CN=bark.riemann.io 4 | -------------------------------------------------------------------------------- /pkg/deb/postinst.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # Fakeroot and lein don't get along, so we set ownership after the fact. 3 | chown -R root:root /usr/share/riemann 4 | chown root:root /usr/bin/riemann 5 | chown riemann:riemann /var/log/riemann 6 | chown -R riemann:riemann /etc/riemann 7 | chown root:root /etc/init.d/riemann 8 | chown root:root /etc/default/riemann 9 | 10 | # Start riemann on boot 11 | if [ -x "/etc/init.d/riemann" ]; then 12 | if [ ! -e "/etc/init/riemann.conf" ]; then 13 | update-rc.d riemann defaults >/dev/null 14 | fi 15 | fi 16 | 17 | invoke-rc.d riemann start 18 | -------------------------------------------------------------------------------- /pkg/tar/riemann.config: -------------------------------------------------------------------------------- 1 | ; -*- mode: clojure; -*- 2 | ; vim: filetype=clojure 3 | 4 | (logging/init {:file "riemann.log"}) 5 | 6 | ; Listen on the local interface over TCP (5555), UDP (5555), and websockets 7 | ; (5556) 8 | (let [host "127.0.0.1"] 9 | (tcp-server {:host host}) 10 | (udp-server {:host host}) 11 | (ws-server {:host host})) 12 | 13 | ; Expire old events from the index every 5 seconds. 14 | (periodically-expire 5) 15 | 16 | (let [index (index)] 17 | ; Inbound events will be passed to these streams: 18 | (streams 19 | (default :ttl 60 20 | ; Index all events immediately. 21 | index 22 | 23 | ; Log expired events. 24 | (expired 25 | (fn [event] (info "expired" event)))))) 26 | -------------------------------------------------------------------------------- /pkg/deb/riemann.config: -------------------------------------------------------------------------------- 1 | ; -*- mode: clojure; -*- 2 | ; vim: filetype=clojure 3 | 4 | (logging/init {:file "/var/log/riemann/riemann.log"}) 5 | 6 | ; Listen on the local interface over TCP (5555), UDP (5555), and websockets 7 | ; (5556) 8 | (let [host "127.0.0.1"] 9 | (tcp-server {:host host}) 10 | (udp-server {:host host}) 11 | (ws-server {:host host})) 12 | 13 | ; Expire old events from the index every 5 seconds. 14 | (periodically-expire 5) 15 | 16 | (let [index (index)] 17 | ; Inbound events will be passed to these streams: 18 | (streams 19 | (default :ttl 60 20 | ; Index all events immediately. 21 | index 22 | 23 | ; Log expired events. 24 | (expired 25 | (fn [event] (info "expired" event)))))) 26 | -------------------------------------------------------------------------------- /pkg/rpm/riemann.config: -------------------------------------------------------------------------------- 1 | ; -*- mode: clojure; -*- 2 | ; vim: filetype=clojure 3 | 4 | (logging/init {:file "/var/log/riemann/riemann.log"}) 5 | 6 | ; Listen on the local interface over TCP (5555), UDP (5555), and websockets 7 | ; (5556) 8 | (let [host "127.0.0.1"] 9 | (tcp-server {:host host}) 10 | (udp-server {:host host}) 11 | (ws-server {:host host})) 12 | 13 | ; Expire old events from the index every 5 seconds. 14 | (periodically-expire 5) 15 | 16 | (let [index (index)] 17 | ; Inbound events will be passed to these streams: 18 | (streams 19 | (default :ttl 60 20 | ; Index all events immediately. 21 | index 22 | 23 | ; Log expired events. 24 | (expired 25 | (fn [event] (info "expired" event)))))) 26 | -------------------------------------------------------------------------------- /test/riemann/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.test-utils 2 | "Utilities for writing tests.") 3 | 4 | (defn stub 5 | "Returns a fn that records its arguments in an atom, using conj. Always 6 | returns ::stub." 7 | [a] 8 | (fn [& args] 9 | (swap! a conj args) 10 | ::stub)) 11 | 12 | (defmacro with-mock 13 | "For the duration of the body, turns var-symbol into a stub which simply 14 | records its arguments as a seq in an atom. That atom is bound to `a-sym` for 15 | the duration of the body. You can deref `a-sym` at any point to find a vector 16 | of argument lists, one for each call made to the var. Calls to var will 17 | return ::stub. 18 | 19 | (with-mock [x class] (class 3 5) (class 6) @x) 20 | ; => [(3 5) (6)]" 21 | [[a-sym var-symbol] & body] 22 | `(let [~a-sym (atom [])] 23 | (with-redefs [~var-symbol (stub ~a-sym)] 24 | ~@body))) 25 | -------------------------------------------------------------------------------- /riemann.config: -------------------------------------------------------------------------------- 1 | ; -*- mode: clojure; -*- 2 | ; vim: filetype=clojure 3 | 4 | (logging/init {:file "riemann.log" :console true}) 5 | 6 | (tcp-server {:tls? false 7 | :key "test/data/tls/server.pkcs8" 8 | :cert "test/data/tls/server.crt" 9 | :ca-cert "test/data/tls/demoCA/cacert.pem"}) 10 | 11 | (instrumentation {:interval 1}) 12 | 13 | (udp-server) 14 | (ws-server) 15 | (graphite-server) 16 | 17 | (periodically-expire 1) 18 | 19 | (let [index (tap :index (index))] 20 | (streams 21 | (default :ttl 3 22 | (expired #(prn "Expired" %)) 23 | (where (not (service #"^riemann ")) 24 | index)))) 25 | 26 | (tests 27 | (deftest index-test 28 | (is (= (inject! [{:service "test" 29 | :time 1}]) 30 | {:index [{:service "test" 31 | :time 1 32 | :ttl 3}]})))) 33 | -------------------------------------------------------------------------------- /test/riemann/campfire_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.campfire-test 2 | (:use riemann.campfire 3 | clojure.test) 4 | (:require [riemann.logging :as logging])) 5 | 6 | (logging/init) 7 | 8 | (def campfire_api_token "test_token") 9 | (def campfire_domain "example.com") 10 | 11 | (def campfire-settings 12 | {:api-token campfire_api_token 13 | :ssl true 14 | :sub-domain campfire_domain}) 15 | 16 | (def test-campfire_message 17 | "Riemann alert on host01 - test_service is OK - Description: Quick brown fox") 18 | 19 | (def test-event 20 | {:host "host01" :service "test_service" :state "ok" :description "Quick brown fox"}) 21 | 22 | ; test that settings and formatting work 23 | (deftest camp-settings 24 | (is (= (cf-settings 25 | campfire_api_token true campfire_domain) campfire-settings)) 26 | (is (= (campfire_message test-event) 27 | test-campfire_message))) 28 | -------------------------------------------------------------------------------- /test/riemann/pushover_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.pushover-test 2 | (:use [riemann.time :only [unix-time]] 3 | riemann.pushover 4 | clojure.test) 5 | (:require [clj-http.client :as http] 6 | [riemann.test-utils :refer [with-mock]] 7 | [riemann.logging :as logging])) 8 | 9 | (logging/init) 10 | 11 | (deftest ^:pushover pushover-test 12 | (with-mock [calls clj-http.client/post] 13 | (let [pshvr (pushover "token" "user") 14 | time (unix-time)] 15 | 16 | (testing "an event without metric") 17 | (pshvr {:host "testhost" 18 | :service "testservice" 19 | :state "ok"}) 20 | (is (= (last @calls) 21 | ["https://api.pushover.net/1/messages.json" 22 | {:form-params {:token "token" 23 | :user "user" 24 | :title "testhost testservice" 25 | :message "testhost testservice is ok ()"}}]))))) 26 | -------------------------------------------------------------------------------- /test/riemann/nagios_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.nagios-test 2 | (:use riemann.nagios 3 | clj-nsca.core 4 | clojure.test) 5 | (:require [riemann.logging :as logging])) 6 | 7 | (logging/init) 8 | 9 | (def test-event 10 | {:host "host01" :service "test_service" :state "ok" :description "Quick brown fox"}) 11 | 12 | (def expected 13 | (let [e test-event] 14 | (nagios-message (:host e) (:state e) (:service e) (:description e)))) 15 | 16 | (deftest test-event-to-nagios 17 | (testing "Transform event to Nagios message" 18 | (is (= expected (event->nagios test-event)))) 19 | (testing "Malformed events are rejected" 20 | (is (thrown? IllegalArgumentException (event->nagios (merge test-event {:state "borken"})))))) 21 | 22 | (deftest test-stream 23 | (testing "Events get transformed and are sent" 24 | (let [message (atom nil)] 25 | (with-redefs [clj-nsca.core/send-message (fn [_ msg] (reset! message msg))] 26 | ((nagios {}) test-event)) 27 | (is (= expected @message))))) 28 | -------------------------------------------------------------------------------- /src/riemann/pushover.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.pushover 2 | "Forwards events to Pushover." 3 | (:require [clj-http.client :as client])) 4 | 5 | (def ^:private event-url 6 | "https://api.pushover.net/1/messages.json") 7 | 8 | (defn- post 9 | "POST to Pushover." 10 | [token user event] 11 | (client/post event-url 12 | {:form-params 13 | {:token token 14 | :user user 15 | :title (:title event) 16 | :message (:message event)}})) 17 | 18 | (defn- format-event 19 | "Formats an event for Pushover" 20 | [event] 21 | {:title (str (:host event) " " (:service event)) 22 | :message (str (:host event) " " 23 | (:service event) " is " 24 | (:state event) " (" 25 | (:metric event) ")")}) 26 | 27 | (defn pushover 28 | "Returns a function which accepts an event and sends it to Pushover. 29 | Usage: 30 | 31 | (pushover \"APPLICATION_TOKEN\" \"USER_KEY\")" 32 | [token user] 33 | (fn [event] 34 | (post token user (format-event event)))) 35 | -------------------------------------------------------------------------------- /test/riemann/kairosdb_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.kairosdb-test 2 | (:use riemann.kairosdb 3 | [riemann.time :only [unix-time]] 4 | clojure.test) 5 | (:require [riemann.logging :as logging])) 6 | 7 | (logging/init) 8 | 9 | (deftest ^:kairosdb ^:integration kairosdb-test 10 | (let [k (kairosdb {:block-start true})] 11 | (k {:host "riemann.local" 12 | :service "kairosdb test" 13 | :state "ok" 14 | :description "all clear, uh, situation normal" 15 | :metric -2 16 | :time (unix-time)})) 17 | 18 | (let [k (kairosdb {:block-start true})] 19 | (k {:service "kairosdb test" 20 | :state "ok" 21 | :description "all clear, uh, situation normal" 22 | :metric 3.14159 23 | :time (unix-time)})) 24 | 25 | (let [k (kairosdb {:block-start true})] 26 | (k {:host "no-service.riemann.local" 27 | :state "ok" 28 | :description "Missing service, not transmitted" 29 | :metric 4 30 | :time (unix-time)}))) 31 | -------------------------------------------------------------------------------- /test/riemann/opentsdb_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.opentsdb-test 2 | (:use riemann.opentsdb 3 | [riemann.time :only [unix-time]] 4 | clojure.test) 5 | (:require [riemann.logging :as logging])) 6 | 7 | (logging/init) 8 | 9 | (deftest ^:opentsdb ^:integration opentsdb-test 10 | (let [k (opentsdb {:block-start true})] 11 | (k {:host "riemann.local" 12 | :service "opentsdb test" 13 | :state "ok" 14 | :description "all clear, uh, situation normal" 15 | :metric -2 16 | :time (unix-time)})) 17 | 18 | (let [k (opentsdb {:block-start true})] 19 | (k {:service "opentsdb test" 20 | :state "ok" 21 | :description "all clear, uh, situation normal" 22 | :metric 3.14159 23 | :time (unix-time)})) 24 | 25 | (let [k (opentsdb {:block-start true})] 26 | (k {:host "no-service.riemann.local" 27 | :state "ok" 28 | :description "Missing service, not transmitted" 29 | :metric 4 30 | :time (unix-time)}))) 31 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/careq.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC1zCCAb8CAQAwgZExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UE 3 | BwwNU2FuIEZyYW5jaXNjbzEaMBgGA1UECgwRUHVwcHkgQnVyZWF1Y3JhY3kxJzAl 4 | BgNVBAsMHkRpc3RyaWJ1dGVkIFN5c3RlbXMgRGVwYXJ0bWVudDEYMBYGA1UEAwwP 5 | dGVzdC5yaWVtYW5uLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 6 | sGcj8Qrbv7ky7218Adg7sFjNAtTCAUCBoNa/p1cJiFbLlYkuGOCuoblwjarXNU0b 7 | VAtWmj+5sdkY4k5BYAACL9+7gMKbz/vrE3go6Q5CzLMh7/PePhnOo6zyZ3iYv/vg 8 | o8+/9wzdNT85J74SzhlmCqJVhHwUSsz1KGl0AuXJ7vN23zjjBP0ImejR1vx/9+kj 9 | 2yN5UB1nCnw6Zo2JKXn9xxsA5gmDoeVnd9xYvmmdemSn08zm1jJ2N+VfUGmMazxp 10 | MwryaLsN8hZIgsYRndo2fTq6DTs8HXI+v/q7/ng+sHrIFRtPKpkMj+ggW35uAoFI 11 | 1MRw0+rHjFHDGCXOXKVzswIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAF3JqSXd 12 | aw39LqJrpTt+OB52lqpQGPsGwrM9CmQ2XdlSEUOgrbwdU6b2tfj+dCYI8xoGBXkw 13 | OWUdA4N1gGkBa7njdpINNutIYVtI2SCxcLyCz3XBAIN46FAzSOeHOq2Ht86Oom2h 14 | 4SY9Pd5uKcp4BtZTMaj49coyEmf5XMLjwe8aUbpjT+a58wSOMcoZSC+ddpEtsoab 15 | QkrD6KTL2874QNb+lRU1SCXHVeqec7dTDktmglQs4ZgDDXOCiVOoXQZnyizyVxoo 16 | Ilxv/ESJFFWoYJJhbwmS8iWnRxdSKc3pctJrTCNmZuAF4qHPYHhKxdw7M5SXHuca 17 | GumkC/wZZZ1ywW8= 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /test/riemann/keenio_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.keenio-test 2 | (:require [clj-http.client :as client] 3 | [clojure.test :refer :all] 4 | [riemann.keenio :as k] 5 | [riemann.time :refer [unix-time]] 6 | [riemann.test-utils :refer [with-mock]])) 7 | 8 | (deftest keenio-test 9 | (with-mock [calls client/post] 10 | (let [k (k/keenio "ships" "tau-ceti-v" "shodan")] 11 | (k {:host "vonbraun" 12 | :service "the many" 13 | :description "the glory of the flesh" 14 | :state "perfect" 15 | :time 12345678 16 | :ttl 300}) 17 | (is (= 1 (count @calls))) 18 | (is (= (last @calls) 19 | ["https://api.keen.io/3.0/projects/tau-ceti-v/events/ships" 20 | {:body "{\"description\":\"the glory of the flesh\",\"service\":\"the many\",\"time\":12345678,\"state\":\"perfect\",\"host\":\"vonbraun\",\"ttl\":300}" 21 | :query-params {"api_key" "shodan"} 22 | :socket-timeout 5000 23 | :conn-timeout 5000 24 | :content-type :json 25 | :accept :json 26 | :throw-entire-message? true}]))))) 27 | -------------------------------------------------------------------------------- /test/riemann/opsgenie_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.opsgenie-test 2 | (:use riemann.opsgenie 3 | clojure.test) 4 | (:require [riemann.logging :as logging])) 5 | 6 | (def service-key (System/getenv "OPSGENIE_SERVICE_KEY")) 7 | (def recipients (System/getenv "OPSGENIE_RECIPIENTS")) 8 | 9 | (when-not service-key 10 | (println "export OPSGENIE_SERVICE_KEY=\"...\" to run these tests.")) 11 | 12 | (when-not recipients 13 | (println "export OPSGENIE_RECIPIENTS=\"...\" to run these tests.")) 14 | 15 | (logging/init) 16 | 17 | (deftest ^:opsgenie ^:integration test-resolve 18 | (let [og (opsgenie service-key recipients)] 19 | ((:resolve og) {:host "localhost" 20 | :service "opsgenie notification" 21 | :description "Testing resolving event" 22 | :metric 42 23 | :state "ok"}))) 24 | 25 | (deftest ^:opsgenie ^:integration test-trigger 26 | (let [og (opsgenie service-key recipients)] 27 | ((:trigger og) {:host "localhost" 28 | :service "opsgenie notification" 29 | :description "Testing triggering event" 30 | :metric 20 31 | :state "error"}))) 32 | -------------------------------------------------------------------------------- /src/riemann/keenio.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.keenio 2 | "Forwards events to Keen IO" 3 | (:require [clj-http.client :as client]) 4 | (:require [cheshire.core :as json])) 5 | 6 | (def ^:private event-url 7 | "https://api.keen.io/3.0/projects/") 8 | 9 | (defn post 10 | "POST to Keen IO." 11 | [collection project-id write-key request] 12 | (let [final-event-url 13 | (str event-url project-id "/events/" collection)] 14 | (client/post final-event-url 15 | {:body (json/generate-string request) 16 | :query-params { "api_key" write-key } 17 | :socket-timeout 5000 18 | :conn-timeout 5000 19 | :content-type :json 20 | :accept :json 21 | :throw-entire-message? true}))) 22 | 23 | (defn keenio 24 | "Creates a keen adapter. Takes your Keen project id and write key, and 25 | returns a function that accepts an event and sends it to Keen IO. The full 26 | event will be sent. 27 | 28 | (streams 29 | (let [kio (keenio \"COLLECTION_NAME\" \"PROJECT_ID\" \"WRITE_KEY\")] 30 | (where (state \"error\") kio)))" 31 | [collection project-id write-key] 32 | (fn [event] 33 | (post collection project-id write-key event))) 34 | -------------------------------------------------------------------------------- /src/riemann/repl.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.repl 2 | "The riemann REPL server is a bit of a special case. Since it controls almost 3 | every aspect of Riemann--and can shut those aspects down--it needs to live 4 | above them. While you usually *start* a repl server from the config file, it 5 | is not bound to the usual config lifecycle and won't be shut down or 6 | interrupted during config reload." 7 | (:use clojure.tools.logging) 8 | (:require [clojure.tools.nrepl.server :as nrepl])) 9 | 10 | (def server nil) 11 | 12 | (defn stop-server! 13 | "Stops the REPL server." 14 | [] 15 | (when-let [s server] 16 | (nrepl/stop-server s)) 17 | (def server nil)) 18 | 19 | (defn start-server! 20 | "Starts a new repl server. Stops the old server first, if any. Options: 21 | 22 | :host (default \"127.0.0.1\") 23 | :port (default 5557)" 24 | [opts] 25 | (stop-server!) 26 | (let [opts (merge {:port 5557 :host "127.0.0.1"} opts)] 27 | (def server (nrepl/start-server 28 | :port (:port opts) 29 | :bind (:host opts))) 30 | (info "REPL server" opts "online"))) 31 | 32 | (defn start-server 33 | "Starts a new REPL server, when one isn't already running." 34 | [opts] 35 | (when-not server (start-server! opts))) 36 | -------------------------------------------------------------------------------- /pkg/tar/riemann: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | top="$(dirname "$0")/.." 3 | 4 | JAR="$top/lib/riemann.jar:$EXTRA_CLASSPATH" 5 | CONFIG="$top/etc/riemann.config" 6 | COMMAND="start" 7 | AGGRESSIVE_OPTS="-server -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -XX:+UseCompressedOops -XX:+CMSClassUnloadingEnabled" 8 | 9 | usage() 10 | { 11 | cat << EOF 12 | usage: $0 [-a] [java options ...] [command] [config-file] 13 | 14 | Runs Riemann with the given configuration file. 15 | 16 | OPTIONS: 17 | -h Show this message 18 | -a Adds some default aggressive, nonportable JVM optimization flags. 19 | 20 | COMMANDS: 21 | start Start the Riemann server (this is the default) 22 | test Run the configuration tests 23 | 24 | Any unrecognized options (e.g. -XX:+UseParNewGC) will be passed on to java. 25 | EOF 26 | } 27 | 28 | OPTS= 29 | for arg in "$@"; do 30 | case $arg in 31 | "-a") 32 | OPTS="$AGGRESSIVE_OPTS $OPTS" 33 | ;; 34 | "-h") 35 | usage 36 | exit 0 37 | ;; 38 | -*) 39 | OPTS="$OPTS $arg" 40 | ;; 41 | test|start) 42 | COMMAND="$arg" 43 | ;; 44 | *) 45 | CONFIG="$arg" 46 | ;; 47 | esac 48 | done 49 | 50 | exec java $EXTRA_JAVA_OPTS $OPTS -cp "$JAR" riemann.bin "$COMMAND" "$CONFIG" 51 | -------------------------------------------------------------------------------- /pkg/deb/riemann: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f /etc/default/riemann ]; then 4 | . /etc/default/riemann 5 | fi 6 | 7 | JAR="/usr/share/riemann/riemann.jar:$EXTRA_CLASSPATH" 8 | CONFIG="/etc/riemann/riemann.config" 9 | COMMAND="start" 10 | AGGRESSIVE_OPTS="-server -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -XX:+UseCompressedOops -XX:+CMSClassUnloadingEnabled" 11 | 12 | usage() 13 | { 14 | cat << EOF 15 | usage: $0 [-a] [java options ...] [command] [config-file] 16 | 17 | Runs Riemann with the given configuration file. 18 | 19 | OPTIONS: 20 | -h Show this message 21 | -a Adds some default aggressive, nonportable JVM optimization flags. 22 | 23 | COMMANDS: 24 | start Start the Riemann server (this is the default) 25 | test Run the configuration tests 26 | 27 | Any unrecognized options (e.g. -XX:+UseParNewGC) will be passed on to java. 28 | EOF 29 | } 30 | 31 | OPTS= 32 | for arg in "$@"; do 33 | case $arg in 34 | "-a") 35 | OPTS="$AGGRESSIVE_OPTS $OPTS" 36 | ;; 37 | "-h") 38 | usage 39 | exit 0 40 | ;; 41 | -*) 42 | OPTS="$OPTS $arg" 43 | ;; 44 | test|start) 45 | COMMAND="$arg" 46 | ;; 47 | *) 48 | CONFIG="$arg" 49 | ;; 50 | esac 51 | done 52 | 53 | exec java $EXTRA_JAVA_OPTS $OPTS -cp "$JAR" riemann.bin "$COMMAND" "$CONFIG" 54 | -------------------------------------------------------------------------------- /pkg/rpm/riemann: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f /etc/sysconfig/riemann ]; then 4 | . /etc/sysconfig/riemann 5 | fi 6 | 7 | JAR="/usr/lib/riemann/riemann.jar:$EXTRA_CLASSPATH" 8 | CONFIG="/etc/riemann/riemann.config" 9 | COMMAND="start" 10 | AGGRESSIVE_OPTS="-server -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -XX:+UseCompressedOops -XX:+CMSClassUnloadingEnabled" 11 | 12 | usage() 13 | { 14 | cat << EOF 15 | usage: $0 [-a] [java options ...] [command] [config-file] 16 | 17 | Runs Riemann with the given configuration file. 18 | 19 | OPTIONS: 20 | -h Show this message 21 | -a Adds some default aggressive, nonportable JVM optimization flags. 22 | 23 | COMMANDS: 24 | start Start the Riemann server (this is the default) 25 | test Run the configuration tests 26 | 27 | Any unrecognized options (e.g. -XX:+UseParNewGC) will be passed on to java. 28 | EOF 29 | } 30 | 31 | OPTS= 32 | for arg in "$@"; do 33 | case $arg in 34 | "-a") 35 | OPTS="$AGGRESSIVE_OPTS $OPTS" 36 | ;; 37 | "-h") 38 | usage 39 | exit 0 40 | ;; 41 | -*) 42 | OPTS="$OPTS $arg" 43 | ;; 44 | test|start) 45 | COMMAND="$arg" 46 | ;; 47 | *) 48 | CONFIG="$arg" 49 | ;; 50 | esac 51 | done 52 | 53 | exec java $EXTRA_JAVA_OPTS $OPTS -cp "$JAR" riemann.bin "$COMMAND" "$CONFIG" 54 | -------------------------------------------------------------------------------- /test/riemann/user_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.user-test 2 | "Userland macros for testing snippets of Riemann config." 3 | (:use 4 | riemann.streams 5 | riemann.email 6 | riemann.sns 7 | [riemann.time :only [unix-time linear-time once! every!]]) 8 | (:require 9 | riemann.streams 10 | riemann.config 11 | riemann.core 12 | riemann.index 13 | riemann.query)) 14 | 15 | (defmacro configure-core [& conf] 16 | "Load the given Riemann conf into the current core and reset the index. 17 | FOR TESTING PURPOSES ONLY." 18 | `(binding [*ns* (find-ns 'riemann.config)] 19 | (eval '(do 20 | (~'riemann.time/reset-tasks!) 21 | (~'clear!) 22 | (~'pubsub/sweep! (:pubsub @~'core)) 23 | (~'logging/init) 24 | (~'instrumentation {:enabled? false}) 25 | (~'periodically-expire) 26 | (~'streams ~@conf) 27 | (when-let [idx# (:index @~'core)] 28 | (~'riemann.index/clear idx#)) 29 | (~'apply!))))) 30 | 31 | (defn stream-events [& events] 32 | "Run the given events through the current config." 33 | (doseq [ev events] 34 | (riemann.core/stream! @riemann.config/core ev))) 35 | 36 | (defn search-index [query] 37 | "Query the current state of the Riemann index." 38 | (riemann.index/search (:index @riemann.config/core) 39 | (riemann.query/ast query))) 40 | -------------------------------------------------------------------------------- /src/riemann/campfire.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.campfire 2 | "Forwards events to Campfire" 3 | (:use [clojure.string :only [join upper-case]]) 4 | (:require [clj-campfire.core :as cf])) 5 | 6 | (defn cf-settings 7 | "Setup settings for campfire. Required information is your api-token, ssl connection 8 | true or false, and your campfire sub-domain." 9 | [token ssl sub-domain] 10 | {:api-token token, :ssl ssl, :sub-domain sub-domain}) 11 | 12 | (defn room 13 | "Sets up the room to send events too. Pass in the settings created with cf-settings 14 | and the room name" 15 | [settings room-name] 16 | (cf/room-by-name settings room-name)) 17 | 18 | (defn campfire_message 19 | "Formats an event into a string" 20 | [e] 21 | (str (join " " ["Riemann alert on" (str (:host e)) "-" (str (:service e)) "is" (upper-case (str (:state e))) "- Description:" (str (:description e))]))) 22 | 23 | (defn campfire 24 | "Creates an adaptor to forward events to campfire. The campfire event will 25 | contain the host, state, service, metric and description. 26 | 27 | Tested with: 28 | (streams 29 | (by [:host, :service] 30 | (let [camp (campfire \"token\", true, \"sub-domain\", \"room\")] 31 | camp)))" 32 | [token ssl sub-domain room-name] 33 | (fn [e] 34 | (let [message_string (campfire_message e) 35 | settings (cf-settings token ssl sub-domain)] 36 | (cf/message (room settings room-name) message_string)))) 37 | -------------------------------------------------------------------------------- /src/riemann/nagios.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.nagios 2 | "Forwards events to Nagios via NSCA" 3 | (:require [clj-nsca.core :as nsca])) 4 | 5 | (def NONE nsca/NO_ENCRYPTION) 6 | (def XOR nsca/XOR_ENCRYPTION) 7 | (def TRIPLE_DES nsca/TRIPLE_DES_ENCRYPTION) 8 | 9 | (defn event->nagios 10 | "Converts an event into a Nagios message" 11 | [e] 12 | (nsca/nagios-message (str (:host e)) 13 | (str (:state e)) 14 | (str (:service e)) 15 | (str (:description e)))) 16 | 17 | (defn nagios 18 | "Creates an adapter to forward events to Nagios. The Nagios message will 19 | contain the host, state, service and description. Use: 20 | 21 | (nagios {:host \"localhost\" :port 5667 :password \"secret\" :encryption TRIPLE_DES}) 22 | 23 | :host Host where the Nagios service runs. Defaults to \"127.0.0.1\". 24 | :port The port to connect to. Defaults to 5667. 25 | :password The password as set in /etc/nsca.cfg. Defaults to \"password\". 26 | :encryption The encryption method as set in /etc/nsca.cfg. Defaults to TRIPLE_DES. 27 | Please note that currently only NONE, XOR and TRIPLE_DES are supported. 28 | " 29 | [opts] 30 | (let [opts (merge {:host "127.0.0.1" 31 | :port 5667 32 | :password "password" 33 | :encryption TRIPLE_DES} opts) 34 | sender (nsca/nagios-sender (nsca/nagios-settings opts))] 35 | (fn [event] 36 | (let [msg (event->nagios event)] 37 | (nsca/send-message sender msg))))) 38 | -------------------------------------------------------------------------------- /test/riemann/instrumentation_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.instrumentation-test 2 | (:require [riemann.logging :as logging]) 3 | (:use clojure.test 4 | riemann.instrumentation 5 | riemann.time.controlled 6 | [riemann.time :only [unix-time]])) 7 | 8 | (logging/init) 9 | (use-fixtures :each reset-time!) 10 | (use-fixtures :once control-time!) 11 | 12 | (deftest measure-latency-test 13 | (let [r (rate+latency {:service "meow" 14 | :meow true} 15 | [0 3/5 1.0])] 16 | (dotimes [i 100] 17 | (measure-latency r (Thread/sleep 1))) 18 | 19 | (advance! 5) 20 | (let [es (events r)] 21 | ; Should have merged from the original event 22 | (is (every? true? (map :meow es))) 23 | 24 | ; Should emit a rate and quantiles 25 | (is (= ["riemann meow rate" 26 | "riemann meow latency 0" 27 | "riemann meow latency 3/5" 28 | "riemann meow latency 1.0"] 29 | (map :service es))) 30 | 31 | ; Uses unix-time 32 | (is (every? (partial = 5) (map :time es))) 33 | 34 | ; Has float metrics 35 | (is (every? float? (map :metric es))) 36 | 37 | (let [quantiles (rest (map :metric es))] 38 | ; Quantiles are sorted 39 | (is (= quantiles (sort quantiles))) 40 | 41 | ; Quantiles are roughly in ms 42 | (is (every? (fn [x] (< 1 x 20)) quantiles)))))) 43 | -------------------------------------------------------------------------------- /src/riemann/shinken.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.shinken 2 | "Forwards events to Shinken." 3 | (:require [clj-http.client :as http])) 4 | 5 | (defn- post 6 | "POST to Shinken." 7 | [hostname port username password event] 8 | (http/post 9 | (str "http://" hostname ":" port "/push_check_result") 10 | {:basic-auth [username password] 11 | :form-params event})) 12 | 13 | (defn- format-event 14 | "Formats an event for Shinken." 15 | [event] 16 | {:time_stamp (int (:time event)) 17 | :host_name (:host event) 18 | :service_description (:service event) 19 | :return_code (:state event) 20 | :output (:metric event)}) 21 | 22 | (defn shinken 23 | "Returns a function which accepts an event and sends it to Shinken's 24 | ws-arbiter module. 25 | 26 | (shinken {:hostname \"127.0.0.1\" :port 7760 :username \"admin\" :password 27 | \"admin\"}) 28 | 29 | Options: 30 | 31 | :hostname Host name of the Shinken receiver/arbiter. Default is 32 | \"127.0.0.1\". 33 | :port Port that mod-ws-arbiter is listening to. Default is 7760 34 | :username Username. Default is \"admin\". 35 | :password Password of the corresponding user. Default is \"admin\"." 36 | 37 | [opts] 38 | (let 39 | [opts (merge {:hostname "127.0.0.1" 40 | :port 7760 41 | :username "admin" 42 | :password "admin"} opts)] 43 | 44 | (fn[event] (post 45 | (:hostname opts) 46 | (:port opts) 47 | (:username opts) 48 | (:password opts) 49 | (format-event event))))) 50 | -------------------------------------------------------------------------------- /src/riemann/cloudwatch.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.cloudwatch 2 | "Forwards riemann events to Amazon CloudWatch" 3 | (:import org.joda.time.DateTime) 4 | (:require [amazonica.core :as core] 5 | [amazonica.aws.cloudwatch :as cloudwatch])) 6 | 7 | (defn generate-datapoint 8 | "Accepts riemann event and converts it into cloudwatch datapoint." 9 | [event] 10 | (conj [] 11 | {:metric-name (:service event) 12 | :timestamp (DateTime. ) 13 | :value (:metric event) 14 | :dimensions [{:name "Host" 15 | :value (:host event)}]})) 16 | 17 | (defn cloudwatch 18 | "Returns a function which accepts an event and sends it to cloudwatch. 19 | Usage: 20 | 21 | (cloudwatch {:access-key \"AKJALPVWYQ6BFMVLSZDA\" 22 | :secret-key \"ZFEemkafy0paNMx5JcinMUiOC4dcMKhxXCL85DhM\"}) 23 | 24 | Options: 25 | 26 | :access-key AWS access key of your AWS Account. 27 | 28 | :secret-key AWS secret key for the above access key. 29 | 30 | :endpoint AWS Endpoint for posting metrics(changes with AWS region). 31 | 32 | :namespace AWS CloudWatch namespace." 33 | [opts] 34 | (let [opts (merge {:access-key "aws-access-key" 35 | :secret-key "aws-secret-key" 36 | :endpoint "monitoring.us-east-1.amazonaws.com" 37 | :namespace "Riemann"} opts)] 38 | (fn [event] 39 | (when (:metric event) 40 | (when (:service event) 41 | (let [datapoint (generate-datapoint event)] 42 | (cloudwatch/put-metric-data opts 43 | :namespace (:namespace opts) 44 | :metric-data datapoint))))))) 45 | -------------------------------------------------------------------------------- /test/riemann/datadog_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.datadog-test 2 | (:use riemann.datadog 3 | [riemann.time :only [unix-time]] 4 | clojure.test) 5 | (:require [riemann.logging :as logging])) 6 | 7 | (logging/init) 8 | 9 | (deftest ^:datadog ^:integration datadog-test 10 | (let [k (datadog {:api-key "datadog-test-key"})] 11 | (k {:host "riemann.local" 12 | :service "datadog test" 13 | :state "ok" 14 | :description "all clear, uh, situation normal" 15 | :metric -2 16 | :time (unix-time)})) 17 | 18 | (let [k (datadog {:api-key "datadog-test-key"})] 19 | (k {:service "datadog test" 20 | :state "ok" 21 | :description "all clear, uh, situation normal" 22 | :metric 3.14159 23 | :time (unix-time)})) 24 | 25 | (let [k (datadog {:api-key "datadog-test-key"})] 26 | (k {:host "no-service.riemann.local" 27 | :state "ok" 28 | :description "Missing service, not transmitted" 29 | :metric 4 30 | :time (unix-time)})) 31 | 32 | (let [k (datadog {:api-key "datadog-test-key"})] 33 | (k [ 34 | {:host "no-service.riemann.local" 35 | :state "ok" 36 | :description "Missing service, not transmitted" 37 | :metric 4 38 | :time (unix-time)}, 39 | {:host "no-service.riemann.local" 40 | :state "ok" 41 | :description "Missing service, not transmitted" 42 | :metric 4 43 | :time (unix-time)} 44 | ]))) 45 | -------------------------------------------------------------------------------- /src/riemann/datadog.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.datadog 2 | "Forward events to Datadog." 3 | (:use [clojure.string :only [join split]]) 4 | (:require [clj-http.client :as client] 5 | [cheshire.core :refer [generate-string]])) 6 | 7 | (def ^:private gateway-url "https://app.datadoghq.com/api/v1/series") 8 | 9 | (defn datadog-metric-name 10 | "Constructs a metric-name from an event." 11 | [event] 12 | (let [service (:service event) 13 | split-service (if service (split service #" ") [])] 14 | (join "." split-service))) 15 | 16 | (defn generate-datapoint 17 | "Creates a vector from riemann event." 18 | [event] 19 | [(vector (:time event) (:metric event))]) 20 | 21 | (defn post-datapoint 22 | "Post the riemann metrics datapoints." 23 | [api-key data] 24 | (let [url (str gateway-url "?api_key=" api-key) 25 | http-options {:body data 26 | :content-type :json}] 27 | (client/post url http-options))) 28 | 29 | (defn datadog 30 | "Return a function which accepts event and sends it to datadog. 31 | Usage: 32 | 33 | (datadog {:api-key \"bn14a6ac2e3b5h795085217d49cde7eb\"}) 34 | 35 | Option: 36 | 37 | :api-key Datadog's API Key for authentication." 38 | [opts] 39 | (let [opts (merge {:api-key "datadog-api-key"} opts)] 40 | (fn [event] 41 | (let [post-data {:series [{:metric (datadog-metric-name event) 42 | :type "gauge" 43 | :host (:host event) 44 | :tags (:tags event) 45 | :points (generate-datapoint event)}]} 46 | json-data (generate-string post-data)] 47 | (post-datapoint (:api-key opts) json-data))))) 48 | -------------------------------------------------------------------------------- /test/riemann/stackdriver_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.stackdriver-test 2 | (:use riemann.stackdriver 3 | [riemann.time :only [unix-time]] 4 | clojure.test) 5 | (:require [riemann.logging :as logging])) 6 | 7 | (logging/init) 8 | 9 | (deftest ^:stackdriver ^:integration stackdriver-test 10 | (let [k (stackdriver {:api-key "stackdriver-test-key"})] 11 | (k {:host "riemann.local" 12 | :service "stackdriver test" 13 | :state "ok" 14 | :description "all clear, uh, situation normal" 15 | :metric -2 16 | :time (unix-time)})) 17 | 18 | (let [k (stackdriver {:api-key "stackdriver-test-key"})] 19 | (k {:service "stackdriver test" 20 | :state "ok" 21 | :description "all clear, uh, situation normal" 22 | :metric 3.14159 23 | :time (unix-time)})) 24 | 25 | (let [k (stackdriver {:api-key "stackdriver-test-key"})] 26 | (k {:host "no-service.riemann.local" 27 | :state "ok" 28 | :description "Missing service, not transmitted" 29 | :metric 4 30 | :time (unix-time)})) 31 | 32 | (let [k (stackdriver {:api-key "stackdriver-test-key"})] 33 | (k [ 34 | {:host "no-service.riemann.local" 35 | :state "ok" 36 | :description "Missing service, not transmitted" 37 | :metric 4 38 | :time (unix-time)}, 39 | {:host "no-service.riemann.local" 40 | :state "ok" 41 | :description "Missing service, not transmitted" 42 | :metric 4 43 | :time (unix-time)} 44 | ]))) 45 | -------------------------------------------------------------------------------- /src/riemann/stackdriver.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.stackdriver 2 | "Forwards events to Stackdriver." 3 | (:require riemann.time 4 | [clj-http.client :as client] 5 | [cheshire.core :refer [generate-string]] 6 | [clojure.string :as str])) 7 | 8 | (def gateway-url "https://custom-gateway.stackdriver.com/v1/custom") 9 | 10 | (defn metric-name 11 | "Constructs a metric name from an event." 12 | [opts event] 13 | (let [service ((:name opts) event)] 14 | (str/replace service #"\s+" "."))) 15 | 16 | (defn generate-datapoints 17 | "Accepts riemann event/events and converts it into equivalent stackdriver datapoint." 18 | [opts event-or-events] 19 | (->> (if (sequential? event-or-events) event-or-events (list event-or-events)) 20 | (remove (comp nil? :metric)) 21 | (map (fn [event] 22 | {:name (metric-name opts event) 23 | :value (:metric event) 24 | :collected_at (long (:time event))})))) 25 | 26 | (defn post-datapoints 27 | "Post the riemann metrics datapoints." 28 | [api-key uri data] 29 | (let [http-options {:body data 30 | :content-type :json 31 | :headers {"x-stackdriver-apikey" api-key}}] 32 | (client/post uri http-options))) 33 | 34 | (defn stackdriver 35 | "Returns a function which accepts an event/events and sends it to Stackdriver." 36 | [opts] 37 | (let [ts (atom 0) 38 | opts (merge {:api-key "stackdriver-api-key" 39 | :name :service} opts)] 40 | (fn [event-or-events] 41 | (when-let [data (not-empty (generate-datapoints opts event-or-events))] 42 | (->> {:timestamp (swap! ts #(max (inc %) (long (riemann.time/unix-time)))) 43 | :proto_version 1 44 | :data data} 45 | (generate-string) 46 | (post-datapoints (:api-key opts) gateway-url)))))) 47 | -------------------------------------------------------------------------------- /src/riemann/pagerduty.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.pagerduty 2 | "Forwards events to Pagerduty" 3 | (:require [clj-http.client :as client]) 4 | (:require [cheshire.core :as json])) 5 | 6 | (def ^:private event-url 7 | "https://events.pagerduty.com/generic/2010-04-15/create_event.json") 8 | 9 | (defn- post 10 | "POST to the PagerDuty events API." 11 | [request] 12 | (client/post event-url 13 | {:body (json/generate-string request) 14 | :socket-timeout 5000 15 | :conn-timeout 5000 16 | :content-type :json 17 | :accept :json 18 | :throw-entire-message? true})) 19 | 20 | (defn- format-event 21 | "Formats an event for PD. event-type is one of :trigger, :acknowledge, 22 | :resolve" 23 | [service-key event-type event] 24 | {:service_key service-key 25 | :event_type event-type 26 | :incident_key (str (:host event) " " (:service event)) 27 | :description (str (:host event) " " 28 | (:service event) " is " 29 | (:state event) " (" 30 | (:metric event) ")") 31 | :details event}) 32 | 33 | (defn pagerduty 34 | "Creates a pagerduty adapter. Takes your PD service key, and returns a map of 35 | functions which trigger, acknowledge, and resolve events. Event service will 36 | be used as the incident key. The PD description will be the service, state, 37 | and metric. The full event will be attached as the details. 38 | 39 | (let [pd (pagerduty \"my-service-key\")] 40 | (changed-state 41 | (where (state \"ok\") (:resolve pd)) 42 | (where (state \"critical\") (:trigger pd))))" 43 | [service-key] 44 | {:trigger (fn [e] (post (format-event service-key :trigger e))) 45 | :acknowledge (fn [e] (post (format-event service-key :acknowledge e))) 46 | :resolve (fn [e] (post (format-event service-key :resolve e)))}) 47 | -------------------------------------------------------------------------------- /test/riemann/test_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.test-test 2 | "Who tests the testers?" 3 | (:require [riemann.test :refer [tap io with-test-env inject!]] 4 | [clojure.test :refer :all] 5 | [riemann.streams :refer :all])) 6 | 7 | (defmacro bound 8 | "Invokes body in a bound fn. Tests may run without a binding context, which 9 | breaks eval + ns." 10 | [& body] 11 | `((bound-fn [] ~@body))) 12 | 13 | (deftest only-one-tap-per-context 14 | (let [err (try 15 | (with-test-env 16 | (eval 17 | `(do (tap :foo prn) 18 | (tap :bar prn) 19 | (tap :foo nil)))) 20 | (catch RuntimeException e 21 | (.getMessage e)))] 22 | (is (re-find #"Tap :foo \(.+?:\) already defined at :" err)))) 23 | 24 | (deftest tap-captures-events 25 | (with-test-env 26 | (bound 27 | (eval 28 | '(do 29 | (ns riemann.test-test) 30 | (let [downstream (promise) 31 | s (rate 5 (tap :cask (partial deliver downstream)))] 32 | (is (= (inject! [s] 33 | [{:time 0 :metric 0} 34 | {:time 1 :metric 1} 35 | {:time 2 :metric 2} 36 | {:time 3 :metric 3} 37 | {:time 4 :metric 4} 38 | {:time 5 :metric 5}]) 39 | {:cask [{:time 5 :metric 2}]})) 40 | (is (= @downstream {:time 5 :metric 2})))))))) 41 | 42 | (deftest io-suppression 43 | (with-test-env 44 | (bound 45 | (eval 46 | '(do 47 | (ns riemann.test-test) 48 | (let [downstream (promise) 49 | s (sdo (io (partial deliver downstream)))] 50 | (inject! [s] [{:time 2 :metric 0}]) 51 | (is (nil? (deref downstream 0 nil))))))))) 52 | -------------------------------------------------------------------------------- /test/riemann/time/controlled_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.time.controlled-test 2 | (:use riemann.time.controlled 3 | riemann.time 4 | [riemann.common :exclude [unix-time linear-time]] 5 | clojure.math.numeric-tower 6 | clojure.test) 7 | (:require [riemann.logging :as logging])) 8 | 9 | (use-fixtures :once control-time!) 10 | (use-fixtures :each reset-time!) 11 | 12 | (deftest clock-test 13 | (is (= (unix-time-controlled) 0)) 14 | (advance! -1) 15 | (is (= (unix-time-controlled) 0)) 16 | (advance! 4.5) 17 | (is (= (unix-time-controlled) 4.5)) 18 | (reset-time!) 19 | (is (= (unix-time-controlled) 0))) 20 | 21 | (deftest once-test 22 | (let [x (atom 0) 23 | once1 (once! 1 #(swap! x inc)) 24 | once2 (once! 2 #(swap! x inc)) 25 | once3 (once! 3 #(swap! x inc))] 26 | 27 | (advance! 0.5) 28 | (is (= @x 0)) 29 | 30 | (advance! 2) 31 | (is (= @x 2)) 32 | 33 | (cancel once3) 34 | (advance! 3) 35 | (is (= @x 2)))) 36 | 37 | (deftest every-test 38 | (let [x (atom 0) 39 | bump #(swap! x inc) 40 | task (every! 1 2 bump)] 41 | 42 | (is (= @x 0)) 43 | 44 | (advance! 1) 45 | (is (= @x 0)) 46 | 47 | (advance! 2) 48 | (is (= @x 1)) 49 | 50 | (advance! 3) 51 | (is (= @x 2)) 52 | 53 | (advance! 4) 54 | (is (= @x 3)) 55 | 56 | ; Double-down 57 | (defer task -3) 58 | (is (= @x 3)) 59 | (advance! 5) 60 | (is (= @x 8)) 61 | 62 | ; Into the future! 63 | (defer task 4) 64 | (advance! 8) 65 | (is (= @x 8)) 66 | (advance! 9) 67 | (is (= @x 9)) 68 | (advance! 10) 69 | (is (= @x 10)))) 70 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/private/cakey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQISOICI3uyj8QCAggA 3 | MBQGCCqGSIb3DQMHBAgB+n4x15hWsASCBMhtjDTQ3P6Rm/YsIEVAeUleXRA7/hGf 4 | qqhLiA05sYlyhjVHTkikSj7nfGgUDyirzdjnQhfutE3Ye7Wys3ZHQmAqxaCB2bdr 5 | IhbGGCLAtDjDZ6wP9gjsKDxfuGz7VFGaC46d2ejEbkqv6lqVBDhIEC0g2/6NRl2Z 6 | 61vKUZjHvy6F4Pq+Shp3nb92oLBSsn2taJzvaNsRkvmEKe7w/DcgDW3Kb0Op9EVy 7 | upPq1IAeDjPf/McfJ+BXOaxMUK8lNNl0j5NorC32GFB8z2RwQ5uqL2fjq73DNruI 8 | wmy7vJoI9Ro68VXjHH78d6Wa3hzL1nQMTGZXLwOtOkQxi+Z7kV9J7lJTMRPn9h0M 9 | rNxFEGdCaiXNtRxGRxHHNNGGZ8u6J7xVbJRBoo2TDTsQ1Xo9BPZFNUMj2BmKgA1S 10 | IzcaTl0lsJMhSeD6+I/2HW7ZRlh5L3nM27g8O4ab8/KCjUYTUEpSRNifafX7h+eM 11 | kBGZo4wYq3fjATERczryIIPk/GK3tw2jhfr+6KOxkWWTvX8UCrVQxRcTxeexIScp 12 | UAop/WXoslRfMAsVEmxA4glsL7iNQdUJIfxsSJsNzyPJM9nxE3x/wjYowGZ95UAp 13 | YOEwmKi6frP32gs1NJ/ToMfcdUOn8gG9fA6B5l2lBFF/WvuyzemjnMvj1yQ6t1Kf 14 | KOTWJD8kINyMfcZXHtUOIgXl9i8pJjJZmL9d2MXz5RdeJxZ6nr3uYdvdwgU2S0YX 15 | tdcFmJouODqtWLVygUkj3v3kAwFkZvEg1iYVSwYwt8dS+WLe/Q2kVNgvPXHM7MfX 16 | 1zwztyxCTI7hkUzRUg3nhqDfQ6JrXHBLFN+wJrw9dESA/JfeN5EoPtyE8FjHBA6T 17 | hIxMu+UXKUAgwekqS4fCxFKY0VXc77ypWk8XDxIy3JoeizkNk/9x/mu480RjW5FT 18 | wqYVTiE5KoS6og3+8BaQxfAwHE6lrFWYT6LA2YsDiRJSyF29rBHyeA9ekEw2VoMb 19 | Gg/56Stb8rj2822YOVVlivkmmWj6JT51/5HWYpQKMnJEoxJzzZi1KT34GJKU7JYZ 20 | pQQqeCRQt+A5E7Z6itBNFmf8YCmxpWGU1kYe0zK9g/jHln3PkErFyRY4T8CCwPmD 21 | OzddAG9o7NOkgRJR7Ycr+QAAoaNcQT+ALLKRAeQh/Joevvs+9x8GcGdIS37tOqlL 22 | Ab5aMS29FBHwieMFjZvk+6OJLIdE9s+G7gwH4TqpuhL/5JxaWbzXVlcOoLyPHabZ 23 | hlTF0re/eoLbD5H31F1OiA0oVsJV6q4WxjCEEdNd24unnb+q8LiMK5rAwbcWEMUQ 24 | 57t5NUfRwOMVNwp/g1Fxv3fhx+H+C26fFObN01NRQf03/PPIkHplHZfF6/gJb/WM 25 | 6v2jTE7h4dgxiUyFWUwdWvaDRYTuY2Zhv2vCidFO+TUhZjYO/ktkqgcdptI2QOjL 26 | qJdYd9AyQoBMNlmYqnPEI3ZdCnHZUo04KL5S74W7WFOb/N/zDI9fot/VGX7HEv4P 27 | M/0phmTaYzOxUkKnTVbbPVXWaxuX8ukxUf7BVvpLJvmDIrzT5eYWY6I5IbX8djNB 28 | m6ShcXU6caQhSPEPxvPdWrMIWY1rFLL3EVqk2bqO5JZAW7ml1tpwjpNo+ls7/Tyh 29 | 21o= 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /test/riemann/cloudwatch_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.cloudwatch-test 2 | (:use riemann.cloudwatch 3 | [riemann.time :only [unix-time]] 4 | clojure.test) 5 | (:require [riemann.logging :as logging])) 6 | 7 | (logging/init) 8 | 9 | (deftest ^:cloudwatch ^:integration cloudwatch-test 10 | (let [k (cloudwatch {:access-key "aws-access-key" 11 | :secret-key "aws-secret-key"})] 12 | (k {:host "riemann.local" 13 | :service "cloudwatch test" 14 | :state "ok" 15 | :description "all clear, uh, situation normal" 16 | :metric -2 17 | :time (unix-time)})) 18 | 19 | (let [k (cloudwatch {:access-key "aws-access-key" 20 | :secret-key "aws-secret-key"})] 21 | (k {:service "cloudwatch test" 22 | :state "ok" 23 | :description "all clear, uh, situation normal" 24 | :metric 3.14159 25 | :time (unix-time)})) 26 | 27 | (let [k (cloudwatch {:access-key "aws-access-key" 28 | :secret-key "aws-secret-key"})] 29 | (k {:host "no-service.riemann.local" 30 | :state "ok" 31 | :description "Missing service, not transmitted" 32 | :metric 4 33 | :time (unix-time)})) 34 | 35 | (let [k (cloudwatch {:access-key "aws-access-key" 36 | :secret-key "aws-secret-key"})] 37 | (k [ 38 | {:host "no-service.riemann.local" 39 | :state "ok" 40 | :description "Missing service, not transmitted" 41 | :metric 4 42 | :time (unix-time)}, 43 | {:host "no-service.riemann.local" 44 | :state "ok" 45 | :description "Missing service, not transmitted" 46 | :metric 4 47 | :time (unix-time)} 48 | ]))) 49 | -------------------------------------------------------------------------------- /test/riemann/blueflood_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.blueflood-test 2 | (:use riemann.blueflood 3 | riemann.streams 4 | riemann.time.controlled 5 | riemann.time 6 | [riemann.test :refer [run-stream]] 7 | [riemann.config :refer [apply!]] 8 | clojure.test) 9 | (:require 10 | [riemann.logging :as logging] 11 | [cheshire.core :as json] 12 | [clj-http.client :as client])) 13 | 14 | (logging/init) 15 | 16 | (use-fixtures :once control-time!) 17 | (use-fixtures :each reset-time!) 18 | 19 | ; These tests assume you've got blueflood running on the localhost 20 | ; Fix the host:port below if that's not the case 21 | (def query-url-template 22 | (str "http://localhost:20000/v2.0/tenant-id/views/%s.%s?" 23 | "from=000000000&to=1500000000&resolution=FULL")) 24 | 25 | (defn test-helper [opts] 26 | (let [service (str (java.util.UUID/randomUUID)) 27 | host "a" 28 | query-url (format query-url-template host service) 29 | timestamp 3 30 | value 3 31 | input 32 | [{:host host 33 | :service service 34 | :metric value 35 | :time timestamp} 36 | 37 | ;; This second event doesn't get included in the batch but 38 | ;; the timestamp causes the the batch to complete and be sent 39 | ;; to blueflood with just the first event. 40 | {:time (inc timestamp)}] 41 | stream (blueflood-ingest opts prn)] 42 | ;; Create the async executor 43 | (apply!) 44 | ;; Feed the input into BF 45 | (run-stream (sdo stream) input) 46 | (Thread/sleep 300) 47 | ;; Read it back from BF 48 | (is (= (as-> (client/get query-url) x 49 | (:body x) 50 | (json/parse-string x) 51 | (x "values")) 52 | [{"numPoints" 1, "timestamp" timestamp, "average" value}])))) 53 | 54 | (deftest ^:blueflood ^:integration blueflood-ingest-test 55 | ;; test synchronously 56 | (test-helper {}) 57 | ;; test asynchronously 58 | (test-helper {:async-queue-name :testq})) 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/riemann/time/controlled.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.time.controlled 2 | "Provides controllable periodic and deferred execution. Calling (advance! 3 | delta-in-seconds) moves the clock forward, triggering events that would have 4 | occurred, in sequence." 5 | (:use riemann.time 6 | clojure.math.numeric-tower)) 7 | 8 | (def clock 9 | "Reference to the current time, in seconds." 10 | (atom nil)) 11 | 12 | (defn reset-clock! 13 | [] 14 | (reset! clock 0)) 15 | 16 | (defn reset-time! 17 | "Resets the clock and task queue. If a function is given, calls f after 18 | resetting the time and task list." 19 | ([f] (reset-time!) (f)) 20 | ([] 21 | (reset-clock!) 22 | (reset-tasks!))) 23 | 24 | (defn set-time! 25 | "Sets the current time, without triggering callbacks." 26 | [t] 27 | (reset! clock t)) 28 | 29 | (defn unix-time-controlled 30 | [] 31 | @clock) 32 | 33 | (defn linear-time-controlled 34 | [] 35 | @clock) 36 | 37 | (defn advance! 38 | "Advances the clock to t seconds, triggering side effects." 39 | [t] 40 | (when (< @clock t) 41 | (loop [] 42 | (when-let [task (poll-task!)] 43 | (if (<= (:t task) t) 44 | (do 45 | ; Consume task 46 | (swap! clock max (:t task)) 47 | (run task) 48 | (when-let [task' (succ task)] 49 | (schedule-sneaky! task')) 50 | (recur)) 51 | ; Return task 52 | (schedule-sneaky! task)))) 53 | (swap! clock max t))) 54 | 55 | (defmacro with-controlled-time! 56 | "Like control-time! but for without the fn callback. Again, *not* threadsafe; 57 | bindings take effect globally." 58 | [& body] 59 | ; Please forgive me 60 | `(with-redefs [riemann.time/unix-time unix-time-controlled 61 | riemann.time/linear-time linear-time-controlled] 62 | ~@body)) 63 | 64 | (defn control-time! 65 | "Switches riemann.time functions to time.controlled counterparts, invokes f, 66 | then restores them. Definitely not threadsafe. Not safe by any standard, 67 | come to think of it. Only for testing purposes." 68 | [f] 69 | (with-controlled-time! (f))) 70 | 71 | -------------------------------------------------------------------------------- /pkg/rpm/init.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | ### BEGIN INIT INFO 3 | # Provides: riemann 4 | # Required-Start: $all 5 | # Required-Stop: $all 6 | # Default-Start: 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Starts Riemann 9 | # chkconfig: - 80 15 10 | # Description: Riemann event monitoring server. 11 | ### END INIT INFO 12 | 13 | # Source function library. 14 | . /etc/rc.d/init.d/functions 15 | 16 | # Pull in sysconfig settings 17 | [ -f /etc/sysconfig/riemann ] && . /etc/sysconfig/riemann 18 | 19 | RIEMANN_USER=riemann 20 | 21 | DAEMON=/usr/bin/riemann 22 | NAME=riemann 23 | PID_FILE=${PIDFILE:-/var/run/${NAME}.pid} 24 | LOCK_FILE=${LOCKFILE:-/var/lock/subsys/${NAME}} 25 | NFILES=${NFILES:-32768} 26 | 27 | RIEMANN_PATH_CONF=${RIEMANN_PATH_CONF:-/etc/${NAME}} 28 | RIEMANN_CONFIG=${RIEMANN_CONFIG:-${RIEMANN_PATH_CONF}/riemann.config} 29 | 30 | DAEMON_OPTS="${RIEMANN_OPTS} ${RIEMANN_CONFIG}" 31 | 32 | start() { 33 | echo -n $"Starting ${NAME}: " 34 | ulimit -n $NFILES 35 | # daemon --pidfile $PID_FILE --user $RIEMANN_USER $DAEMON $DAEMON_OPTS 36 | daemonize -u $RIEMANN_USER -p $PID_FILE -l $LOCK_FILE $DAEMON $DAEMON_OPTS 37 | RETVAL=$? 38 | [ $RETVAL -eq 0 ] && touch $LOCK_FILE 39 | [ $RETVAL -eq 0 ] && success || failure 40 | echo 41 | return $RETVAL 42 | } 43 | 44 | reload() { 45 | echo -n $"Reloading ${NAME}: " 46 | killproc -p ${PID_FILE} $DAEMON -1 47 | RETVAL=$? 48 | echo 49 | return $RETVAL 50 | } 51 | 52 | stop() { 53 | echo -n $"Stopping ${NAME}: " 54 | killproc -p ${PID_FILE} -d 10 $DAEMON 55 | RETVAL=$? 56 | echo 57 | [ $RETVAL = 0 ] && rm -f ${LOCK_FILE} ${PID_FILE} 58 | return $RETVAL 59 | } 60 | 61 | case "$1" in 62 | start) 63 | start 64 | ;; 65 | stop) 66 | stop 67 | ;; 68 | status) 69 | status -p ${PID_FILE} $DAEMON 70 | RETVAL=$? 71 | ;; 72 | reload|force-reload) 73 | reload 74 | ;; 75 | restart) 76 | stop 77 | start 78 | ;; 79 | *) 80 | N=/etc/init.d/${NAME} 81 | echo "Usage: $N {start|stop|status|restart|force-reload}" >&2 82 | RETVAL=2 83 | ;; 84 | esac 85 | 86 | exit $RETVAL 87 | -------------------------------------------------------------------------------- /test/riemann/client_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.client-test 2 | (:use riemann.common 3 | riemann.core 4 | riemann.transport.tcp 5 | riemann.client 6 | [riemann.index :only [index]] 7 | [riemann.logging :only [suppress]] 8 | clojure.test)) 9 | 10 | (riemann.logging/init) 11 | 12 | (deftest reconnect 13 | (suppress ["riemann.transport.tcp" "riemann.core" "riemann.pubsub"] 14 | (let [server (tcp-server) 15 | core (transition! (core) {:services [server]}) 16 | client (tcp-client)] 17 | (.. client transport reconnectDelay (set 0)) 18 | (try 19 | ; Initial connection works 20 | (is @(send-event client {:service "test"})) 21 | 22 | ; Kill server; should fail. 23 | (stop! core) 24 | (is (thrown? java.io.IOException 25 | @(send-event client {:service "test"}))) 26 | 27 | ; Restart server; should work 28 | (start! core) 29 | (Thread/sleep 200) 30 | 31 | (try 32 | @(send-event client {:service "test"}) 33 | (finally 34 | (stop! core))) 35 | 36 | (finally 37 | (close! client) 38 | (stop! core)))))) 39 | 40 | ; Check that server error messages are correctly thrown. 41 | (deftest server-errors 42 | (suppress ["riemann.transport.tcp" "riemann.core" "riemann.pubsub"] 43 | (let [index (index) 44 | server (tcp-server) 45 | core (transition! (core) {:services [server] 46 | :index index}) 47 | client (tcp-client)] 48 | 49 | (try 50 | (is (thrown-with-msg? 51 | com.aphyr.riemann.client.ServerError 52 | #"^mismatched input 'no' expecting \{, 'or', 'and'\}$" 53 | @(query client "oh no not again"))) 54 | (finally 55 | (close! client) 56 | (stop! core)))))) 57 | -------------------------------------------------------------------------------- /test/riemann/email_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.email-test 2 | (:use [riemann.time :only [unix-time]] 3 | riemann.email 4 | [riemann.logging :only [suppress]] 5 | clojure.test)) 6 | 7 | (riemann.logging/init) 8 | 9 | (deftest override-formatting-test 10 | (let [a (promise)] 11 | (with-redefs [postal.core/send-message #(deliver a [%1 %2])] 12 | (email-event {} {:body (fn [events] 13 | (apply str "body " 14 | (map :service events))) 15 | :subject (fn [events] 16 | (apply str "subject " 17 | (map :service events)))} 18 | {:service "foo"})) 19 | (is (= @a [{} {:subject "subject foo" 20 | :body "body foo"}])))) 21 | 22 | (deftest email-test-erroring 23 | (testing "sending an email integration test" 24 | (let [email (mailer {:from "riemann-test"})] 25 | (is (thrown? java.lang.AssertionError 26 | (email [:a :b]))) 27 | (is (thrown? java.lang.AssertionError 28 | (email {:host "localhost" 29 | :service "email test" 30 | :state "ok" 31 | :description "all clear, uh, situation normal" 32 | :metric 3.14159 33 | :time (unix-time)})))))) 34 | 35 | (deftest email-test-list-input 36 | (testing "sending an email to a list of recipients" 37 | (let [p (promise)] 38 | (with-redefs [riemann.email/email-event (fn [_ m _] (deliver p (:to m)))] 39 | (let [email ((mailer) ["a@a.a" "b@b.b"])] 40 | (email {:service "foo"})) 41 | (is (= (deref p 500 nil) ["a@a.a" "b@b.b"])))))) 42 | 43 | (deftest ^:email ^:integration email-test 44 | (testing "sending an email integration test" 45 | (let [email (mailer {:from "riemann-test"}) 46 | stream (email "aphyr@aphyr.com")] 47 | (stream {:host "localhost" 48 | :service "email test" 49 | :state "ok" 50 | :description "all clear, uh, situation normal" 51 | :metric 3.14159 52 | :time (unix-time)})))) 53 | -------------------------------------------------------------------------------- /test/riemann/hipchat_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.hipchat-test 2 | (:use riemann.hipchat 3 | clojure.test) 4 | (:require [riemann.logging :as logging])) 5 | 6 | (def server (System/getenv "HIPCHAT_SERVER")) 7 | (def api-key (System/getenv "HIPCHAT_API_KEY")) 8 | (def room (System/getenv "HIPCHAT_ALERT_ROOM")) 9 | 10 | (when-not server 11 | (println "export HIPCHAT_SERVER=\"...\" to run these tests.")) 12 | 13 | (when-not api-key 14 | (println "export HIPCHAT_API_KEY=\"...\" to run these tests.")) 15 | 16 | (when-not room 17 | (println "export HIPCHAT_ALERT_ROOM=\"...\" to run these tests.")) 18 | 19 | (logging/init) 20 | 21 | (deftest ^:hipchat ^:integration good_event 22 | (let [hc (hipchat {:server server :token api-key :room room :notify 0})] 23 | (hc {:host "localhost" 24 | :service "hipchat test good" 25 | :description "Testing a metric with ok state" 26 | :metric 42 27 | :state "ok"}))) 28 | 29 | (deftest ^:hipchat ^:integration error_event 30 | (let [hc (hipchat {:server server :token api-key :room room :notify 0})] 31 | (hc {:host "localhost" 32 | :service "hipchat test error" 33 | :description "Testing a metric with error state" 34 | :metric 43 35 | :state "error"}))) 36 | 37 | (deftest ^:hipchat ^:integration critical_event 38 | (let [hc (hipchat {:server server :token api-key :room room :notify 0})] 39 | (hc {:host "localhost" 40 | :service "hipchat test critical" 41 | :description "Testing a metric with critical state" 42 | :metric 44 43 | :state "critical"}))) 44 | 45 | (deftest ^:hipchat ^:integration yellow 46 | (let [hc (hipchat {:server server :token api-key :room room :notify 0})] 47 | (hc {:host "localhost" 48 | :service "hipchat test yellow" 49 | :description "Testing a metric with unknown state" 50 | :metric 45 51 | :state "unknown"}))) 52 | 53 | (deftest ^:hipchat ^:integration multiple_events 54 | (let [hc (hipchat {:server server :token api-key :room room :notify 0})] 55 | (hc [{:host "localhost" 56 | :service "hipchat multi 1" 57 | :description "Testing multiple metrics" 58 | :metric 46 59 | :state "ok"} 60 | {:host "localhost" 61 | :service "hipchat multi 2" 62 | :description "Still testing multiple metrics" 63 | :metric 47 64 | :state "ok"}]))) 65 | -------------------------------------------------------------------------------- /test/riemann/pubsub_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.pubsub-test 2 | (:use riemann.pubsub 3 | riemann.core 4 | riemann.index 5 | [riemann.common :only [event]] 6 | clojure.test) 7 | (:require [riemann.logging :as logging])) 8 | 9 | (deftest one-to-one 10 | (let [r (pubsub-registry) 11 | out (atom []) 12 | id (subscribe! r :foo #(swap! out conj %))] 13 | 14 | (publish! r :foo 1) 15 | (publish! r :foo 2) 16 | (is (= @out [1 2])))) 17 | 18 | (deftest one-to-many 19 | (let [r (pubsub-registry) 20 | out1 (atom []) 21 | out2 (atom []) 22 | id1 (subscribe! r :foo #(swap! out1 conj %)) 23 | id2 (subscribe! r :foo #(swap! out2 conj %))] 24 | 25 | (publish! r :foo 1) 26 | (publish! r :foo 2) 27 | (is (= @out1 @out2 [1 2])))) 28 | 29 | (deftest unsub 30 | (let [r (pubsub-registry) 31 | out1 (atom []) 32 | out2 (atom []) 33 | foo1 (subscribe! r :foo #(swap! out1 conj %)) 34 | foo2 (subscribe! r :foo #(swap! out2 conj %))] 35 | 36 | (publish! r :foo 1) 37 | 38 | (unsubscribe! r foo1) 39 | (publish! r :foo 2) 40 | 41 | (unsubscribe! r foo2) 42 | (publish! r :foo 3) 43 | 44 | (is (= @out1 [1])) 45 | (is (= @out2 [1 2])))) 46 | 47 | (deftest sweep-test 48 | (let [r (pubsub-registry) 49 | pers (atom []) 50 | temp (atom [])] 51 | (subscribe! r :foo #(swap! pers conj %) true) 52 | (subscribe! r :foo #(swap! temp conj %)) 53 | 54 | (publish! r :foo 1) 55 | (logging/suppress "riemann.pubsub" 56 | (sweep! r)) 57 | (publish! r :foo 2) 58 | 59 | (is (= @pers [1 2])) 60 | (is (= @temp [1])))) 61 | 62 | (deftest index-subscription-test 63 | (let [ps (pubsub-registry) 64 | i (wrap-index (index) ps) 65 | core (logging/suppress 66 | ["riemann.core" "riemann.pubsub"] 67 | (transition! (core) {:index i :pubsub ps})) 68 | l (atom nil) 69 | e1 (event {:host "a" :service "b" :metric 1}) 70 | e2 (event {:host "b" :service "a" :metric 2})] 71 | (subscribe! ps "index" (partial swap! l conj)) 72 | (i e1) 73 | (i e2) 74 | (is (= @l (list e2 e1))))) 75 | -------------------------------------------------------------------------------- /src/riemann/opsgenie.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.opsgenie 2 | "Forwards events to OpsGenie" 3 | (:require [clj-http.client :as client]) 4 | (:require [cheshire.core :as json])) 5 | 6 | (def ^:private alerts-url 7 | "https://api.opsgenie.com/v1/json/alert") 8 | 9 | (defn- post 10 | "Post to OpsGenie" 11 | [url body] 12 | (client/post url 13 | {:body body 14 | :socket-timeout 5000 15 | :conn-timeout 5000 16 | :content-type :json 17 | :accept :json 18 | :throw-entire-message? true})) 19 | 20 | (defn- message 21 | "Generate description based on event. 22 | Because service might be quite long and opsgenie limits message, it 23 | pulls more important info into beginning of the string" 24 | [event] 25 | (str (:host event) 26 | ": [" (:state event) "] " 27 | (:service event))) 28 | 29 | (defn- description 30 | "Generate message based on event" 31 | [event] 32 | (str 33 | "Host: " (:host event) 34 | " \nService: " (:service event) 35 | " \nState: " (:state event) 36 | " \nMetric: " (:metric event) 37 | " \nDescription: " (:description event))) 38 | 39 | (defn- api-alias 40 | "Generate OpsGenie alias based on event" 41 | [event] 42 | (hash (str (:host event) " " 43 | (:service event)))) 44 | 45 | (defn- create-alert 46 | "Create alert in OpsGenie" 47 | [api-key event recipients] 48 | (post alerts-url (json/generate-string 49 | {:message (message event) 50 | :description (description event) 51 | :apiKey api-key 52 | :alias (api-alias event) 53 | :recipients recipients}))) 54 | (defn- close-alert 55 | "Close alert in OpsGenie" 56 | [api-key event] 57 | (post (str alerts-url "/close") 58 | (json/generate-string 59 | {:apiKey api-key 60 | :alias (api-alias event)}))) 61 | 62 | (defn opsgenie 63 | "Creates an OpsGenie adapter. Takes your OG service key, and returns a map of 64 | functions which trigger and resolve events. clojure/hash from event host and service 65 | will be used as the alias. 66 | 67 | (let [og (opsgenie \"my-service-key\" \"recipient@example.com\")] 68 | (changed-state 69 | (where (state \"ok\") (:resolve og)) 70 | (where (state \"critical\") (:trigger og))))" 71 | [service-key recipients] 72 | {:trigger #(create-alert service-key % recipients) 73 | :resolve #(close-alert service-key %)}) 74 | -------------------------------------------------------------------------------- /src/riemann/xymon.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.xymon 2 | "Forwards events to Xymon" 3 | (:require [clojure.java.io :as io] 4 | [clojure.string :as s] 5 | [clojure.tools.logging :refer [error]] 6 | [clojure.math.numeric-tower :refer [ceil]]) 7 | (:import java.net.Socket)) 8 | 9 | (defn format-line 10 | "Formats an event as a Xymon status message: 11 | 12 | status[+LIFETIME][/group:GROUP] HOSTNAME.TESTNAME COLOR 13 | 14 | Note about fields mapping: 15 | - HOSTNAME results from the string conversion (\".\" -> \",\") of :host 16 | - TESTNAME results from the string conversion (#\"(\\.| )\" -> \"_\") of :service 17 | - COLOR is taken as is from :state 18 | - is taken \"as is\" from :description 19 | - GROUP is not handled 20 | - LIFETIME results from the rounding up to the nearest whole number of the division by 60 of :ttl. 21 | - No :ttl (i.e. :ttl nil) ends up with no LIFETIME set (defaults to Xymon server's default lifetime) 22 | - :ttl 0 becomes +0 LIFETIME and will end up as immediate purple 23 | 24 | " 25 | [{:keys [ttl host service state description] 26 | :or {host "" service "" description "" state "unknown"}}] 27 | (let [ttl-prefix (if ttl (str "+" (int (ceil (/ ttl 60)))) "") 28 | host (s/replace host "." ",") 29 | service (s/replace service #"(\.| )" "_")] 30 | (format "status%s %s.%s %s %s\n" 31 | ttl-prefix host service state description))) 32 | 33 | (defn send-line 34 | "Connects to Xymon server, sends line, then closes the connection. 35 | This is a blocking operation and should happen on a separate thread." 36 | [opts line] 37 | (try 38 | (with-open [sock (Socket. (:host opts) (:port opts)) 39 | writer (io/writer sock)] 40 | (.write writer line) 41 | (.flush writer)) 42 | (catch Exception e 43 | (error e "could not reach xymon host")))) 44 | 45 | (defn xymon 46 | "Returns a function which accepts an event and sends it to Xymon. 47 | Silently drops events when xymon is down. Attempts to reconnect 48 | automatically every five seconds. Use: 49 | 50 | (xymon {:host \"127.0.0.1\" :port 1984}) 51 | 52 | " 53 | [opts] 54 | (let [opts (merge {:host "127.0.0.1" 55 | :port 1984} opts)] 56 | (fn [{:keys [state service] :as event}] 57 | (when (and state service) 58 | (let [statusmessage (format-line event)] 59 | (send-line opts statusmessage)))))) 60 | -------------------------------------------------------------------------------- /test/riemann/xymon_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.xymon-test 2 | (:use riemann.xymon 3 | [riemann.time :only [unix-time]] 4 | clojure.test) 5 | (:require [riemann.logging :as logging])) 6 | 7 | (logging/init) 8 | 9 | (deftest ^:xymon-format format-line-test 10 | (let [pairs [[{} 11 | "status . unknown \n"] 12 | [{:host "foo" :service "bar"} 13 | "status foo.bar unknown \n"] 14 | [{:host "foo" :service "bar" :state "ok"} 15 | "status foo.bar ok \n"] 16 | [{:host "foo" :service "bar" :state "ok" :description "blah"} 17 | "status foo.bar ok blah\n"] 18 | [{:host "foo" :service "bar" :state "ok" :ttl 300} 19 | "status+5 foo.bar ok \n"] 20 | [{:host "foo" :service "bar" :state "ok" :ttl 61} 21 | "status+2 foo.bar ok \n"] 22 | [{:host "foo" :service "bar" :state "ok" :ttl 59} 23 | "status+1 foo.bar ok \n"] 24 | [{:host "foo" :service "bar" :state "ok" :ttl 1} 25 | "status+1 foo.bar ok \n"] 26 | [{:host "foo" :service "bar" :state "ok" :ttl 0} 27 | "status+0 foo.bar ok \n"] 28 | [{:host "example.com" :service "some.metric rate" :state "ok"} 29 | "status example,com.some_metric_rate ok \n"]]] 30 | (doseq [[event line] pairs] 31 | (is (= line (format-line event)))))) 32 | 33 | (deftest ^:xymon ^:integration xymon-test 34 | (let [k (xymon nil)] 35 | (k {:host "riemann.local" 36 | :service "xymon test" 37 | :state "green" 38 | :description "all clear, uh, situation normal" 39 | :metric -2 40 | :time (unix-time)})) 41 | 42 | (let [k (xymon nil)] 43 | (k {:service "xymon test" 44 | :state "green" 45 | :description "all clear, uh, situation normal" 46 | :metric 3.14159 47 | :time (unix-time)})) 48 | 49 | (let [k (xymon nil)] 50 | (k {:host "riemann.local" 51 | :service "xymon test" 52 | :state "ok" 53 | :description "all clear, uh, situation normal" 54 | :metric 3.14159 55 | :time (unix-time)})) 56 | 57 | (let [k (xymon nil)] 58 | (k {:host "no-service.riemann.local" 59 | :state "ok" 60 | :description "Missing service, not transmitted" 61 | :metric 4 62 | :time (unix-time)}))) 63 | -------------------------------------------------------------------------------- /test/riemann/shinken_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.shinken-test 2 | (:use [riemann.time :only [unix-time]] 3 | riemann.shinken 4 | clojure.test) 5 | (:require [clj-http.client :as http] 6 | [riemann.test-utils :refer [with-mock]] 7 | [riemann.logging :as logging])) 8 | 9 | (logging/init) 10 | 11 | (deftest ^:shinken shinken-test 12 | (with-mock [calls clj-http.client/post] 13 | (let [shkn (shinken {}) 14 | time (unix-time)] 15 | 16 | (testing "an event with unix time" 17 | (shkn {:host "testhost" 18 | :service "testservice" 19 | :description "testdsecription" 20 | :metric 42 21 | :time (/ 351406934039 250) 22 | :state "ok"}) 23 | (is (= (last @calls) 24 | ["http://127.0.0.1:7760/push_check_result" 25 | {:basic-auth ["admin" "admin"] 26 | :form-params {:time_stamp 1405627736, 27 | :host_name "testhost" 28 | :service_description "testservice" 29 | :return_code "ok" 30 | :output 42}}]))) 31 | 32 | (testing "sending to another port" 33 | ((shinken {:host "verne" :port 7761}) 34 | {:host "testhost" 35 | :service "testservice" 36 | :description "testdsecription" 37 | :metric 42 38 | :time 1405458798 39 | :state "ok"}) 40 | (is (= (last @calls) 41 | ["http://127.0.0.1:7761/push_check_result" 42 | {:basic-auth ["admin" "admin"] 43 | :form-params {:time_stamp 1405458798 44 | :host_name "testhost" 45 | :service_description "testservice" 46 | :return_code "ok" 47 | :output 42}}]))) 48 | 49 | (testing "a string as a metric and an integer as the state" 50 | (shkn {:host "testhost" 51 | :service "testservice" 52 | :description "testdsecription" 53 | :metric "stringmetric" 54 | :time 1405458798 55 | :state 0}) 56 | (is (= (last @calls) 57 | ["http://127.0.0.1:7760/push_check_result" 58 | {:basic-auth ["admin" "admin"] 59 | :form-params {:time_stamp 1405458798 60 | :host_name "testhost" 61 | :service_description "testservice" 62 | :return_code 0 63 | :output "stringmetric"}}])))))) 64 | -------------------------------------------------------------------------------- /test/riemann/logentries_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.logentries-test 2 | (:import 3 | (java.net Socket 4 | ServerSocket) 5 | (java.io BufferedReader 6 | InputStreamReader)) 7 | (:use [riemann.logentries :only [logentries event-to-le-format]] 8 | riemann.time 9 | clojure.test) 10 | (:require [riemann.logging :as logging])) 11 | 12 | (logging/init) 13 | 14 | (def host "localhost") 15 | (def port 8273) 16 | (def token "my-token-123") 17 | 18 | (defn test-event [opts] 19 | (merge {:host "localhost" 20 | :service "logentries test good" 21 | :description "Testing a log entry with ok state" 22 | :state "ok" 23 | :time (unix-time)} 24 | opts)) 25 | 26 | (defprotocol LogentriesStub 27 | (open [client] 28 | "Creates a Logentries stub") 29 | (close [client] 30 | "Cleans up (closes sockets etc.)")) 31 | 32 | (defrecord DefaultLogentriesStub [^int port] 33 | LogentriesStub 34 | (open [this] 35 | (let [server-socket (ServerSocket. port) 36 | client-socket (promise) 37 | input (promise)] 38 | (future 39 | (let [socket (.accept server-socket) 40 | in (BufferedReader. (InputStreamReader. (.getInputStream socket)))] 41 | (deliver client-socket socket) 42 | (deliver input in))) 43 | (assoc this 44 | :server-socket server-socket 45 | :client-socket client-socket 46 | :input input))) 47 | (close [this] 48 | (.close ^BufferedReader @(:input this)) 49 | (.close ^Socket @(:client-socket this)) 50 | (.close ^ServerSocket (:server-socket this)))) 51 | 52 | (deftest event-to-le-format-test 53 | (let [message (event-to-le-format {:description "New user" :service "production front" :state "ok"})] 54 | (is (= message 55 | "New user, service='production front' state='ok'")))) 56 | 57 | (deftest test-logentries 58 | (logging/suppress ["riemann.logentries"] 59 | (let [le-stub (open (DefaultLogentriesStub. port)) 60 | le (logentries {:host host 61 | :port port 62 | :token token 63 | :pool-size 1 64 | :claim-timeout 1})] 65 | (le (test-event {:description "Test event 1"})) 66 | (le (test-event {:description "Test event 2"})) 67 | (let [in @(:input le-stub) 68 | line-1 (.readLine in) 69 | line-2 (.readLine in)] 70 | (is (.startsWith line-1 "Test event 1")) 71 | (is (.startsWith line-2 "Test event 2")) 72 | (is (.endsWith line-1 token)) 73 | (is (.endsWith line-2 token))) 74 | (close le-stub)))) 75 | -------------------------------------------------------------------------------- /src/riemann/deps.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.deps 2 | "Riemann's dependency resolution system expresses stateful relationships 3 | between events. Dependencies are expressed as Rules; a Rule is a statement 4 | about the relationship between a particular event and the current state of 5 | the index. 6 | 7 | Maps are rules which specify that their keys and values should be present in 8 | some event in the index. {} will match any non-empty index. {:service \"a\" 9 | :state \"ok\"} will match an index which has {:service \"a\" :state \"ok\" 10 | :metric 123}, and so on. 11 | 12 | (all & rules) matches only if all rules match. 13 | 14 | (any & rules) matches if any of the rules match. 15 | 16 | (localhost & rules) states that all child rules must have the same host as 17 | the event of interest. 18 | 19 | (depends a & bs) means that if a matches the current event (and only the 20 | current event, not the full index), b must match the current event and index. 21 | " 22 | (:use riemann.index) 23 | (:require [riemann.streams :as streams])) 24 | 25 | (defprotocol Rule 26 | (match [this context event])) 27 | 28 | (extend-protocol Rule 29 | clojure.lang.IPersistentMap 30 | (match [this index _] 31 | (some (fn [e] (= this (select-keys e (keys this)))) 32 | index) 33 | )) 34 | 35 | (defrecord All [rules] 36 | Rule 37 | (match [this index event] 38 | ; (prn "Matching all" rules) 39 | ; (prn "index are" index) 40 | ; (prn "event is" event) 41 | (every? #(match % index event) rules))) 42 | 43 | (defn all [& rules] 44 | (All. rules)) 45 | 46 | (defrecord Any [rules] 47 | Rule 48 | (match [this index event] 49 | (some #(match % index event) rules))) 50 | 51 | (defn any [& rules] 52 | (Any. rules)) 53 | 54 | (defrecord Localhost [rule] 55 | Rule 56 | (match [this index event] 57 | (match rule 58 | (filter (fn [e] (= (:host event) (:host e))) index) 59 | event))) 60 | 61 | (defn localhost [& rules] 62 | (Localhost. (apply all rules))) 63 | 64 | (defrecord Depends [a b] 65 | Rule 66 | (match [this index event] 67 | (if (match a [event] event) 68 | (match b index event) 69 | true))) 70 | 71 | (defn depends [a & bs] 72 | (Depends. a (All. bs))) 73 | 74 | (defn deps-tag [index rule & children] 75 | "Returns a stream which accepts events, checks whether they satisfy the given 76 | rule, and associates those which have their dependencies satisfied with 77 | {:deps-satisfied true}, and false for those which are satisfied." 78 | (fn [event] 79 | (streams/call-rescue 80 | (assoc event :deps-satisfied? (match rule index event)) 81 | children))) 82 | -------------------------------------------------------------------------------- /test/riemann/twilio_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.twilio-test 2 | (:require [riemann.twilio :as twilio] 3 | [riemann.common :refer [body]] 4 | [clj-http.client :as client] 5 | [clojure.test :refer [deftest testing is are]])) 6 | 7 | (defn- mock-post [result-atom sms event] 8 | (with-redefs [client/post (fn [url opts] 9 | (reset! result-atom 10 | (merge (:form-params opts) 11 | {:url url 12 | :basic-auth (:basic-auth opts)})))] 13 | (sms event))) 14 | 15 | (deftest twilio-test 16 | (let [result-atom (atom {}) 17 | account "testaccount" 18 | service-key "testkey" 19 | recipient "+15005550006" 20 | event {:service "testservice" 21 | :host "test host" 22 | :time 123 23 | :metric 17} 24 | default-body-result (body [event]) 25 | messenger (twilio/twilio {:account account 26 | :service-key service-key}) 27 | sms (messenger recipient)] 28 | 29 | (testing "ensure the data posted to twilio matches expectations" 30 | (mock-post result-atom sms event) 31 | (are [key result] (= result (key @result-atom)) 32 | :url (str "https://api.twilio.com/2010-04-01/Accounts/" account "/Messages.json") 33 | :basic-auth [account service-key] 34 | :from (str "+15005550006") 35 | :to (list recipient) 36 | :body default-body-result)) 37 | 38 | (testing "ensure message overrides are used" 39 | (let [body-formatter-result "this is the body" 40 | body-formatter (fn [_] body-formatter-result) 41 | from-override "+15005550006" 42 | messenger (twilio/twilio {:account account 43 | :service-key service-key} 44 | {:body body-formatter 45 | :from from-override}) 46 | sms (messenger recipient)] 47 | (mock-post result-atom sms event) 48 | (are [rkey result] (= result (rkey @result-atom)) 49 | :body body-formatter-result 50 | :from from-override))) 51 | 52 | (testing "ensure twilio options are split out when given only one map" 53 | (let [from-override "+15005550006" 54 | messenger (twilio/twilio {:account account 55 | :service-key service-key 56 | :from from-override}) 57 | sms (messenger recipient)] 58 | (mock-post result-atom sms event) 59 | (is (= (:from @result-atom) 60 | from-override)))))) 61 | -------------------------------------------------------------------------------- /src/riemann/hipchat.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:doc "Forwards events to HipChat" 2 | :author "Hubert Iwaniuk"} 3 | riemann.hipchat 4 | (:require [clj-http.client :as client] 5 | [clojure.string :refer [join]])) 6 | 7 | (defn- message-colour [ev] 8 | "Set the colour to be used in the 9 | hipchat message." 10 | (let [state (or (:state ev) (:state (first ev)))] 11 | (get {"ok" "green" 12 | "critical" "red" 13 | "error" "red"} 14 | state 15 | "yellow"))) 16 | 17 | (defn ^:private chat-url [server room] 18 | (str "https://" server "/v2/room/" room "/notification")) 19 | 20 | (defn- format-message [ev] 21 | "Formats a message, accepts a single 22 | event or a sequence of events." 23 | (join "\n\n" 24 | (map 25 | (fn [e] 26 | (str 27 | "Host: " (:host e) 28 | " \nService: " (:service e) 29 | " \nState: " (:state e) 30 | " \nMetric: " (:metric e) 31 | " \nDescription: " (:description e))) ev))) 32 | 33 | (defn- format-event [{:keys [message] :as conf} event] 34 | "Creates an event suitable for posting to hipchat." 35 | (merge {:color (message-colour event)} 36 | conf 37 | (when-not message 38 | {:message (format-message (flatten [event]))}))) 39 | 40 | (defn- post 41 | "POST to the HipChat API." 42 | [token {:keys [server room_id] :as conf} event] 43 | (client/post (str (chat-url server room_id) "?auth_token=" token) 44 | {:form-params (format-event (assoc conf :message_format "text") event) 45 | :socket-timeout 5000 46 | :conn-timeout 5000 47 | :accept :json 48 | :throw-entire-message? true})) 49 | 50 | (defn hipchat 51 | "Creates a HipChat adapter. Takes your HipChat v2 authentication token, 52 | and returns a function which posts a message to a HipChat. 53 | 54 | You can any a personal or room-specific token, which can be obtained from 55 | your profile page or a specific room. 56 | 57 | More on api tokens at https://www.hipchat.com/docs/apiv2/auth 58 | 59 | If you're using hosted HipChat, you can leave out :server (or set it to 60 | 'api.hipchat.com'). 61 | 62 | (let [hc (hipchat {:server \"...\" 63 | :token \"...\" 64 | :room 12345 65 | :notify 0})] 66 | (changed-state hc))" 67 | [{:keys [server token room notify]}] 68 | (if (not (= 40 (count token))) 69 | (throw (IllegalArgumentException. "This adapter now requires a v2 API key"))) 70 | (fn [e] (post token 71 | {:server (or server "api.hipchat.com") 72 | :room_id room 73 | :notify notify} 74 | e))) 75 | -------------------------------------------------------------------------------- /src/riemann/bin.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.bin 2 | "Main function." 3 | (:require [riemann.config :as config] 4 | riemann.logging 5 | riemann.time 6 | [riemann.test :as test] 7 | riemann.pubsub) 8 | (:use clojure.tools.logging) 9 | (:gen-class :name riemann.bin)) 10 | 11 | (def config-file 12 | "The configuration file loaded by the bin tool" 13 | (atom nil)) 14 | 15 | (def reload-lock (Object.)) 16 | 17 | (defn reload! 18 | "Reloads the given configuration file by clearing the task scheduler, shutting 19 | down the current core, and loading a new one." 20 | [] 21 | (locking reload-lock 22 | (try 23 | (riemann.config/validate-config @config-file) 24 | (riemann.time/reset-tasks!) 25 | (riemann.config/clear!) 26 | (riemann.pubsub/sweep! (:pubsub @riemann.config/core)) 27 | (riemann.config/include @config-file) 28 | (riemann.config/apply!) 29 | :reloaded 30 | (catch Exception e 31 | (error e "Couldn't reload:") 32 | e)))) 33 | 34 | (defn handle-signals 35 | "Sets up POSIX signal handlers." 36 | [] 37 | (if (not (.contains (. System getProperty "os.name") "Windows")) 38 | (sun.misc.Signal/handle 39 | (sun.misc.Signal. "HUP") 40 | (proxy [sun.misc.SignalHandler] [] 41 | (handle [sig] 42 | (info "Caught SIGHUP, reloading") 43 | (reload!)))))) 44 | (defn pid 45 | "Process identifier, such as it is on the JVM. :-/" 46 | [] 47 | (let [name (-> (java.lang.management.ManagementFactory/getRuntimeMXBean) 48 | (.getName))] 49 | (try 50 | (get (re-find #"^(\d+).*" name) 1) 51 | (catch Exception e name)))) 52 | 53 | (defn -main 54 | "Start Riemann. Loads a configuration file from the first of its args." 55 | ([] 56 | (-main "riemann.config")) 57 | ([config] 58 | (-main "start" config)) 59 | ([command config] 60 | (when (nil? (System/getProperty "log4j.configuration")) 61 | (riemann.logging/init)) 62 | (case command 63 | "start" (try 64 | (info "PID" (pid)) 65 | (reset! config-file config) 66 | (handle-signals) 67 | (riemann.time/start!) 68 | (riemann.config/include @config-file) 69 | (riemann.config/apply!) 70 | nil 71 | (catch Exception e 72 | (error e "Couldn't start"))) 73 | 74 | "test" (try 75 | (test/with-test-env 76 | (reset! config-file config) 77 | (riemann.config/include @config-file) 78 | (binding [test/*streams* (:streams @config/next-core)] 79 | (let [results (clojure.test/run-all-tests #".*-test")] 80 | (if (and (zero? (:error results)) 81 | (zero? (:fail results))) 82 | (System/exit 0) 83 | (System/exit 1))))))))) 84 | -------------------------------------------------------------------------------- /src/riemann/instrumentation.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.instrumentation 2 | "Tracks Riemann performance data" 3 | (:use [riemann.time :only [unix-time]] 4 | [riemann.common :only [event localhost]] 5 | [interval-metrics.core :only [Metric 6 | update! 7 | snapshot! 8 | rate 9 | quantile 10 | uniform-reservoir]])) 11 | 12 | (defprotocol Instrumented 13 | "These things can can be asked to report events about their performance and 14 | health." 15 | (events [this] 16 | "Returns a sequence of events describing the current state of the 17 | object.")) 18 | 19 | (defn nanos->millis 20 | "Convert nanoseconds to milliseconds." 21 | [nanos] 22 | (when nanos 23 | (* 1e-6 nanos))) 24 | 25 | (defrecord RateLatency [event quantiles rate latencies] 26 | Metric 27 | (update! [this time] 28 | (update! latencies time) 29 | (update! rate 1)) 30 | 31 | Instrumented 32 | (events [this] 33 | (let [rate (snapshot! rate) 34 | latencies (snapshot! latencies) 35 | t (unix-time)] 36 | (cons 37 | ; Rate 38 | (merge event {:service (str (:service event) " rate") 39 | :metric rate 40 | :time t}) 41 | ; Latencies 42 | (map (fn [q] 43 | (merge event {:service (str (:service event) " latency " q) 44 | :metric (-> latencies 45 | (quantile q) 46 | nanos->millis) 47 | :time t})) 48 | quantiles))))) 49 | 50 | (defn rate+latency 51 | "Returns a Metric which can be updated with latency measurements. When asked 52 | for events, returns a rate of total throughput, plus the given quantiles of 53 | latency metrics. Takes an optional base event which is used as the template. 54 | Input latencies are in nanoseconds (for storage as longs), emits latencies as 55 | doubles in milliseconds." 56 | ([event] (rate+latency event [0.0 0.5 0.95 0.99 0.999])) 57 | ([ev quantiles] 58 | (let [ev (merge ev {:service (str "riemann " (:service ev))})] 59 | (RateLatency. ev quantiles (rate) (uniform-reservoir))))) 60 | 61 | (defn instrumented? 62 | "Does a thingy provide instrumentation?" 63 | [thingy] 64 | (satisfies? Instrumented thingy)) 65 | 66 | (defmacro measure-latency 67 | "Wraps body in a macro which reports its running time in nanoseconds to a 68 | Metric." 69 | [metric & body] 70 | `(let [t0# (System/nanoTime) 71 | value# (do ~@body) 72 | t1# (System/nanoTime)] 73 | (update! ~metric (- t1# t0#)) 74 | value#)) 75 | -------------------------------------------------------------------------------- /src/riemann/blueflood.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.blueflood 2 | "Forwards events to Blueflood" 3 | (:require [clj-http.client :as client] 4 | [cheshire.core :as json] 5 | [clojure.string :as s] 6 | [clojure.tools.logging :as logging] 7 | [riemann.streams :as streams] 8 | [riemann.config :as config])) 9 | 10 | (def version "1.0") 11 | (def url-template "http://%s:%s/v2.0/%s/ingest") 12 | (def defaults 13 | {:ttl 2592000 14 | :host "localhost" 15 | :port "19000" 16 | :tenant-id "tenant-id" 17 | :n 100 18 | :dt 1}) 19 | 20 | (defn- prep-event-for-bf [ev] 21 | {:collectionTime (:time ev) 22 | :ttlInSeconds (or (:ttl ev) (defaults :ttl)) 23 | :metricValue (:metric ev) 24 | :metricName (s/join "." [(:host ev) (:service ev)])}) 25 | 26 | (defn- bf-body [evs] 27 | (->> evs 28 | (map prep-event-for-bf) 29 | json/generate-string)) 30 | 31 | (defn log-bf-body [evs] 32 | (let [r (bf-body evs)] 33 | (logging/info "bf-body" r) 34 | r)) 35 | 36 | (defn blueflood-ingest-synchronous [url & children] 37 | (fn [evs] 38 | (client/post 39 | url 40 | {:body (bf-body evs) 41 | :content-type :json 42 | :accept :json 43 | :socket-timeout 5000 44 | :conn-timeout 5000 45 | :throw-entire-message? true}) 46 | (streams/call-rescue evs children))) 47 | 48 | (defn blueflood-ingest [opts & children] 49 | "A stream which creates a batch, optionally asynchronous, of events to 50 | forward to BF 51 | 52 | Options: 53 | Parameters to Blueflood server 54 | :host BF hostname 55 | :port BF port 56 | :tenant-id BF tenant for this batch of metrics 57 | 58 | Parameters to riemann.streams/batch, (they pass through unchanged.) 59 | :n Max number of events in a batch 60 | :dt Max seconds in a batch 61 | 62 | Parameters to riemann.config/async-queue! (they pass through unchanged.) 63 | :async-queue-name Name of queue; if nil, stream is synchronous 64 | (i.e. async-queue! stream not used.) 65 | :threadpool-service-opts Options to riemann.service/threadpool-service 66 | Use: 67 | (blueflood-ingest {:host \"blueflood-server\" 68 | :tenant-id \"tenant\" 69 | :async-queue-name :bf-queue}) 70 | 71 | or for synchronous, just: 72 | (blueflood-ingest {:host \"blueflood-server\" 73 | :tenant-id \"tenant\"})" 74 | (let [opts (merge defaults opts) 75 | {:keys [n dt host port tenant-id 76 | async-queue-name threadpool-service-opts]} opts 77 | url (format url-template host port tenant-id) 78 | bf-stream (apply blueflood-ingest-synchronous url children)] 79 | (streams/where 80 | ;; BF doesn't handle events with null metrics so drop them 81 | metric 82 | (streams/batch 83 | n dt 84 | (if async-queue-name 85 | (config/async-queue! async-queue-name 86 | threadpool-service-opts bf-stream) 87 | bf-stream))))) 88 | -------------------------------------------------------------------------------- /src/riemann/plugin.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.plugin 2 | "Simple plugin loader for riemann. 3 | 4 | Riemann already offers the ability to load jar files in the classpath 5 | through its initialization scripts. This namespace allows walking the 6 | classpath, looking for small meta files distributed with plugins which 7 | hint at the requires. 8 | 9 | This allows `load-plugins` or `load-plugin` to make new functions and 10 | streams available in the config file without resorting to manual 11 | requires. 12 | 13 | The meta file distributed with plugins is expected to be an EDN file 14 | containing at least two keys: 15 | 16 | - `plugin`: the plugin name 17 | - `require`: the namespace to load 18 | 19 | The namespace will be made available using the plugin name's symbol 20 | value. 21 | " 22 | (:require [clojure.java.classpath :as cp] 23 | [clojure.java.io :refer [resource]] 24 | [clojure.tools.logging :refer [info]])) 25 | 26 | (def repo 27 | "Default location of local maven repository" 28 | (atom "/etc/riemann/plugins")) 29 | 30 | (defn read-safely 31 | "Read data without evaluation" 32 | [src] 33 | (binding [*read-eval* false] 34 | (read-string src))) 35 | 36 | (defn load-from-meta 37 | "Given a metadata description map, require plugin's namespace" 38 | [{:keys [plugin require]} {:keys [alias]}] 39 | (let [to-symbol (comp symbol name)] 40 | (when require 41 | (info "loading plugin:" plugin) 42 | (if alias 43 | (clojure.core/require [(to-symbol require) :as (to-symbol plugin)]) 44 | (clojure.core/require [(to-symbol require)]))))) 45 | 46 | (defn load-from-resource 47 | "Given a path to a java resource, load metadata and require plugin" 48 | [src options] 49 | (-> src 50 | resource 51 | slurp 52 | read-safely 53 | (load-from-meta options))) 54 | 55 | (defn load-plugins 56 | "Walk classpath and try to load all plugins that were found. 57 | Optionally accepts an option map containing the :alias keyword which determines 58 | whether to create named aliases for loaded plugins" 59 | ([options] 60 | (info "walking classpath to find plugins") 61 | (let [plugin-desc? (partial re-matches #"riemann_plugin/(.*)/meta.edn$") 62 | files (mapcat cp/filenames-in-jar (cp/classpath-jarfiles))] 63 | (doseq [desc-file (filter plugin-desc? files)] 64 | (load-from-resource desc-file options)))) 65 | ([] 66 | (load-plugins {:alias true}))) 67 | 68 | (defn load-plugin 69 | "Given a plugin name, look for its metadata description file, and 70 | require plugin's namespace. 71 | Optionally accepts an option map containing the :alias keyword which determines 72 | whether to create named aliases for the loaded plugin" 73 | ([plugin] 74 | (load-from-resource (format "riemann_plugin/%s/meta.edn" (name plugin)) 75 | {:alias true})) 76 | ([plugin options] 77 | (load-from-resource (format "riemann_plugin/%s/meta.edn" (name plugin)) 78 | options))) 79 | -------------------------------------------------------------------------------- /resources/query.g4: -------------------------------------------------------------------------------- 1 | grammar Query; 2 | 3 | // Kind of a simplified, restructured variant of the Clojure/Java lexers with a 4 | // way different structure. 5 | // 6 | // https://github.com/antlr/grammars-v4/blob/master/clojure/Clojure.g4 7 | 8 | predicate 9 | : primary 10 | | 'not' predicate 11 | | predicate 'and' predicate 12 | | predicate 'or' predicate 13 | ; 14 | 15 | primary 16 | : '(' predicate ')' 17 | | simple 18 | ; 19 | 20 | simple 21 | : tagged 22 | | equal 23 | | not_equal 24 | | lesser 25 | | greater 26 | | lesser_equal 27 | | greater_equal 28 | | like 29 | | regex_match 30 | | field 31 | | value 32 | ; 33 | 34 | tagged : 'tagged' string ; 35 | equal : value '=' value ; 36 | not_equal : value '!=' value ; 37 | lesser : value '<' number ; 38 | greater : value '>' number ; 39 | lesser_equal : value '<=' number ; 40 | greater_equal : value '>=' number ; 41 | like : value '=~' string ; 42 | regex_match : value '~=' string ; 43 | 44 | // Values ----------------------------------------------------------- 45 | 46 | value 47 | : true 48 | | false 49 | | nil 50 | | number 51 | | string 52 | | field 53 | ; 54 | 55 | field : NAME ; 56 | string : STRING ; 57 | true : TRUE ; 58 | false : FALSE ; 59 | nil : NIL ; 60 | 61 | number 62 | : float 63 | | bign 64 | | long 65 | ; 66 | 67 | float : FLOAT; 68 | bign : BIGN; 69 | long : LONG; 70 | 71 | 72 | // Lexers ------------------------------------------------------------- 73 | 74 | STRING : '"' ( ~'"' | '\\' '"' )* '"' ; 75 | 76 | FLOAT 77 | : '-'? [0-9]+ FLOAT_TAIL 78 | | '-'? 'Infinity' 79 | | '-'? 'NaN' 80 | ; 81 | 82 | fragment 83 | FLOAT_TAIL 84 | : FLOAT_DECIMAL FLOAT_EXP 85 | | FLOAT_DECIMAL 86 | | FLOAT_EXP 87 | ; 88 | 89 | fragment 90 | FLOAT_DECIMAL 91 | : '.' [0-9]+ 92 | ; 93 | 94 | fragment 95 | FLOAT_EXP 96 | : [eE] '-'? [0-9]+ 97 | ; 98 | fragment 99 | HEXD: [0-9a-fA-F] ; 100 | HEX: '0' [xX] HEXD+ ; 101 | LONG: '-'? [0-9]+[lL]?; 102 | BIGN: '-'? [0-9]+[nN]; 103 | 104 | CHAR_U 105 | : '\\' 'u'[0-9D-Fd-f] HEXD HEXD HEXD ; 106 | CHAR_NAMED 107 | : '\\' ( 'newline' 108 | | 'return' 109 | | 'space' 110 | | 'tab' 111 | | 'formfeed' 112 | | 'backspace' ) ; 113 | CHAR_ANY 114 | : '\\' . ; 115 | 116 | NIL : 'nil' 117 | | 'null' ; 118 | 119 | TRUE : 'true' ; 120 | FALSE : 'false' ; 121 | 122 | // Names 123 | 124 | NAME: SYMBOL_HEAD SYMBOL_REST* (':' SYMBOL_REST+)* ; 125 | 126 | fragment 127 | SYMBOL_HEAD 128 | : ~('0' .. '9' 129 | | '^' | '`' | '\'' | '"' | '#' | '~' | '@' | ':' | '/' | '%' | '(' | ')' | '[' | ']' | '{' | '}' // FIXME: could be one group 130 | | [ \n\r\t\,] // FIXME: could be WS 131 | ) 132 | ; 133 | 134 | fragment 135 | SYMBOL_REST 136 | : SYMBOL_HEAD 137 | | '0'..'9' 138 | | '.' 139 | ; 140 | 141 | 142 | // Whitespace 143 | 144 | WS : [ \n\r\t\,] -> channel(HIDDEN) ; 145 | -------------------------------------------------------------------------------- /test/riemann/mailgun_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.mailgun-test 2 | (:require [riemann.mailgun :as mailgun] 3 | [riemann.common :refer [body]] 4 | [clj-http.client :as client] 5 | [clojure.test :refer [deftest testing is are]])) 6 | 7 | (defn- mock-post [result-atom email event] 8 | (with-redefs [client/post (fn [url opts] 9 | (reset! result-atom 10 | (merge (:form-params opts) 11 | {:url url 12 | :basic-auth (:basic-auth opts)})))] 13 | (email event))) 14 | 15 | (deftest mailgun-test 16 | (let [result-atom (atom {}) 17 | sandbox "mail.relay" 18 | service-key "testkey" 19 | recipient "somedude@somewhere.com" 20 | event {:service "testservice" 21 | :host "test host" 22 | :time 123 23 | :metric 17} 24 | default-body-result (body [event]) 25 | default-subject-result "test host testservice" 26 | mailer (mailgun/mailgun {:sandbox sandbox 27 | :service-key service-key}) 28 | email (mailer recipient)] 29 | 30 | (testing "ensure the data posted to mailgun matches expectations" 31 | (mock-post result-atom email event) 32 | (are [key result] (= result (key @result-atom)) 33 | :url (str "https://api.mailgun.net/v2/" sandbox "/messages") 34 | :basic-auth ["api" service-key] 35 | :from (str "Riemann ") 36 | :to (list recipient) 37 | :subject default-subject-result 38 | :text default-body-result)) 39 | 40 | (testing "ensure message overrides are used" 41 | (let [body-formatter-result "this is the body" 42 | body-formatter (fn [_] body-formatter-result) 43 | subject-formatter-result "this is the subject" 44 | subject-formatter (fn [_] subject-formatter-result) 45 | from-override "my-override@xanadu" 46 | mailer (mailgun/mailgun {:sandbox sandbox 47 | :service-key service-key} 48 | {:subject subject-formatter 49 | :body body-formatter 50 | :from from-override}) 51 | email (mailer recipient)] 52 | (mock-post result-atom email event) 53 | (are [rkey result] (= result (rkey @result-atom)) 54 | :subject subject-formatter-result 55 | :text body-formatter-result 56 | :from from-override))) 57 | 58 | (testing "ensure mailgun options are split out when given only one map" 59 | (let [from-override "my-override@xanadu" 60 | mailer (mailgun/mailgun {:sandbox sandbox 61 | :service-key service-key 62 | :from from-override}) 63 | email (mailer recipient)] 64 | (mock-post result-atom email event) 65 | (is (= (:from @result-atom) 66 | from-override)))))) 67 | -------------------------------------------------------------------------------- /src/riemann/twilio.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.twilio 2 | "Forwards events to Twilio" 3 | (:require [clj-http.client :as client] 4 | [riemann.common :refer [body]])) 5 | 6 | (def ^:private messages-url 7 | "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json") 8 | 9 | (defn- post 10 | "POST to the Twilio Messages API." 11 | [twilio-opts msg-opts] 12 | (client/post (format messages-url (:account twilio-opts)) 13 | {:basic-auth [(:account twilio-opts) (:service-key twilio-opts)] 14 | :form-params 15 | {:from (:from msg-opts) 16 | :to (:to msg-opts) 17 | :body (:body msg-opts)} 18 | :socket-timeout 5000 19 | :conn-timeout 5000 20 | :accept :json 21 | :throw-entire-message? true})) 22 | 23 | 24 | (defn twilio-message 25 | "Send a message via Twilio" 26 | [twilio-opts msg-opts events] 27 | (let [events (flatten [events]) 28 | body ((get msg-opts :body body) events)] 29 | (post 30 | twilio-opts 31 | (merge msg-opts {:body body})))) 32 | 33 | 34 | (defn twilio 35 | "Returns a messenger, which is a function invoked with a phone number or a sequence 36 | of phone numbers and returns a stream. That stream is a function which takes a 37 | single event, or a sequence of events, and sends a message about them. 38 | 39 | (def messenger (twilio)) 40 | (def text (messenger \"+15005550006\" \"+15005550006\")) 41 | 42 | This messenger sends sms out via twilio using the twilio http api. When used 43 | it outputs the http response recieved from twilio. 44 | 45 | (changed :state 46 | #(info \"twilio response\" (text %))) 47 | 48 | The first argument is a map of the twilio options :account and :key. 49 | The second argument is a map of default message option (:from). 50 | 51 | (def text (twilio {:account \"id\" :service-key \"key\"} 52 | {:from \"+15005550006\"})) 53 | 54 | If you provide a single map, the messenger will split the twilio options out 55 | for you. 56 | 57 | (def text (twilio {:account \"id\" 58 | :service-key \"key\" 59 | :from \"+15005550006\"})) 60 | 61 | By default, riemann uses (body events) to format messages. 62 | You can set your own body formatter functions by including :body in msg-opts. 63 | These formatting functions take a sequence of 64 | events and return a string. 65 | 66 | (def text (twilio {} {:body (fn [events] 67 | (apply prn-str events))}))" 68 | ([] (twilio {})) 69 | ([opts] 70 | (let [twilio-keys #{:account :service-key} 71 | twilio-opts (select-keys opts twilio-keys) 72 | msg-opts (select-keys opts (remove twilio-keys (keys opts)))] 73 | (twilio twilio-opts msg-opts))) 74 | ([twilio-opts msg-opts] 75 | (let [msg-opts (merge {:from "+15005550006"} msg-opts)] 76 | (fn make-stream [& recipients] 77 | (fn stream [event] 78 | (let [msg-opts (if (empty? recipients) 79 | msg-opts 80 | (merge msg-opts {:to recipients}))] 81 | (twilio-message twilio-opts msg-opts event))))))) 82 | 83 | -------------------------------------------------------------------------------- /test/riemann/transport/opentsdb_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.transport.opentsdb-test 2 | (:use clojure.test 3 | [riemann.common :only [event]] 4 | riemann.transport.opentsdb 5 | [slingshot.slingshot :only [try+]]) 6 | (:require [riemann.logging :as logging] 7 | [riemann.core :as core] 8 | [riemann.opentsdb :as client] 9 | [riemann.pubsub :as pubsub] 10 | [clojure.pprint :refer [pprint]])) 11 | 12 | (deftest decode-opentsdb-line-success-test 13 | (is (= (event {:service "name" :description "name" :metric 456.0 :time 123}) 14 | (decode-opentsdb-line "put name 123 456"))) 15 | (is (= (event {:host "host" :service "name" :description "name" :metric 456.0 :time 123}) 16 | (decode-opentsdb-line "put name 123 456 host=host"))) 17 | (is (= (event {:service "name tag=value" :description "name" :metric 456.0 :tag "value" :time 123}) 18 | (decode-opentsdb-line "put name 123 456 tag=value"))) 19 | (is (= (event {:service "name tag=value tag2=value2" :description "name" :metric 456.0 :tag "value" :tag2 "value2" :time 123}) 20 | (decode-opentsdb-line "put name 123 456 tag=value tag2=value2"))) 21 | (is (= (event {:service "name service=value" :description "name" :metric 456.0 :servicetag "value" :time 123}) 22 | (decode-opentsdb-line "put name 123 456 service=value"))) 23 | ) 24 | 25 | (deftest decode-opentsdb-line-failure-test 26 | (let [err #(try+ (decode-opentsdb-line %) 27 | (catch Object e e))] 28 | (is (= (err "") "blank line")) 29 | (is (= (err "version") "version request")) 30 | (is (= (err "put") "no metric name")) 31 | (is (= (err "put name") "no timestamp")) 32 | (is (= (err "put name 123") "no metric")) 33 | (is (= (err "put name 123 NaN") "NaN metric")) 34 | (is (= (err "put name 123 metric") "invalid metric")) 35 | (is (= (err "put name timestamp 123") "invalid timestamp")))) 36 | 37 | (deftest round-trip-test 38 | (riemann.logging/suppress ["riemann.transport" 39 | "riemann.pubsub" 40 | "riemann.opentsdb" 41 | "riemann.core"] 42 | (let [server (opentsdb-server) 43 | sink (promise) 44 | core (core/transition! (core/core) 45 | {:services [server] 46 | :streams [(partial deliver sink)]})] 47 | (try 48 | ; Open a client and send an event 49 | (let [client (client/opentsdb {:pool-size 1 :block-start true})] 50 | (client {:host "computar" 51 | :service "hi there" :metric 2.5 :time 123 :ttl 10}) 52 | 53 | ; Verify event arrives 54 | (is (= (deref sink 1000 :timed-out) 55 | (event {:host "computar" 56 | :service "hi.there" 57 | :state nil 58 | :description "hi.there" 59 | :metric 2.5 60 | :tags nil 61 | :time 123 62 | :ttl nil})))) 63 | (finally 64 | (core/stop! core)))))) 65 | -------------------------------------------------------------------------------- /test/riemann/transport/graphite_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.transport.graphite-test 2 | (:use clojure.test 3 | [riemann.common :only [event]] 4 | riemann.transport.graphite 5 | [slingshot.slingshot :only [try+]]) 6 | (:require [riemann.logging :as logging] 7 | [riemann.core :as core] 8 | [riemann.graphite :as client])) 9 | 10 | (deftest decode-graphite-line-success-test 11 | (is (= (event {:service "name", :metric 123.0, :time 456}) 12 | (decode-graphite-line "name 123 456"))) 13 | (is (= (event {:service "name", :metric 456.0 :time 789}) 14 | (decode-graphite-line "name\t456\t789"))) 15 | (is (= (event {:service "name", :metric 456.0 :time 789}) 16 | (decode-graphite-line "name\t 456\t 789"))) 17 | (is (= (event {:service "name", :metric 456.0 :time 789}) 18 | (decode-graphite-line "name\t\t456\t\t789"))) 19 | (is (= (event {:service "name", :metric 456.0 :time 789}) 20 | (decode-graphite-line "name\t\t456 789"))) 21 | (is (= (event {:service "name", :metric 456.0 :time 789}) 22 | (decode-graphite-line "name 456\t789"))) 23 | (is (= (event {:service "name", :metric 456.0 :time 789}) 24 | (decode-graphite-line "name\t456 789"))) 25 | (is (= (event {:service "name", :metric 456.0 :time 789}) 26 | (decode-graphite-line "name 456 789")))) 27 | 28 | (deftest decode-graphite-line-failure-test 29 | (let [err #(try+ (decode-graphite-line %) 30 | (catch Object e e))] 31 | (is (= (err "") "blank line")) 32 | (is (= (err "name nan 456") "NaN metric")) 33 | (is (= (err "name metric 456") "invalid metric")) 34 | (is (= (err "name 123 timestamp") "invalid timestamp")) 35 | (is (= (err "name with space 123 456") "too many fields")) 36 | (is (= (err "name\twith\ttab\t123\t456") "too many fields")) 37 | (is (= (err "name with space\tand\ttab 123\t456") "too many fields")) 38 | (is (= (err "name\t\t123\t456\t\t\t789") "too many fields")))) 39 | 40 | (deftest round-trip-test 41 | (riemann.logging/suppress ["riemann.transport" 42 | "riemann.pubsub" 43 | "riemann.graphite" 44 | "riemann.core"] 45 | (let [server (graphite-server) 46 | sink (promise) 47 | core (core/transition! (core/core) 48 | {:services [server] 49 | :streams [(partial deliver sink)]})] 50 | (try 51 | ; Open a client and send an event 52 | (let [client (client/graphite {:pool-size 1 :block-start true})] 53 | (client {:host "computar" 54 | :service "hi there" :metric 2.5 :time 123 :ttl 10}) 55 | 56 | ; Verify event arrives 57 | (is (= (deref sink 1000 :timed-out) 58 | (event {:host nil 59 | :service "computar.hi.there" 60 | :state nil 61 | :description nil 62 | :metric 2.5 63 | :tags nil 64 | :time 123 65 | :ttl nil})))) 66 | (finally 67 | (core/stop! core)))))) 68 | -------------------------------------------------------------------------------- /src/riemann/email.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.email 2 | "Send email about events. Create a mailer with (mailer opts), then create 3 | streams which send email with (your-mailer \"shodan@tau.ceti.five\"). Or 4 | simply call email-event directly." 5 | (:use [riemann.common :only [deprecated body subject human-uniq]]) 6 | (:use postal.core) 7 | (:use [clojure.string :only [join]])) 8 | 9 | (defn email-event 10 | "Send an event, or a sequence of events, with the given smtp and msg 11 | options." 12 | [smtp-opts msg-opts events] 13 | (let [events (flatten [events]) 14 | subject ((get msg-opts :subject subject) events) 15 | body ((get msg-opts :body body) events)] 16 | (send-message 17 | smtp-opts 18 | (merge msg-opts {:subject subject :body body})))) 19 | 20 | (defn mailer 21 | "Returns a mailer, which is a function invoked with an address or a sequence 22 | of addresses and returns a stream. That stream is a function which takes a 23 | single event, or a sequence of events, and sends email about them. 24 | 25 | (def email (mailer)) 26 | 27 | This mailer uses the local sendmail. 28 | 29 | (changed :state 30 | (email \"xerxes@trioptimum.org\" \"shodan@trioptimum.org\")) 31 | 32 | The first argument are SMTP options like :host, :port, :user, :pass, :tls, 33 | and :ssl. The second argument is a map of default message options, like :from 34 | or :subject. 35 | 36 | (def email (mailer {:host \"mail.relay\"} 37 | {:from \"riemann@trioptimum.com\"})) 38 | 39 | If you provide a single map, mailer will split the SMTP options out for you. 40 | 41 | (def email (mailer {:host \"mail.relay\" 42 | :user \"foo\" 43 | :pass \"bar\" 44 | :from \"riemann@trioptimum.com\"})) 45 | 46 | smtp-opts and msg-opts are passed to postal. For more documentation, see 47 | https://github.com/drewr/postal 48 | 49 | By default, riemann uses (subject events) and (body events) to format emails. 50 | You can set your own subject or body formatter functions by including 51 | :subject or :body in msg-opts. These formatting functions take a sequence of 52 | events and return a string. 53 | 54 | (def email (mailer {} {:body (fn [events] 55 | (apply prn-str events))}))" 56 | ([] (mailer {})) 57 | ([opts] 58 | (let [smtp-keys #{:host :port :user :pass :ssl :tls :sender} 59 | smtp-opts (select-keys opts smtp-keys) 60 | msg-opts (select-keys opts (remove smtp-keys (keys opts)))] 61 | (mailer smtp-opts msg-opts))) 62 | ([smtp-opts msg-opts] 63 | (let [msg-opts (merge {:from "riemann"} msg-opts)] 64 | (fn make-stream [& [head & tail :as args]] 65 | (let [recipients (if (and (sequential? head) (empty? tail)) head args)] 66 | (assert (every? string? recipients) 67 | (str "email was passed a recipient that wasn't a string: " 68 | (pr-str recipients) 69 | " if those are events, you'll wanna call (email \"someemail@example.com\")")) 70 | (fn stream [event] 71 | (let [msg-opts (if (empty? recipients) 72 | msg-opts 73 | (merge msg-opts {:to recipients}))] 74 | (email-event smtp-opts msg-opts event)))))))) 75 | -------------------------------------------------------------------------------- /src/riemann/logentries.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.logentries 2 | "Forwards events to Logentries." 3 | (:import 4 | (java.net Socket) 5 | (java.io OutputStreamWriter)) 6 | (:use clojure.tools.logging 7 | riemann.pool 8 | riemann.common)) 9 | 10 | (defn format-event-data [data] 11 | (apply str 12 | (map 13 | (fn [[k v]] (str " " (name k) "='" v "'")) 14 | data))) 15 | 16 | (defn event-to-le-format [data] 17 | (let [message (:description data)] 18 | (if message 19 | (str message "," (format-event-data (dissoc data :description))) 20 | (format-event-data data)))) 21 | 22 | (defprotocol LogentriesClient 23 | (open [client] 24 | "Creates a Logentries client") 25 | (send-line [client line] 26 | "Sends a formatted line to Logentries") 27 | (close [client] 28 | "Cleans up (closes sockets etc.)")) 29 | 30 | (defrecord LogentriesTokenClient [^String host ^int port ^String token] 31 | LogentriesClient 32 | (open [this] 33 | (let [sock (Socket. host port)] 34 | (assoc this 35 | :socket sock 36 | :out (OutputStreamWriter. (.getOutputStream sock))))) 37 | (send-line [this line] 38 | (let [out (:out this) 39 | line (str line " " token "\n")] 40 | (.write ^OutputStreamWriter out ^String line) 41 | (.flush ^OutputStreamWriter out))) 42 | (close [this] 43 | (.close ^OutputStreamWriter (:out this)) 44 | (.close ^Socket (:socket this)))) 45 | 46 | (defn logentries 47 | "Returns a function which accepts an event and sends it to Logentries. 48 | Silently drops events when Logentries is down. Attempts to reconnect 49 | automatically every five seconds. Use: 50 | 51 | (logentries {:token \"2bfbea1e-10c3-4419-bdad-7e6435882e1f\"}) 52 | 53 | Options: 54 | 55 | :pool-size The number of connections to keep open. Default 4. 56 | 57 | :reconnect-interval How many seconds to wait between attempts to connect. 58 | Default 5. 59 | 60 | :claim-timeout How many seconds to wait for a Logentries connection from 61 | the pool. Default 0.1. 62 | 63 | :block-start Wait for the pool's initial connections to open 64 | before returning." 65 | [opts] 66 | (let [host (get opts :host "data.logentries.com") 67 | port (get opts :port 80) 68 | token (get opts :token) 69 | claim-timeout (get opts :claim-timeout 0.1) 70 | pool-size (get opts :pool-size 4) 71 | block-start (get opts :block-start) 72 | reconnect-interval (get opts :reconnect-interval 5) 73 | pool (fixed-pool 74 | (fn [] 75 | (info "Connecting to " host port) 76 | (let [client (open (LogentriesTokenClient. host port token))] 77 | (info "Connected") 78 | client)) 79 | (fn [client] 80 | (info "Closing connection to " host port) 81 | (close client)) 82 | {:size pool-size 83 | :block-start block-start 84 | :regenerate-interval reconnect-interval})] 85 | 86 | (fn [event] 87 | (with-pool [client pool claim-timeout] 88 | (send-line client (event-to-le-format event)))))) 89 | -------------------------------------------------------------------------------- /test/data/tls/client.pkcs8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCb770cV7NNgLHN 3 | Sp7DK9TL6e5gVzllH75IiATwYT9pvhZekmkG79USFmciuEVfXDNvlsz9hJblmRvE 4 | gk4ocKhaUKGChhed4qm+qqJ/+nIad+9ULk42P95uBT2EbvMZ1PxclflCeSMF6GZr 5 | EMsN9HBRk0hfEJ7gq2CXHqiVD6eynnJrSNA4rzg7RlpgkN6elqO8EshDWECVTHDT 6 | IYb0lob1vUUOWNWM8S8QrHFVcbcculW4l2UXNfb0A1aIQWFZ3eB7PjOcBYirfPFk 7 | QlPlf2LFwIo+86kDpUplcyFE0I1KOxxhdws6HUcdSOYbyHhs1k5wyvGqVUH1gtz6 8 | gBRsk9Zgxb43iRJYlW2r8cThXTng8jVkhsOnluw+JDSydRD8bSDkNQzowVF91xae 9 | 1jhOmY3Gb1MeMxGChHinYttIAZnc/1MqJqqYUfU0WDq+TmZ/mDFrG7yOLRNPjCK4 10 | 9poFOKiygdnf3CwGS6bLLupvxxFB1kh2EFk4MuMvTE9PqGRmPEKKYiSVzpmUM38D 11 | W7+FUj9MHBULFfqoXUVqFUPHtxPc1/JuudN5FkgSsn/SruJfAlh2UVSpdUA3KliC 12 | j5ebNJjkCoWybOPgFNTIsEK3INWpeOwgEeFMdYmhJRU51gM7WpQJSsne+I7Qy0j6 13 | DiGpj6GHplk+YRh5CfUPN8Tpmt5YdQIDAQABAoICAQCAviJIMDAdIM+rSpxhTuLV 14 | qEHi0KDWWKrlf0d0nxWX3BTj58VGsOQdltl31OORo7Hjw4FjHgDnds1yJJMa2Ehm 15 | qINwG/2LoQO8I20edEuYhsTVn5V7PKgL9c+gc2nKPgpM9pVgyFqeGg3U/3xl/RZu 16 | g0cRFcvWeie5HNralp5B3odhBDDFXAT1C1S6vWDPVlfAg1FMKKTJnt7XsaqWbWDn 17 | qmhFPBqzzryo63Z2sPgZg84aJQSVcTx+ofLjc/dIFuOrXOOHQRphWzqBKDHUHKQT 18 | 2K9K4ij8lY8iyzcAdZ7xVnRME9j1Fg8Moo85CP5D/XutrCByHQnkBTMkrnIuYrBu 19 | 02h27Y1oedMuQ518wgJ8hATSSwZtidkg7aEMEgflKUs8ug6IGw5Vkb1oA0bvttSX 20 | bA7X4bKGxFy5dmXPJXjaJkx+Xvj29eRJngCdEjCgU4giI2ZBBpEec//BSW0BTLtY 21 | 1DAQDzQ3Wo0OsKvgM3KeHr2JyorgLgMme1aWLN4v2582guCkof3fDiOcq5uVFaG7 22 | ADBbHFRkUmQ5V+cEB6I6trlvuWMjV8XTHkA0q5SX4vPVQTiojuBiMm2MUTHZPU5t 23 | sM97wIcrF1OkYnPnTgoRhbuhWNco2+IkOBAhBhrtlzQQcy0kMyYXWBhuv3G15s4V 24 | xWXXb6irAamhmTIoLby70QKCAQEAyJgw2uxjWm83j8Rk5t7s7RroiJaMBWanQ85r 25 | ximqQEcmvqI5sIaXHnYVKl1h6NJ0OdXT8L1bagI8nQ95sU7nwL9LVJFvJ9bOX+Nw 26 | pX7FG2kOX0gATNt6yXWXbbvsPga772U9LFNLT4+DC1fOylkM9FQDngsLHSxChKW+ 27 | Eq1lzUc0FcUCFmQ23lx/EUY80pOEZR6GHhtyTO2pR8aCC1TBENPbgruVHuWl2pkA 28 | dZvZuUbWFHiGH1nH+D/0/4RBoXmguHocUYllFiHkoyNCvxLux04mzYugJYyz8hRA 29 | sgP7dPZKfu+iILYDIRHa+WrsFp0kqBaE/uOZnBnsPRHdpXt8+wKCAQEAxwHUicDx 30 | 7BPvJ915E713I+NmvZ6F8fhzn/zuTqtobO3ZEXRriIUtnUQn8FTJw+Hq7ni/ksSa 31 | g5M2ZF+CVSzs9ZIp+Y9vAOwd2sT6kLX7OsAyl5ZaOa+VmiDVQsbrm3lhXmNucCmM 32 | JqH1iqVNxtOQmzILuHn+aUqAKCzm3SyjArcyVKqi52ot+1xDTkbJqChN7Coc0fLG 33 | 0+mNr3QYAs9uBZcsWAdFUoDcq+dpuGO0qDnD3mVHp7Ck5FCdd0En9QEeuRFpof3m 34 | ja8bb2ZNStX6amV8kS/o0hClYw2SevBzm/OqvUDyfCmtcasLDwi8fHYpaBKhqdGU 35 | OocwNqfCxo+lTwKCAQAJXCCbdoBETgA3syBKLYi8n47OIMgz0FBpt44L1xHcofVK 36 | 2+L5O659e2ENcwIIjRljQXj8OLm1Ppl5cqlo/peNxN9M+ORI5ZsHmmM9NacY2lQT 37 | Wha5f1xBXj1Mn4Iser/2MbEjClfvRHEG7Hf3tSQHmov4qedA2znXWqx9zp17cKEb 38 | iu/H/7EXuxUqxy47XSrGVSBI81roAAbwFHEo+jFwCZKqjPouUOTmSFFfK4CDRqzi 39 | k6UefV/5U3KpIZC2aih3syrzGwgeekJi4lhQ9h8yKZFh8evtFJ2o5A0IwWrQUFW1 40 | ipT0Fcobluuyy6xsebSmKzbVdeVLBWKijtzaxeJVAoIBAQCDAzBx8+UQf1sdyo8w 41 | MLZjmQyFXvxdfSxZskEM54la1Qbnl9ZHTS3nb/w/GwKtg2iP0EyGigoS/vJ7B+mc 42 | u3PQg4ZAtm3klI1e/fjbFgMr/WYRJ7mEapS5oE3lpWFsvG8enaUSEDglItCskwDu 43 | GVAE6+CNdTrJBHP0fwMJrp4uZn6rAJ84bE2TQn1I5g9SNh5+kIbirMq6rfJKBMrd 44 | 5sNOyOQ3m6nGk/Ey9qlB69n+OfE500FmI0Oaz3urC+kLQc6BFfaN38JNmm8cWqQd 45 | 1VsDeoaUao8C5FSNLl01tGDX1YWdDEnW9fUqdOlV33F/GqdNzZ5CVygXk/JouO7g 46 | m6a/AoIBAQC4kx9/oP0/GUDNPbM6TUBKLB0AdCTR0X6L+QfavSxHwp5BV146dhTJ 47 | nJDeS73i5UD6WJqhEvAEUc66hg5JugpTMT4WZxQdyvPqJ6WX6giN7SP94wEiH35u 48 | 8GX6K3U/J+4vI5bCWT0zyTcA7e7Zo4bEGSq22d9ODYsvK5t6CA1P7rjkTBpWI7DD 49 | 9tyuVxd55OPnIcM8lf7VSGQ7uxt5CJjhG5k1w++TXx2K0bG9lEQL1woAINOh+04u 50 | vcWc9v0JllT386w/c9Zp8AoGgnaJxAGZoJA1EPalYuAa5943dHMkOpgzxeE3PvA4 51 | 487JsL8Q5ZeQNX8lMWbH1kr9g3dz3non 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/data/tls/server.pkcs8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCTnk/Tv/nXC86M 3 | CK4ma/f5zcKu+SL97AzM3PHnKrUvZRxvYe9XQNW33fnpMPbBieG0Odu3BrhFykwe 4 | ht7+o7wKM0U70Lz4f7psRPn/MwPEwuDUIRQryHb3Yi6Qsqhzy5kUbFZZQmlb9CbJ 5 | DTG1EldfknNewRbt87qB1i9gIuTSvaHRdidjXKVDv5ZEMba0LveOyUDUF1RKvuKz 6 | DF7LZhmpQJiy2OxZuxd2khjwIwQmpfpz93djyNOOIcVwOnE9l9zlAGjkZuQKsIH2 7 | lKOhd4cKmiJqjbZkN8S4c87kpFRNZrfPe9NanGiO97fxgppKhQikmsY7HcGPVMiO 8 | bRPeO1GncRbNAHTSz3qb6rzYItfHP6IovAcX+Yjd6/1+MeeIr5zby9yG/b046XLC 9 | sRFoS+lwzdIBrzdHZpdZXuODv2uZOimBseP5TIEgAgAEBsiXJfWESyASspPsmhfY 10 | 12nHJ2UYwI6zfqpsOUODH5WBIVmgHR8Vny0vWzRvRDBmOeyOKQFwo5eKyjBKUEAV 11 | WelEmOmamvJQGzkiSrEjWKtNE+NK6UJPLcd0q4iSJLw/HYYZVAVM4H5tTv1nf2hw 12 | Ia1FEmkvT6KWR7F22nahwmvCAMF/ugAsyDaHwPmbMTNATMG+PmAJtkjIbWhExFQ9 13 | pcE9J10SRxaq0H4fLk+gXIRcmA4vXQIDAQABAoICABQTdsXUmRTvDSMsVwrwqiP2 14 | IOENDv47qS3/aoLG4ydT9/8J8bQLJEXCUaeuOhDuQJzzNXd52XvcXGfWbeNxvzQf 15 | 7u2IoPqm3GK3KszTUlLjMhKHgSfHNq7EUpLpSVGeQe5uIXw4NP4FgXTcb4MxwGJG 16 | F5yuZADuodHgoEbR2A71LVnAWPEBe5RwLO6PWEq/DHoF0uRFW/hiZ5B0QTwmYZ0M 17 | JPhF2jcF49YDXpLiIEFlV1+IK1uNx3/QBfsSZ645xhf660kM2Ty+7p3rbymZiOsf 18 | NUkKhuCwWPfBAEVGOBTiwGLec0sidHHlLMYSvgsedqN0TTti+50Tqcb8/LO/m4YW 19 | UWxo5L00Xzsdo32Jzf+s+XHXrXWnPvUWtvzeqRfLkZ7KF/WLyJSp3dS8dhWhcAed 20 | rzFGRBYdBcM3NR0GR/VP092aR0kl5nboUiga8hz/m9zB3cRb0iFf7Q1mUzTUcGbF 21 | sdoyy0VkvOiZVu0/kBn/nN9hV70cn/OC6RB8nq5kMpQ/0HMA62TtGdnre7hgwFR8 22 | T+5QypfdE521R9Ft1v9e0UcBN8JbM3c7Cj+yexcTCGAarf1oKwC49OpiqvS3ImPD 23 | PAPfj0rAiMgSDgNbT1x10fYqGdvP210GNENvVIDcMtJCWLaj3jJ0g3ZB53uwZgRg 24 | G0tO1hlWI7TLTFPBpLl9AoIBAQDD7A9UAacwB7oj+PtSGRAeR+34tuipQR6UxvW/ 25 | wc2fdtSfOYMx4LawUzqOh2qvjVnPg8muu1nigcH1jGxTIA+VcWpsgy8oSG7xwJ/1 26 | a+sCj1BQngc7m3y9ndjYre2d1LRS7QcnbJJEy/PR8Ae+bHc5wXkim8Xat8l+0Uq/ 27 | BX1cNlL44k/KMUka4Ngg3vfzTnrMnDla9EOhM60Qn1L/UL+opwpYoCgO/F5Cj58E 28 | 21xIsrs+pY5r6PQ4UK2osJVyRLzisdBWUtBQ/x7K/f+o+eB335fBlnaE/EK4c8iq 29 | yeVIPe7U4EvOGM6NKkCLGmqoJFLOGm4LU1vY9tBa6MJO40F/AoIBAQDA4mXVFl3T 30 | bFkXfEgy5tOJhiagEAKaDL4dc2DZJVggDHR/tVI3DG7aApdgwZgQLUzMTk5c/NJX 31 | e4TxrK0542cu3gU6ubNyGSGwXxZ8i+v+1wqnvpz7kPlwPU0EUTM/I3F+eTT0BaMJ 32 | vc3pC+iy/lke6QdMQQjezvYNnqhtr4RLhCF5vtQxalf2tPH9c8VaMKxh10qhkVaA 33 | B8Pc3pq0QUh6k+vPA4YV51NR1QB3fBPszoeMlxDb4zlJ+bTezAxsjfI4jsyPpwVw 34 | lAPnmbiKFIvk+Cur5DozQMnZpeLkhN4VIi2S4ftMewYfGOfW2KNWtAeXGHWUJ8y3 35 | +jQwVc5yK0UjAoIBAQCPbf+CtnsY1G9W6m3CmoqMQIhcrjsBvaSPsmAyc8T+2tWp 36 | g0cieqoDx8p0kXpu7oIzQv2hJ5MUGX5PLvAWosAF3bPVoOwjB3QBE82Gs+ymQRjB 37 | DA+reZcGkcowRpRQGWmx08iK+hbSuqTSqnBg3bMi2xq9VWCxUB748mtQEMrHeRWh 38 | Erzq/s3QGY7f8Zt/yZJovG3Ywj3Ig7ZNFvaB5zGIXFFctFLfNa4j+FSoD5ctuXsO 39 | z9DF+xLfL2ESv8OIlf3Zz+b/az0KLtryLKS/pb7Iwy2sEWTO0oZd1pWvQoLSMlTo 40 | DxQv20VLXwVFUJ+IXJ8qN3scW3hAC+BYzVGCwygtAoIBAElK/hT9fcOj7SqucCTf 41 | b0xXrH+v9b0h0HAScp+wwA7VjMMmXEpMsCapS2pZxfWHsSIFM2PEMg1KA1duHRjd 42 | H01A0k3hWJ2njRSe3UWQSam5fvQEy1QQX6hVstlMHaQFTNAZMJT1O2GtPPwsKU0y 43 | txy2wa4pHDzF+dttCWU8h5HGcI35hFoOpcQ6N6XugvRlgGthSpugRXU6/iMEd1a9 44 | Y9QoNzefiCj0kMWMobPFczDNvdYfZSmY6yEAAMmUUbhCK+Nv561Ccx+3LtCnqebW 45 | Ld5gqv+TJsZo/Qp2LYRWNKllJlolAr+qE8ZnSVlrUo3UtPmmMq+MfA8AxSoEwQDD 46 | Dm8CggEBAIdUL1cJ/SrCMU/Y0txJd8tphDyBiuhwoDtTQBm3nmdMzJtSw7PLB/HV 47 | wETavMos+Ix0zcbwWn/3RXSPO4U3LcBTx9QbvxQxQtM58IboLrnZJpsIIcy0YlsG 48 | CoitVRgWD72gDJ11y941+xLMppzO89QtweMYbVrvcjxvMkVGhiW4l0ieGF6jLNkW 49 | kUeCMsHXPemnMcHHLHkCZjRE7VhkwZ/u0TlTBF2+qRFdvgpQvLpYm6ZS0Q5nOSJL 50 | NmoZSLDL6Opc7hEd7/wwGRNS2QEMnl6xk/ao3Hve/rtskPobiKjeOZ3sHp288Kl9 51 | UDAe82xwRB3BJzitCF3043Eqnm3ReMM= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tasks/leiningen/tar.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.tar 2 | (:use [clojure.java.shell :only [sh with-sh-dir]] 3 | [clojure.java.io :only [file delete-file writer copy]] 4 | [clojure.string :only [join capitalize trim-newline split trim]] 5 | [leiningen.uberjar :only [uberjar]])) 6 | 7 | (defn delete-file-recursively 8 | "Delete file f. If it's a directory, recursively delete all its contents. 9 | Raise an exception if any deletion fails unless silently is true." 10 | [f & [silently]] 11 | (System/gc) ; This sometimes helps release files for deletion on windows. 12 | (let [f (file f)] 13 | (if (.isDirectory f) 14 | (doseq [child (.listFiles f)] 15 | (delete-file-recursively child silently))) 16 | (delete-file f silently))) 17 | 18 | (defn tar-dir 19 | "Tar package working directory." 20 | [project] 21 | (file (:root project) "target" "tar" (str (:name project) "-" 22 | (:version project)))) 23 | 24 | (defn cleanup 25 | [project] 26 | ; Delete working dir. 27 | (when (.exists (file (:root project) "target" "tar")) 28 | (delete-file-recursively (file (:root project) "target" "tar")))) 29 | 30 | (defn reset 31 | [project] 32 | (cleanup project) 33 | (sh "rm" (str (:root project) "/target/*.tar.bz2"))) 34 | 35 | (defn make-tar-dir 36 | "Creates the tarball package structure in a new directory." 37 | [project] 38 | (let [dir (tar-dir project)] 39 | (.mkdirs dir) 40 | 41 | ; Jar 42 | (.mkdirs (file dir "lib")) 43 | (copy (file (:root project) "target" 44 | (str "riemann-" (:version project) "-standalone.jar")) 45 | (file dir "lib" "riemann.jar")) 46 | 47 | ; Binary 48 | (.mkdirs (file dir "bin")) 49 | (copy (file (:root project) "pkg" "tar" "riemann") 50 | (file dir "bin" "riemann")) 51 | (.setExecutable (file dir "bin" "riemann") true false) 52 | 53 | ; Config 54 | (.mkdirs (file dir "etc")) 55 | (copy (file (:root project) "pkg" "tar" "riemann.config") 56 | (file dir "etc" "riemann.config")) 57 | 58 | dir)) 59 | 60 | (defn write 61 | "Write string to file, plus newline" 62 | [file string] 63 | (with-open [w (writer file)] 64 | (.write w (str (trim-newline string) "\n")))) 65 | 66 | (defn md5 67 | "Computes the md5 checksum of a file. Returns a hex string." 68 | [file] 69 | (-> (->> file 70 | str 71 | (sh "md5sum") 72 | :out) 73 | (split #" ") 74 | first 75 | trim)) 76 | 77 | (defn compress 78 | "Convert given package directory to a .tar.bz2." 79 | [project tar-dir] 80 | (let [filename (str (:name project) 81 | "-" 82 | (:version project) 83 | ".tar.bz2") 84 | tarball (str (file (:root project) 85 | "target" 86 | filename))] 87 | (with-sh-dir (.getParent tar-dir) 88 | (print (:err (sh "tar" "cvjf" tarball (.getName tar-dir))))) 89 | 90 | (write (str tarball ".md5") 91 | (str (md5 tarball) " " filename)))) 92 | 93 | (defn tar 94 | ([project] (tar project true)) 95 | ([project uberjar?] 96 | (reset project) 97 | (when uberjar? (uberjar project)) 98 | (compress project (make-tar-dir project)) 99 | (cleanup project))) 100 | -------------------------------------------------------------------------------- /src/riemann/mailgun.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.mailgun 2 | "Forwards events to Mailgun" 3 | (:require [clj-http.client :as client] 4 | [riemann.common :refer [body subject]])) 5 | 6 | (def ^:private event-url 7 | "https://api.mailgun.net/v2/%s/messages") 8 | 9 | (defn- post 10 | "POST to the Mailgun events API." 11 | [mgun-opts msg-opts] 12 | (client/post (format event-url (:sandbox mgun-opts)) 13 | {:basic-auth ["api" (:service-key mgun-opts)] 14 | :form-params 15 | {:from (format (:from msg-opts) (:sandbox mgun-opts)) 16 | :to (:to msg-opts) 17 | :subject (:subject msg-opts) 18 | :text (:body msg-opts)} 19 | :socket-timeout 5000 20 | :conn-timeout 5000 21 | :accept :json 22 | :throw-entire-message? true})) 23 | 24 | 25 | (defn mailgun-event 26 | "Send an event, or a sequence of events, with the given smtp and msg 27 | options." 28 | [mgun-opts msg-opts events] 29 | ;TODO: error checking to ensure sandbox and service-key are set 30 | (let [events (flatten [events]) 31 | subject ((get msg-opts :subject subject) events) 32 | body ((get msg-opts :body body) events)] 33 | (post 34 | mgun-opts 35 | (merge msg-opts {:subject subject :body body})))) 36 | 37 | 38 | (defn mailgun 39 | "Returns a mailer, which is a function invoked with an address or a sequence 40 | of addresses and returns a stream. That stream is a function which takes a 41 | single event, or a sequence of events, and sends email about them. 42 | 43 | (def mailer (mailgun)) 44 | (def email (mailer \"xerxes@trioptimum.org\" \"shodan@trioptimum.org\")) 45 | 46 | This mailer sends email out via mailgun using the mailgun http api. When used 47 | it outputs the http response recieved from mailgun. 48 | 49 | (changed :state 50 | #(info \"mailgun response\" (email %))) 51 | 52 | The first argument is a map of the mailgun options :sandbox and :service-key. 53 | The second argument is a map of default message options, like :from, 54 | :subject, or :body. 55 | 56 | (def email (mailgun {:sandbox \"mail.relay\" :service-key \"key\"} 57 | {:from \"riemann@trioptimum.com\"})) 58 | 59 | If you provide a single map, the mailer will split the mailgun options out 60 | for you. 61 | 62 | (def email (mailgun {:sandbox \"mail.relay\" 63 | :service-key \"foo\" 64 | :from \"riemann@trioptimum.com\"})) 65 | 66 | By default, riemann uses (subject events) and (body events) to format emails. 67 | You can set your own subject or body formatter functions by including 68 | :subject or :body in msg-opts. These formatting functions take a sequence of 69 | events and return a string. 70 | 71 | (def email (mailgun {} {:body (fn [events] 72 | (apply prn-str events))}))" 73 | ([] (mailgun {})) 74 | ([opts] 75 | (let [mg-keys #{:sandbox :service-key} 76 | mgun-opts (select-keys opts mg-keys) 77 | msg-opts (select-keys opts (remove mg-keys (keys opts)))] 78 | (mailgun mgun-opts msg-opts))) 79 | ([mgun-opts msg-opts] 80 | (let [msg-opts (merge {:from "Riemann "} msg-opts)] 81 | (fn make-stream [& recipients] 82 | (fn stream [event] 83 | (let [msg-opts (if (empty? recipients) 84 | msg-opts 85 | (merge msg-opts {:to recipients}))] 86 | (mailgun-event mgun-opts msg-opts event))))))) 87 | 88 | -------------------------------------------------------------------------------- /src/riemann/transport/debug.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.transport.debug 2 | "It is very dark. You are likely to be eaten a grue." 3 | (:require [clojure.tools.logging :refer :all] 4 | [riemann.transport :refer [retain]]) 5 | (:import [io.netty.channel ChannelHandler 6 | ChannelInboundHandler 7 | ChannelOutboundHandler] 8 | [io.netty.handler.codec MessageToMessageEncoder 9 | MessageToMessageDecoder])) 10 | 11 | (defn out-tap 12 | "Logs all outbound messages." 13 | [] 14 | (proxy [MessageToMessageEncoder] [] 15 | (encode [ctx msg out] 16 | (prn :out msg) 17 | (.add out (retain msg))) 18 | 19 | (exceptionCaught [ctx throwable] 20 | (warn throwable "out-tap caught")) 21 | 22 | (isSharable [] true))) 23 | 24 | (defn in-tap 25 | "Logs all inbound messages." 26 | [] 27 | (proxy [MessageToMessageDecoder] [] 28 | (decode [ctx msg out] 29 | (prn :in msg) 30 | (retain msg) 31 | (.add out (retain msg))) 32 | 33 | (exceptionCaught [ctx throwable] 34 | (warn throwable "in-tap caught")) 35 | 36 | (isSharable [] true))) 37 | 38 | (defn tap [n] 39 | "Log fucking everything, prefixed by `n`." 40 | (reify 41 | ChannelHandler 42 | ChannelInboundHandler 43 | ChannelOutboundHandler 44 | 45 | (handlerAdded [_ x] 46 | (prn n :handler-added x)) 47 | 48 | (handlerRemoved [_ x] 49 | (prn n :handler-removed x)) 50 | 51 | (exceptionCaught [_ ctx cause] 52 | (prn n :exception-caught ctx) 53 | (.printStackTrace cause) 54 | (.fireExceptionCaught ctx cause)) 55 | 56 | (channelRegistered [_ ctx] 57 | (prn n :channel-registered ctx) 58 | (.fireChannelRegistered ctx)) 59 | 60 | (channelUnregistered [_ ctx] 61 | (prn n :channel-unregistered ctx) 62 | (.fireChannelUnregistered ctx)) 63 | 64 | (channelActive [_ ctx] 65 | (prn n :channel-active ctx) 66 | (.fireChannelActive ctx)) 67 | 68 | (channelInactive [_ ctx] 69 | (prn n :channel-inactive ctx) 70 | (.fireChannelInactive ctx)) 71 | 72 | (channelRead [_ ctx msg] 73 | (prn n :channel-read ctx msg) 74 | (.fireChannelRead ctx msg)) 75 | 76 | (channelReadComplete [_ ctx] 77 | (prn n :channel-read-complete ctx) 78 | (.fireChannelReadComplete ctx)) 79 | 80 | (userEventTriggered [_ ctx event] 81 | (prn n :user-event-triggered ctx event) 82 | (.fireUserEventTriggered ctx event)) 83 | 84 | (channelWritabilityChanged [_ ctx] 85 | (prn n :channel-writability-changed ctx) 86 | (.fireChannelWritabilityChanged ctx)) 87 | 88 | (bind [_ ctx local-address promise] 89 | (prn n :bind ctx local-address promise) 90 | (.bind ctx local-address promise)) 91 | 92 | (connect [_ ctx remote-address local-address promise] 93 | (prn n :connect ctx remote-address local-address promise) 94 | (.connect ctx remote-address local-address promise)) 95 | 96 | (disconnect [_ ctx promise] 97 | (prn n :disconnect ctx promise) 98 | (.disconnect ctx promise)) 99 | 100 | (close [_ ctx promise] 101 | (prn n :close ctx promise) 102 | (.close ctx promise)) 103 | 104 | (read [_ ctx] 105 | (prn n :read ctx) 106 | (.read ctx)) 107 | 108 | (write [_ ctx msg promise] 109 | (prn n :write ctx msg promise) 110 | (.write ctx msg promise)) 111 | 112 | (flush [_ ctx] 113 | (prn n :flush ctx) 114 | (.flush ctx)))) 115 | -------------------------------------------------------------------------------- /test/riemann/influxdb_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.influxdb-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [riemann.influxdb :as influxdb] 5 | [riemann.logging :as logging] 6 | [riemann.time :refer [unix-time]])) 7 | 8 | (logging/init) 9 | 10 | 11 | (deftest ^:influxdb-8 ^:integration influxdb-test-8 12 | (let [k (influxdb/influxdb {:block-start true})] 13 | (k {:host "riemann.local" 14 | :service "influxdb test" 15 | :state "ok" 16 | :description "all clear, uh, situation normal" 17 | :metric -2 18 | :time (unix-time)})) 19 | 20 | (let [k (influxdb/influxdb {:block-start true})] 21 | (k {:service "influxdb test" 22 | :state "ok" 23 | :description "all clear, uh, situation normal" 24 | :metric 3.14159 25 | :time (unix-time)})) 26 | 27 | (let [k (influxdb/influxdb {:block-start true})] 28 | (k {:host "no-service.riemann.local" 29 | :state "ok" 30 | :description "Missing service, not transmitted" 31 | :metric 4 32 | :time (unix-time)}))) 33 | 34 | 35 | (deftest ^:influxdb-9 ^:integration influxdb-test-9 36 | (let [k (influxdb/influxdb 37 | {:version :0.9 38 | :host (System/getenv "INFLUXDB_HOST") 39 | :db "riemann_test"})] 40 | (k {:host "riemann.local" 41 | :service "influxdb test" 42 | :state "ok" 43 | :description "all clear, uh, situation normal" 44 | :metric -2 45 | :time (unix-time)}) 46 | (k {:service "influxdb test" 47 | :state "ok" 48 | :description "all clear, uh, situation normal" 49 | :metric 3.14159 50 | :time (unix-time)}) 51 | (k {:host "no-service.riemann.local" 52 | :state "ok" 53 | :description "Missing service, not transmitted" 54 | :metric 4 55 | :time (unix-time)}))) 56 | 57 | 58 | (deftest point-conversion 59 | (is (nil? (influxdb/event->point-9 #{} {:service "foo test", :time 1})) 60 | "Event with no metric is converted to nil") 61 | (is (= {"measurement" "test service" 62 | "time" "2015-04-07T00:32:45.000Z" 63 | "tags" {"host" "host-01"} 64 | "fields" {"value" 42.08}} 65 | (influxdb/event->point-9 66 | #{:host} 67 | {:host "host-01" 68 | :service "test service" 69 | :time 1428366765 70 | :metric 42.08})) 71 | "Minimal event is converted to point fields") 72 | (is (= {"measurement" "service_api_req_latency" 73 | "time" "2015-04-06T21:15:41.000Z" 74 | "tags" {"host" "www-dev-app-01.sfo1.example.com" 75 | "sys" "www" 76 | "env" "dev" 77 | "role" "app" 78 | "loc" "sfo1"} 79 | "fields" {"value" 0.8025 80 | "description" "A text description!" 81 | "state" "ok" 82 | "foo" "frobble"}} 83 | (influxdb/event->point-9 84 | #{:host :sys :env :role :loc} 85 | {:host "www-dev-app-01.sfo1.example.com" 86 | :service "service_api_req_latency" 87 | :time 1428354941 88 | :metric 0.8025 89 | :state "ok" 90 | :description "A text description!" 91 | :ttl 60 92 | :tags ["one" "two" "red"] 93 | :sys "www" 94 | :env "dev" 95 | :role "app" 96 | :loc "sfo1" 97 | :foo "frobble"})) 98 | "Full event is converted to point fields") 99 | (is (empty? (influxdb/events->points-9 #{} [{:service "foo test"}])) 100 | "Nil points are filtered from result")) 101 | -------------------------------------------------------------------------------- /test/riemann/graphite_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.graphite-test 2 | (:use riemann.graphite 3 | [riemann.time :only [unix-time]] 4 | [riemann.common :only [event]] 5 | clojure.tools.logging 6 | clojure.test) 7 | (:require [riemann.logging :as logging] 8 | [riemann.client :as client] 9 | [riemann.index :as index] 10 | [riemann.core :refer [transition! core stop! wrap-index]] 11 | [riemann.transport.tcp :refer [tcp-server]] 12 | [riemann.transport.graphite :refer [graphite-server]])) 13 | 14 | (logging/init) 15 | 16 | (deftest graphite-server-test 17 | (logging/suppress 18 | ["riemann.transport" "riemann.core" "riemann.pubsub" "riemann.graphite"] 19 | (let [s1 (graphite-server) 20 | s2 (tcp-server) 21 | index (wrap-index (index/index)) 22 | core (transition! 23 | (core) 24 | {:index index 25 | :services [s1 s2] 26 | :streams [index]}) 27 | sendout! (graphite {:path graphite-path-basic}) 28 | client (client/tcp-client)] 29 | (try 30 | (sendout! {:service "service1" :metric 1.0 :time 0}) 31 | (sendout! {:service "service2" :metric 1.0 :time 0}) 32 | (Thread/sleep 100) 33 | (let [[r1 r2] @(client/query client "true")] 34 | (is (and (#{"service1" "service2"} (:service r1)) 35 | (= 1.0 (:metric r1)))) 36 | (is (and (#{"service1" "service2"} (:service r2)) 37 | (= 1.0 (:metric r2))))) 38 | (finally 39 | (client/close! client) 40 | (stop! core)))))) 41 | 42 | (deftest parse-error-test 43 | (logging/suppress 44 | ["riemann.transport" "riemann.core" "riemann.pubsub" "riemann.graphite"] 45 | (let [server (graphite-server) 46 | trap (promise) 47 | core (transition! (core) 48 | {:services [server] 49 | :streams [(partial deliver trap)]}) 50 | client (open (->GraphiteTCPClient "localhost" 2003))] 51 | (try 52 | (send-line client "too many spaces 1.23 456\n") 53 | (send-line client "valid 1.34 456\n") 54 | (is (= (deref trap 1000 :timeout) 55 | (event {:service "valid" :metric 1.34 :time 456}))) 56 | (finally 57 | (close client) 58 | (stop! core)))))) 59 | 60 | (deftest percentiles 61 | (is (= (graphite-path-percentiles 62 | {:service "foo bar"}) 63 | "foo.bar")) 64 | (is (= (graphite-path-percentiles 65 | {:service "foo bar 1"}) 66 | "foo.bar.1")) 67 | (is (= (graphite-path-percentiles 68 | {:service "foo bar 99"}) 69 | "foo.bar.99")) 70 | (is (= (graphite-path-percentiles 71 | {:service "foo bar 0.99"}) 72 | "foo.bar.99")) 73 | (is (= (graphite-path-percentiles 74 | {:service "foo bar 0.999"}) 75 | "foo.bar.999"))) 76 | 77 | (deftest ^:graphite ^:integration graphite-test 78 | (let [g (graphite {:block-start true})] 79 | (g {:host "riemann.local" 80 | :service "graphite test" 81 | :state "ok" 82 | :description "all clear, uh, situation normal" 83 | :metric -2 84 | :time (unix-time)})) 85 | 86 | (let [g (graphite {:block-start true})] 87 | (g {:service "graphite test" 88 | :state "ok" 89 | :description "all clear, uh, situation normal" 90 | :metric 3.14159 91 | :time (unix-time)})) 92 | 93 | (let [g (graphite {:block-start true})] 94 | (g {:host "no-service.riemann.local" 95 | :state "ok" 96 | :description "all clear, uh, situation normal" 97 | :metric 4 98 | :time (unix-time)}))) 99 | -------------------------------------------------------------------------------- /src/riemann/pubsub.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.pubsub 2 | "Provides publish-subscribe handling of events. Publishers push events onto a 3 | channel, which has n subscribers. Each subscriber subscribes to a channel 4 | with an optional predicate function. Events which match the predicate are 5 | sent to the subscriber." 6 | (:use clojure.tools.logging) 7 | (:import (riemann.service Service 8 | ServiceEquiv))) 9 | 10 | (defn dissoc-in 11 | "Dissociates an entry from a nested associative structure returning a new 12 | nested structure. keys is a sequence of keys. Any empty maps that result 13 | will not be present in the new structure." 14 | [m [k & ks :as keys]] 15 | (if ks 16 | (if-let [nextmap (get m k)] 17 | (let [newmap (dissoc-in nextmap ks)] 18 | (if (seq newmap) 19 | (assoc m k newmap) 20 | (dissoc m k))) 21 | m) 22 | (dissoc m k))) 23 | 24 | (def last-sub-id 25 | "The most recently assigned subscription ID." 26 | (atom 0)) 27 | 28 | (defn sub-id 29 | "Returns a new unique subscription ID." 30 | [] 31 | (swap! last-sub-id inc)) 32 | 33 | (defrecord Subscription [channel id f persistent?]) 34 | 35 | (defprotocol PubSub 36 | "The PubSub protocol defines the interface for publishing and subscribing to 37 | channels; essentially, sets of named callbacks." 38 | (subscribe! [this channel f] 39 | [this channel f persistent?] 40 | "Subscribes to the given channel. Returns a Subscription.") 41 | 42 | (unsubscribe! [this sub] 43 | "Cancels a subscription.") 44 | 45 | (publish! [this channel event] 46 | "Publish an event to a channel.") 47 | 48 | (sweep! [this] 49 | "Shuts down all non-persistent subscriptions. Used when reloading the 50 | pubsub system, and we want to clear any subscriptions from the old 51 | streams.")) 52 | 53 | ; Channels is an atom wrapping a map of channel ids to subscriptions. 54 | (defrecord PubSubService [core channels] 55 | PubSub 56 | (subscribe! [this channel f persistent?] 57 | (let [id (sub-id) 58 | sub (Subscription. channel id f persistent?)] 59 | (swap! channels assoc-in [channel id] sub) 60 | sub)) 61 | 62 | (subscribe! [this channel f] 63 | (subscribe! this channel f false)) 64 | 65 | (unsubscribe! [this sub] 66 | (swap! channels dissoc-in [(:channel sub) (:id sub)])) 67 | 68 | (publish! [this channel event] 69 | (doseq [[id ^Subscription sub] (get @channels channel)] 70 | ((.f sub) event))) 71 | 72 | (sweep! [this] 73 | (info "Sweeping transient subscriptions.") 74 | (swap! channels 75 | (fn [channels] 76 | (into {} 77 | (map (fn [[channel sub-map]] 78 | (let [sub-map (into {} (filter 79 | (comp :persistent? val) 80 | sub-map))] 81 | (if (empty? sub-map) 82 | channels 83 | (assoc channels channel sub-map)))) 84 | channels))))) 85 | 86 | ; All pubsub services are equivalent; we clean out old subscriptions using 87 | ; sweep. 88 | ServiceEquiv 89 | (equiv? [a b] (= (class a) (class b))) 90 | 91 | Service 92 | (conflict? [a b] false) 93 | 94 | (start! [this]) 95 | 96 | (reload! [this new-core] 97 | (locking this 98 | (reset! core new-core))) 99 | 100 | (stop! [this] 101 | (locking this 102 | (info "PubSub shutting down.") 103 | (reset! channels {})))) 104 | 105 | (defn pubsub-registry 106 | "Returns a new pubsub registry, which tracks which subscribers are 107 | listening to which channels." 108 | [] 109 | (PubSubService. (atom nil) (atom {}))) 110 | -------------------------------------------------------------------------------- /src/riemann/logstash.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.logstash 2 | "Forwards events to LogStash." 3 | (:refer-clojure :exclude [replace]) 4 | (:import 5 | (java.net Socket 6 | DatagramSocket 7 | DatagramPacket 8 | InetAddress) 9 | (java.io Writer OutputStreamWriter)) 10 | (:use [clojure.string :only [split join replace]] 11 | clojure.tools.logging 12 | riemann.pool 13 | riemann.common)) 14 | 15 | (defprotocol LogStashClient 16 | (open [client] 17 | "Creates a LogStash client") 18 | (send-line [client line] 19 | "Sends a formatted line to LogStash") 20 | (close [client] 21 | "Cleans up (closes sockets etc.)")) 22 | 23 | (defrecord LogStashTCPClient [^String host ^int port] 24 | LogStashClient 25 | (open [this] 26 | (let [sock (Socket. host port)] 27 | (assoc this 28 | :socket sock 29 | :out (OutputStreamWriter. (.getOutputStream sock))))) 30 | (send-line [this line] 31 | (let [out (:out this)] 32 | (.write ^OutputStreamWriter out ^String line) 33 | (.flush ^OutputStreamWriter out))) 34 | (close [this] 35 | (.close ^OutputStreamWriter (:out this)) 36 | (.close ^Socket (:socket this)))) 37 | 38 | (defrecord LogStashUDPClient [^String host ^int port] 39 | LogStashClient 40 | (open [this] 41 | (assoc this 42 | :socket (DatagramSocket.) 43 | :host host 44 | :port port)) 45 | (send-line [this line] 46 | (let [bytes (.getBytes ^String line) 47 | length (count line) 48 | addr (InetAddress/getByName (:host this)) 49 | datagram (DatagramPacket. bytes length ^InetAddress addr port)] 50 | (.send ^DatagramSocket (:socket this) datagram))) 51 | (close [this] 52 | (.close ^DatagramSocket (:socket this)))) 53 | 54 | 55 | 56 | (defn logstash 57 | "Returns a function which accepts an event and sends it to logstash. 58 | Silently drops events when logstash is down. Attempts to reconnect 59 | automatically every five seconds. Use: 60 | 61 | (logstash {:host \"logstash.local\" :port 2003}) 62 | 63 | Options: 64 | 65 | :pool-size The number of connections to keep open. Default 4. 66 | 67 | :reconnect-interval How many seconds to wait between attempts to connect. 68 | Default 5. 69 | 70 | :claim-timeout How many seconds to wait for a logstash connection from 71 | the pool. Default 0.1. 72 | 73 | :block-start Wait for the pool's initial connections to open 74 | before returning. 75 | 76 | :protocol Protocol to use. Either :tcp (default) or :udp." 77 | [opts] 78 | (let [opts (merge {:host "127.0.0.1" 79 | :port 9999 80 | :protocol :tcp 81 | :claim-timeout 0.1 82 | :pool-size 4} opts) 83 | pool (fixed-pool 84 | (fn [] 85 | (info "Connecting to " (select-keys opts [:host :port])) 86 | (let [host (:host opts) 87 | port (:port opts) 88 | client (open (condp = (:protocol opts) 89 | :tcp (LogStashTCPClient. host port) 90 | :udp (LogStashUDPClient. host port)))] 91 | (info "Connected") 92 | client)) 93 | (fn [client] 94 | (info "Closing connection to " 95 | (select-keys opts [:host :port])) 96 | (close client)) 97 | {:size (:pool-size opts) 98 | :block-start (:block-start opts) 99 | :regenerate-interval (:reconnect-interval opts)})] 100 | 101 | (fn [event] 102 | (with-pool [client pool (:claim-timeout opts)] 103 | (let [string (event-to-json (merge event {:source (:host event)}))] 104 | (send-line client (str string "\n"))))))) 105 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject riemann "0.2.10-SNAPSHOT" 2 | :description 3 | "A network event stream processor. Intended for analytics, metrics, and alerting; and to glue various monitoring systems together." 4 | :url "http://github.com/aphyr/riemann" 5 | ; :warn-on-reflection true 6 | ; :jvm-opts ["-server" "-d64" "-Xms1024m" "-Xmx1024m" "-XX:+UseParNewGC" "-XX:+UseConcMarkSweepGC" "-XX:+CMSParallelRemarkEnabled" "-XX:+AggressiveOpts" "-XX:+UseFastAccessorMethods" "-verbose:gc" "-XX:+PrintGCDetails"] 7 | :jvm-opts ["-server" "-Xms1024m" "-Xmx1024m" "-XX:+UseParNewGC" "-XX:+UseConcMarkSweepGC" "-XX:+CMSParallelRemarkEnabled" "-XX:+AggressiveOpts" "-XX:+UseFastAccessorMethods" "-XX:+CMSClassUnloadingEnabled"] 8 | :maintainer {:email "aphyr@aphyr.com"} 9 | :dependencies [ 10 | [org.clojure/algo.generic "0.1.2"] 11 | [org.clojure/clojure "1.6.0"] 12 | [org.clojure/math.numeric-tower "0.0.4"] 13 | [org.clojure/tools.logging "0.3.1"] 14 | [org.clojure/tools.nrepl "0.2.7"] 15 | [org.clojure/core.cache "0.6.4"] 16 | [org.clojure/java.classpath "0.2.2"] 17 | [log4j/log4j "1.2.17" :exclusions [javax.mail/mail 18 | javax.jms/jms 19 | com.sun.jdmk/jmxtools 20 | com.sun.jmx/jmxri]] 21 | [net.logstash.log4j/jsonevent-layout "1.7"] 22 | [com.cemerick/pomegranate "0.3.0"] 23 | [org.spootnik/http-kit "2.1.18.1"] 24 | [clj-http "1.0.1" :exclusions [org.clojure/tools.reader]] 25 | [cheshire "5.4.0"] 26 | [clj-librato "0.0.5"] 27 | [clj-time "0.9.0"] 28 | [clj-wallhack "1.0.1"] 29 | [com.boundary/high-scale-lib "1.0.6"] 30 | [com.draines/postal "1.11.3"] 31 | [com.amazonaws/aws-java-sdk "1.9.13" :exclusions [joda-time]] 32 | [interval-metrics "1.0.0"] 33 | [io.netty/netty-all "4.0.24.Final"] 34 | [log4j/apache-log4j-extras "1.2.17"] 35 | [clj-antlr "0.2.2"] 36 | [org.slf4j/slf4j-log4j12 "1.7.10"] 37 | [riemann-clojure-client "0.3.2"] 38 | [less-awful-ssl "1.0.0"] 39 | [slingshot "0.12.1"] 40 | [clj-campfire "2.2.0"] 41 | [clj-nsca "0.0.3"] 42 | [amazonica "0.3.13" :exclusions [joda-time]] 43 | [capacitor "0.4.2" :exclusions [http-kit]]] 44 | :plugins [[codox "0.6.1"] 45 | [lein-rpm "0.0.5"]] 46 | :profiles {:dev {:jvm-opts ["-XX:-OmitStackTraceInFastThrow"] 47 | ; "-Dcom.sun.management.jmxremote" 48 | ; "-XX:+UnlockCommercialFeatures" 49 | ; "-XX:+FlightRecorder"] 50 | :dependencies [[criterium "0.4.3"]]}} 51 | :test-selectors {:default (fn [x] (not (or (:integration x) 52 | (:time x) 53 | (:bench x)))) 54 | :integration :integration 55 | :email :email 56 | :sns :sns 57 | :graphite :graphite 58 | :influxdb :influxdb 59 | :kairosdb :kairosdb 60 | :librato :librato 61 | :hipchat :hipchat 62 | :nagios :nagios 63 | :opentsdb :opentsdb 64 | :time :time 65 | :bench :bench 66 | :focus :focus 67 | :slack :slack 68 | :cloudwatch :cloudwatch 69 | :datadog :datadog 70 | :stackdriver :stackdriver 71 | :xymon :xymon 72 | :shinken :shinken 73 | :blueflood :blueflood 74 | :opsgenie :opsgenie 75 | :boundary :boundary 76 | :all (fn [_] true)} 77 | :javac-options ["-target" "1.6" "-source" "1.6"] 78 | :java-source-paths ["src/riemann/"] 79 | :java-source-path "src/riemann/" 80 | ; :aot [riemann.bin] 81 | :main riemann.bin 82 | :codox {:output-dir "site/api" 83 | :src-dir-uri "http://github.com/aphyr/riemann/blob/master/" 84 | :src-linenum-anchor-prefix "L" 85 | :defaults {:doc/format :markdown}} 86 | ) 87 | -------------------------------------------------------------------------------- /test/riemann/time_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.time-test 2 | (:use riemann.time 3 | [riemann.common :exclude [unix-time linear-time]] 4 | clojure.math.numeric-tower 5 | clojure.test 6 | clojure.tools.logging) 7 | (:require [riemann.logging :as logging])) 8 | 9 | (riemann.logging/init) 10 | 11 | (defn reset-time! 12 | [f] 13 | (stop!) 14 | (reset-tasks!) 15 | (start!) 16 | (f) 17 | (stop!) 18 | (reset-tasks!)) 19 | (use-fixtures :each reset-time!) 20 | 21 | (deftest next-tick-test 22 | (are [anchor dt now next] (= (next-tick anchor dt now) next) 23 | 0 1 0 1 24 | 0 2 0 2 25 | 1 1 0 1 26 | 2 1 0 1 27 | 0 2 0 2 28 | 0 2 1 2 29 | 0 2 2 4 30 | 2 2 2 4 31 | 4 2 2 4 32 | 1 2 1 3 33 | 1 2 2 3 34 | 1 2 3 5)) 35 | 36 | (deftest ^:time clock-test 37 | (is (approx-equal (/ (System/currentTimeMillis) 1000) 38 | (unix-time)))) 39 | 40 | (deftest ^:time once-test 41 | "Run a function once, to verify that the threadpool works at all." 42 | (let [t0 (unix-time) 43 | results (atom [])] 44 | (after! 0.1 #(swap! results conj (- (unix-time) t0))) 45 | (Thread/sleep 300) 46 | (is (<= 0.085 (first @results) 0.115)))) 47 | 48 | ; LMAO if this test becomes hilariously unstable and/or exhibits genuine 49 | ; heisenbugs for any unit of time smaller than 250ms. 50 | (deftest ^:time defer-cancel-test 51 | (let [x1 (atom 0) 52 | x2 (atom 0) 53 | t1 (every! 1 (fn [] (swap! x1 inc))) 54 | t2 (every! 1 1 #(swap! x2 inc))] 55 | (Thread/sleep 500) 56 | (is (= 1 @x1)) 57 | (is (= 0 @x2)) 58 | 59 | (Thread/sleep 1000) 60 | (is (= 2 @x1)) 61 | (is (= 1 @x2)) 62 | 63 | ; Defer 64 | (defer t1 1.5) 65 | (Thread/sleep 1000) 66 | (is (= 2 @x1)) 67 | (is (= 2 @x2)) 68 | 69 | (Thread/sleep 1000) 70 | (is (= 3 @x1)) 71 | (is (= 3 @x2)) 72 | 73 | ; Cancel 74 | (cancel t2) 75 | (Thread/sleep 1000) 76 | (is (= 4 @x1)) 77 | (is (= 3 @x2)))) 78 | 79 | (deftest ^:time exception-recovery-test 80 | (let [x (atom 0)] 81 | (every! 0.1 (fn [] (swap! x inc) (/ 1 0))) 82 | (Thread/sleep 150) 83 | (is (= 2 @x)))) 84 | 85 | (defn mapvals 86 | [f kv] 87 | (into {} (map (fn [[k v]] [k (f v)]) kv))) 88 | 89 | (defn pairs 90 | [coll] 91 | (partition 2 1 coll)) 92 | 93 | (defn differences 94 | [coll] 95 | (map (fn [[x y]] (- y x)) (pairs coll))) 96 | 97 | (deftest ^:time periodic-test 98 | "Run one function periodically." 99 | (let [results (atom [])] 100 | ; For a wide variety of intervals, start periodic jobs to record 101 | ; the time. 102 | (doseq [interval (range 1/10 5 1/10)] 103 | (every! interval #(swap! results conj [interval (unix-time)]))) 104 | 105 | (Thread/sleep 20000) 106 | (stop!) 107 | 108 | (let [groups (mapvals (fn [vs] (map second vs)) 109 | (group-by first @results)) 110 | differences (mapvals differences groups)] 111 | (doseq [[interval deltas] differences] 112 | ; First delta will be slightly smaller because the scheduler 113 | ; computed an absolute time in the *past* 114 | (is (<= -0.025 (- (first deltas) interval) 0)) 115 | 116 | (let [deltas (drop 1 deltas)] 117 | ; Remaining deltas should be accurate to within 5ms. 118 | (is (every? (fn [delta] 119 | (< -0.05 (- delta interval) 0.05)) deltas)) 120 | ; and moreover, there should be no cumulative drift. 121 | (is (< -0.005 122 | (- (/ (reduce + deltas) (count deltas)) interval) 123 | 0.005))))))) 124 | -------------------------------------------------------------------------------- /src/riemann/boundary.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.boundary 2 | "Forwards events to Boundary Premium." 3 | (:require [clj-http.client :as client] 4 | [cheshire.core :as json] 5 | [clojure.string :as s])) 6 | 7 | (def ^:private base-uri "Boundary API base URI." 8 | "https://premium-api.boundary.com") 9 | (def ^:private version "Boundary API version to use." 10 | "v1") 11 | (def ^:private sync-path "Path part for syncrhonous communication." 12 | "measurements") 13 | (def ^:private async-path "Path part for asyncrhonous 14 | communication." 15 | "measurementsAsync") 16 | 17 | (defn ^:private boundarify 18 | "As of Boundary's specs, metric ids can only contain characters 19 | matching \"[A-Z0-9_]\", thus all other characters will be stripped 20 | and the remaining ones will be upcased. 21 | 22 | To preserve structure, unacceptable characters will be removed 23 | *after* substituting spaces with underscores. 24 | 25 | Should an organization name be provided, it will be placed before 26 | the name of the service. 27 | 28 | Last but not least, if after all the manipulation of the string, no 29 | characters remain (i.e. empty string), an exception is thrown. 30 | 31 | Examples: 32 | 33 | (boundarify \"foo\") => \"FOO\" 34 | (boundarify \"foo bar\") => \"FOO_BAR\" 35 | (boundarify \"foo@\") => \"FOO\" 36 | (boundarify \"foo@bar\") => \"FOOBAR\" 37 | (boundarify \"foo\" \"org\") => \"ORG_FOO\" 38 | (boundarify \"!#@\") => exception 39 | (boundarify \"!#@\" \"org\") => exception 40 | " 41 | [service & [organization]] 42 | (let [good-ones (-> service 43 | (s/replace #"\s+" "_") 44 | s/upper-case 45 | (s/replace #"[^A-Z0-9_]" ""))] 46 | (when (empty? good-ones) 47 | (throw (RuntimeException. 48 | (str "can't accept the given service string \"" 49 | service "\" as metric id")))) 50 | (if (nil? organization) 51 | good-ones 52 | (str (s/upper-case organization) "_" good-ones)))) 53 | 54 | (defn ^:private packer-upper 55 | "Returns a function packs up the events in a form suitable for 56 | Boundary's API. 57 | 58 | If a metric-id is given, it will be used for all the events in the 59 | pack. Otherwise, every single event service is \"boundarified\". In 60 | both cases, organization is prepended if given." 61 | [{:keys [metric-id org]}] 62 | (let [helper 63 | #(vector 64 | (:host %) 65 | (if (nil? metric-id) 66 | (boundarify (:service %) org) 67 | (boundarify metric-id org)) 68 | (:metric %) 69 | (:time %))] 70 | (fn [events] 71 | (mapv helper events)))) 72 | 73 | (defn boundary 74 | "Returns a function used to generate specific senders (like mailer) 75 | that takes three optional named arguments, namely :metric-id 76 | :org and :async, that modify which metric the events are sent to. 77 | 78 | Specifically, if :metric-id is supplied, every single event is sent 79 | to that metric, otherwise the event's :service field is used to 80 | construct the destination Boundary's metric id. In both cases, 81 | organization is prepended if non nil. The last argument, :async, 82 | switches the endpoint to the asynchronous one (default is sync). 83 | 84 | Examples: 85 | 86 | (def bdry (boundary eml tkn)) 87 | (when :foo (bdry)) => builds the destination metric id with :service 88 | (when :foo (bdry {:async true})) => same as previous, but async 89 | (when :foo (bdry {:metric-id \"METRIC_ID\"})) => sends to METRIC_ID" 90 | [email token] 91 | (fn b 92 | ([] (b {})) 93 | ([{:keys [metric-id org async]}] 94 | (let [pack-up (packer-upper {:metric-id metric-id :org org}) 95 | path (if async async-path sync-path)] 96 | (fn [events] 97 | (let [pack (pack-up events) 98 | req-map {:scheme :https 99 | :basic-auth [email token] 100 | :headers {"Content-Type" "application/json"} 101 | :body (json/generate-string pack)}] 102 | (client/post (s/join "/" [base-uri version path]) 103 | req-map))))))) 104 | -------------------------------------------------------------------------------- /test/riemann/common_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.common-test 2 | (:use riemann.common) 3 | (:use clojure.test)) 4 | 5 | (deftest iso8601->unix-test 6 | (let [times (map iso8601->unix 7 | ["2013-04-15T18:06:58-07:00" 8 | "2013-04-15T18:06:58.123+11:00" 9 | "2013-04-15T18:06:58Z" 10 | "2013-04-15"])] 11 | (is (= times [1366074418 12 | 1366009618 13 | 1366049218 14 | 1365984000])))) 15 | 16 | (deftest subset-test 17 | (are [a b] (subset? a b) 18 | [] [] 19 | [] [1 2 3] 20 | [1] [1] 21 | [1] [2 1 3] 22 | [1 2] [1 2 3] 23 | [1 2] [2 3 1]) 24 | 25 | (are [a b] (not (subset? a b)) 26 | [1] [] 27 | [1 2] [1] 28 | [1] [2] 29 | [1] [2 3] 30 | [1 2] [1 3] 31 | [1 2] [3 1])) 32 | 33 | (deftest overlap-test 34 | (are [a b] (overlap? a b) 35 | [1 2] [1] 36 | [1] [1] 37 | [1 2] [2 3] 38 | [3 2] [1 3] 39 | [1 3] [3 1]) 40 | 41 | (are [a b] (not (overlap? a b)) 42 | [] [] 43 | [1] [] 44 | [1] [2] 45 | [3] [1 2] 46 | [1 2] [3 4])) 47 | 48 | (deftest disjoint-test 49 | (are [a b] (disjoint? a b) 50 | [] [] 51 | [1] [] 52 | [1] [2] 53 | [3] [1 2] 54 | [1 2] [3 4]) 55 | 56 | (are [a b] (not (disjoint? a b)) 57 | [1 2] [1] 58 | [1] [1] 59 | [1 2] [2 3] 60 | [3 2] [1 3] 61 | [1 3] [3 1])) 62 | 63 | (deftest subject-test 64 | (let [s #'subject] 65 | (are [events subject] (= (s events) subject) 66 | [] "" 67 | 68 | [{}] "" 69 | 70 | [{:host "foo"}] "foo" 71 | 72 | [{:host "foo"} {:host "bar"}] "foo and bar" 73 | 74 | [{:host "foo"} {:host "bar"} {:host "baz"}] 75 | "foo, bar, baz" 76 | 77 | [{:host "foo"} {:host "baz"} {:host "bar"} {:host "baz"}] 78 | "foo, baz, bar" 79 | 80 | [{:host 1} {:host 2} {:host 3} {:host 4} {:host 5}] 81 | "5 hosts" 82 | 83 | [{:host "foo" :state "ok"}] "foo ok" 84 | 85 | [{:host "foo" :state "ok"} {:host "bar" :state "ok"}] 86 | "foo and bar ok" 87 | 88 | [{:host "foo" :state "error"} {:host "bar" :state "ok"}] 89 | "foo and bar error and ok" 90 | ))) 91 | 92 | (deftest count-string-bytes-test 93 | (is (= (count-string-bytes "") 0)) 94 | (is (= (count-string-bytes "a") 1)) 95 | (is (= (count-string-bytes "é") 2)) 96 | (is (= (count-string-bytes "あ") 3)) 97 | (is (= (count-string-bytes "𠜎") 4)) 98 | (is (= (count-string-bytes "あいう") 9))) 99 | 100 | (deftest count-character-bytes-test 101 | (is (= (count-character-bytes \a) 1)) 102 | (is (= (count-character-bytes \é) 2)) 103 | (is (= (count-character-bytes \あ) 3))) 104 | 105 | (deftest truncate-test 106 | (is (= (truncate "あいう" -1) "")) 107 | (is (= (truncate "あいう" 0) "")) 108 | (is (= (truncate "あいう" 1) "あ")) 109 | (is (= (truncate "あいう" 3) "あいう")) 110 | (is (= (truncate "あいう" 4) "あいう"))) 111 | 112 | (deftest truncate-bytes-test 113 | (is (= (truncate-bytes "あいう" -1) "")) 114 | (is (= (truncate-bytes "あいう" 0) "")) 115 | (is (= (truncate-bytes "あいう" 1) "")) 116 | (is (= (truncate-bytes "あいう" 3) "あ")) 117 | (is (= (truncate-bytes "あいう" 4) "あ")) 118 | (is (= (truncate-bytes "あいう" 9) "あいう")) 119 | (is (= (truncate-bytes "あいう" 10) "あいう"))) 120 | 121 | (deftest exception->event-test 122 | (is (= ["exception" "clojure.lang.ExceptionInfo"] 123 | (:tags (exception->event (ex-info "fake test error" {}))))) 124 | 125 | (let [e (ex-info "fake test error" {})] 126 | (is (= e 127 | (:exception (exception->event e))))) 128 | 129 | (is (= "original-event" 130 | (:service (:event (exception->event (ex-info "fake test error" {}) {:service "original-event"})))))) 131 | -------------------------------------------------------------------------------- /src/riemann/index.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.index 2 | "Maintains a stateful index of events by [host, service] key. Can be queried 3 | to return the most recent indexed events matching some expression. Can expire 4 | events which have exceeded their TTL. Presently the only implementation of 5 | the index protocol is backed by a nonblockinghashmap, but I plan to add an 6 | HSQLDB backend as well. 7 | 8 | Indexes must extend three protocols: 9 | 10 | Index: indexing and querying events 11 | Seqable: returning a list of events 12 | Service: lifecycle management" 13 | (:require [riemann.query :as query] 14 | [riemann.instrumentation :refer [Instrumented]]) 15 | (:use [riemann.time :only [unix-time]] 16 | riemann.service) 17 | (:import (org.cliffc.high_scale_lib NonBlockingHashMap))) 18 | 19 | (defprotocol Index 20 | (clear [this] 21 | "Resets the index") 22 | (delete [this event] 23 | "Deletes any event with this host & service from index. Returns the deleted 24 | event, or nil.") 25 | (delete-exactly [this event] 26 | "Deletes event from index. Returns the deleted event, or nil.") 27 | (expire [this] 28 | "Return a seq of expired states from this index, removing each.") 29 | (search [this query-ast] 30 | "Returns a seq of events from the index matching this query AST") 31 | (update [this event] 32 | "Updates index with event") 33 | (lookup [this host service] 34 | "Lookup an indexed event from the index")) 35 | 36 | ; The index accepts states and maintains a table of the most recent state for 37 | ; each unique [host, service]. It can be searched for states matching a query. 38 | 39 | (def default-ttl 60) 40 | 41 | (defn query-for-host-and-service 42 | "Check if the AST is only searching for the host and service" 43 | [query-ast] 44 | (if (and (list? query-ast) 45 | (= 'and (first query-ast))) 46 | (let [and-exprs (rest query-ast)] 47 | (if (and (= 2 (count and-exprs)) 48 | (every? list? and-exprs) 49 | (= 2 (count (filter #(= (first %) '=) and-exprs)))) 50 | (let [host (first (filter #(= (second %) :host) and-exprs)) 51 | service (first (filter #(= (second %) :service) and-exprs))] 52 | (if (and host service) 53 | [(last host) (last service)])))))) 54 | 55 | (defn nbhm-index 56 | "Create a new nonblockinghashmap backed index" 57 | [] 58 | (let [hm (NonBlockingHashMap.)] 59 | (reify 60 | Index 61 | (clear [this] 62 | (.clear hm)) 63 | 64 | (delete [this event] 65 | (.remove hm [(:host event) (:service event)])) 66 | 67 | (delete-exactly [this event] 68 | (.remove hm [(:host event) (:service event)] event)) 69 | 70 | (expire [this] 71 | (filter 72 | (fn [event] 73 | (let [age (- (unix-time) (:time event)) 74 | ttl (or (:ttl event) default-ttl)] 75 | (when (< ttl age) 76 | (delete-exactly this event) 77 | true))) 78 | (.values hm))) 79 | 80 | (search [this query-ast] 81 | "O(n) unless the query is for exactly a host and service" 82 | (if-let [[host service] (query-for-host-and-service query-ast)] 83 | (when-let [e (.lookup this host service)] 84 | (list e)) 85 | (let [matching (query/fun query-ast)] 86 | (filter matching (.values hm))))) 87 | 88 | 89 | (update [this event] 90 | (if (= "expired" (:state event)) 91 | (delete this event) 92 | (.put hm [(:host event) (:service event)] event))) 93 | 94 | (lookup [this host service] 95 | (.get hm [host service])) 96 | 97 | Instrumented 98 | (events [this] 99 | (let [base {:state "ok" :time (unix-time)}] 100 | (map (partial merge base) 101 | [{:service "riemann index size" 102 | :metric (.size hm)}]))) 103 | 104 | clojure.lang.Seqable 105 | (seq [this] 106 | (seq (.values hm))) 107 | 108 | ServiceEquiv 109 | (equiv? [this other] (= (class this) (class other))) 110 | 111 | Service 112 | (conflict? [this other] false) 113 | (reload! [this new-core]) 114 | (start! [this]) 115 | (stop! [this])))) 116 | 117 | (defn index 118 | "Create a new index (currently: an nhbm index)" 119 | [] 120 | (nbhm-index)) 121 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/cacert.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 10735806336529667390 (0x94fd3efb7a3d153e) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=US, ST=CA, O=Puppy Bureaucracy, OU=Distributed Systems Department, CN=test.riemann.io 7 | Validity 8 | Not Before: Jul 13 21:41:19 2015 GMT 9 | Not After : Apr 27 21:41:19 2289 GMT 10 | Subject: C=US, ST=CA, O=Puppy Bureaucracy, OU=Distributed Systems Department, CN=test.riemann.io 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:b0:67:23:f1:0a:db:bf:b9:32:ef:6d:7c:01:d8: 16 | 3b:b0:58:cd:02:d4:c2:01:40:81:a0:d6:bf:a7:57: 17 | 09:88:56:cb:95:89:2e:18:e0:ae:a1:b9:70:8d:aa: 18 | d7:35:4d:1b:54:0b:56:9a:3f:b9:b1:d9:18:e2:4e: 19 | 41:60:00:02:2f:df:bb:80:c2:9b:cf:fb:eb:13:78: 20 | 28:e9:0e:42:cc:b3:21:ef:f3:de:3e:19:ce:a3:ac: 21 | f2:67:78:98:bf:fb:e0:a3:cf:bf:f7:0c:dd:35:3f: 22 | 39:27:be:12:ce:19:66:0a:a2:55:84:7c:14:4a:cc: 23 | f5:28:69:74:02:e5:c9:ee:f3:76:df:38:e3:04:fd: 24 | 08:99:e8:d1:d6:fc:7f:f7:e9:23:db:23:79:50:1d: 25 | 67:0a:7c:3a:66:8d:89:29:79:fd:c7:1b:00:e6:09: 26 | 83:a1:e5:67:77:dc:58:be:69:9d:7a:64:a7:d3:cc: 27 | e6:d6:32:76:37:e5:5f:50:69:8c:6b:3c:69:33:0a: 28 | f2:68:bb:0d:f2:16:48:82:c6:11:9d:da:36:7d:3a: 29 | ba:0d:3b:3c:1d:72:3e:bf:fa:bb:fe:78:3e:b0:7a: 30 | c8:15:1b:4f:2a:99:0c:8f:e8:20:5b:7e:6e:02:81: 31 | 48:d4:c4:70:d3:ea:c7:8c:51:c3:18:25:ce:5c:a5: 32 | 73:b3 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Subject Key Identifier: 36 | C4:19:70:FE:F5:DB:A2:F6:52:B5:C2:8E:30:6C:B0:C9:9C:90:78:18 37 | X509v3 Authority Key Identifier: 38 | keyid:C4:19:70:FE:F5:DB:A2:F6:52:B5:C2:8E:30:6C:B0:C9:9C:90:78:18 39 | 40 | X509v3 Basic Constraints: 41 | CA:TRUE 42 | Signature Algorithm: sha256WithRSAEncryption 43 | af:59:52:c1:d8:96:da:15:b1:fc:15:50:4a:72:e1:eb:ac:07: 44 | a9:e1:d4:39:29:ad:81:4b:f2:41:8a:39:d4:ce:96:0e:6a:7d: 45 | 14:e0:98:a9:d5:3e:49:a5:e1:ed:2d:3c:bf:a2:77:44:ed:de: 46 | 80:30:4d:cb:0a:0f:1c:be:50:f1:55:49:e1:af:39:ca:6f:1c: 47 | de:64:9e:b0:6d:57:47:48:1d:d3:a7:11:3c:6f:05:55:2b:42: 48 | 22:58:07:a2:4c:04:51:e6:d7:b6:dc:29:2f:f7:d0:ba:ee:36: 49 | 27:ea:2a:45:c1:55:d7:19:d4:5c:9a:a8:8d:2e:cf:10:9f:4e: 50 | 07:e0:11:04:f3:98:53:6a:52:41:89:6b:9f:92:a7:c7:ce:24: 51 | dc:de:c5:26:38:9c:03:c4:52:9d:a1:c4:7d:6d:b8:8f:6c:f3: 52 | cb:20:e4:8a:68:00:4a:e1:e0:12:dd:e3:e2:47:af:3d:fb:31: 53 | bc:bc:56:51:b5:79:9e:3a:87:11:e2:92:51:80:81:fa:72:07: 54 | 46:5e:dc:db:8a:e0:e9:39:d6:05:c7:ea:f8:ca:24:c9:c5:76: 55 | 4a:98:7c:f7:cd:52:d3:5f:6a:ea:5c:3a:5b:74:82:93:a6:eb: 56 | a1:f4:18:22:e2:cf:83:74:18:93:e4:3f:19:f4:b9:8c:53:bd: 57 | 4c:ee:3c:62 58 | -----BEGIN CERTIFICATE----- 59 | MIIDxzCCAq+gAwIBAgIJAJT9Pvt6PRU+MA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV 60 | BAYTAlVTMQswCQYDVQQIDAJDQTEaMBgGA1UECgwRUHVwcHkgQnVyZWF1Y3JhY3kx 61 | JzAlBgNVBAsMHkRpc3RyaWJ1dGVkIFN5c3RlbXMgRGVwYXJ0bWVudDEYMBYGA1UE 62 | AwwPdGVzdC5yaWVtYW5uLmlvMCAXDTE1MDcxMzIxNDExOVoYDzIyODkwNDI3MjE0 63 | MTE5WjB5MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExGjAYBgNVBAoMEVB1cHB5 64 | IEJ1cmVhdWNyYWN5MScwJQYDVQQLDB5EaXN0cmlidXRlZCBTeXN0ZW1zIERlcGFy 65 | dG1lbnQxGDAWBgNVBAMMD3Rlc3QucmllbWFubi5pbzCCASIwDQYJKoZIhvcNAQEB 66 | BQADggEPADCCAQoCggEBALBnI/EK27+5Mu9tfAHYO7BYzQLUwgFAgaDWv6dXCYhW 67 | y5WJLhjgrqG5cI2q1zVNG1QLVpo/ubHZGOJOQWAAAi/fu4DCm8/76xN4KOkOQsyz 68 | Ie/z3j4ZzqOs8md4mL/74KPPv/cM3TU/OSe+Es4ZZgqiVYR8FErM9ShpdALlye7z 69 | dt844wT9CJno0db8f/fpI9sjeVAdZwp8OmaNiSl5/ccbAOYJg6HlZ3fcWL5pnXpk 70 | p9PM5tYydjflX1BpjGs8aTMK8mi7DfIWSILGEZ3aNn06ug07PB1yPr/6u/54PrB6 71 | yBUbTyqZDI/oIFt+bgKBSNTEcNPqx4xRwxglzlylc7MCAwEAAaNQME4wHQYDVR0O 72 | BBYEFMQZcP7126L2UrXCjjBssMmckHgYMB8GA1UdIwQYMBaAFMQZcP7126L2UrXC 73 | jjBssMmckHgYMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAK9ZUsHY 74 | ltoVsfwVUEpy4eusB6nh1DkprYFL8kGKOdTOlg5qfRTgmKnVPkml4e0tPL+id0Tt 75 | 3oAwTcsKDxy+UPFVSeGvOcpvHN5knrBtV0dIHdOnETxvBVUrQiJYB6JMBFHm17bc 76 | KS/30LruNifqKkXBVdcZ1FyaqI0uzxCfTgfgEQTzmFNqUkGJa5+Sp8fOJNzexSY4 77 | nAPEUp2hxH1tuI9s88sg5IpoAErh4BLd4+JHrz37Mby8VlG1eZ46hxHiklGAgfpy 78 | B0Ze3NuK4Ok51gXH6vjKJMnFdkqYfPfNUtNfaupcOlt0gpOm66H0GCLiz4N0GJPk 79 | Pxn0uYxTvUzuPGI= 80 | -----END CERTIFICATE----- 81 | -------------------------------------------------------------------------------- /test/data/tls/demoCA/newcerts/94FD3EFB7A3D153E.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 10735806336529667390 (0x94fd3efb7a3d153e) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=US, ST=CA, O=Puppy Bureaucracy, OU=Distributed Systems Department, CN=test.riemann.io 7 | Validity 8 | Not Before: Jul 13 21:41:19 2015 GMT 9 | Not After : Apr 27 21:41:19 2289 GMT 10 | Subject: C=US, ST=CA, O=Puppy Bureaucracy, OU=Distributed Systems Department, CN=test.riemann.io 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:b0:67:23:f1:0a:db:bf:b9:32:ef:6d:7c:01:d8: 16 | 3b:b0:58:cd:02:d4:c2:01:40:81:a0:d6:bf:a7:57: 17 | 09:88:56:cb:95:89:2e:18:e0:ae:a1:b9:70:8d:aa: 18 | d7:35:4d:1b:54:0b:56:9a:3f:b9:b1:d9:18:e2:4e: 19 | 41:60:00:02:2f:df:bb:80:c2:9b:cf:fb:eb:13:78: 20 | 28:e9:0e:42:cc:b3:21:ef:f3:de:3e:19:ce:a3:ac: 21 | f2:67:78:98:bf:fb:e0:a3:cf:bf:f7:0c:dd:35:3f: 22 | 39:27:be:12:ce:19:66:0a:a2:55:84:7c:14:4a:cc: 23 | f5:28:69:74:02:e5:c9:ee:f3:76:df:38:e3:04:fd: 24 | 08:99:e8:d1:d6:fc:7f:f7:e9:23:db:23:79:50:1d: 25 | 67:0a:7c:3a:66:8d:89:29:79:fd:c7:1b:00:e6:09: 26 | 83:a1:e5:67:77:dc:58:be:69:9d:7a:64:a7:d3:cc: 27 | e6:d6:32:76:37:e5:5f:50:69:8c:6b:3c:69:33:0a: 28 | f2:68:bb:0d:f2:16:48:82:c6:11:9d:da:36:7d:3a: 29 | ba:0d:3b:3c:1d:72:3e:bf:fa:bb:fe:78:3e:b0:7a: 30 | c8:15:1b:4f:2a:99:0c:8f:e8:20:5b:7e:6e:02:81: 31 | 48:d4:c4:70:d3:ea:c7:8c:51:c3:18:25:ce:5c:a5: 32 | 73:b3 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Subject Key Identifier: 36 | C4:19:70:FE:F5:DB:A2:F6:52:B5:C2:8E:30:6C:B0:C9:9C:90:78:18 37 | X509v3 Authority Key Identifier: 38 | keyid:C4:19:70:FE:F5:DB:A2:F6:52:B5:C2:8E:30:6C:B0:C9:9C:90:78:18 39 | 40 | X509v3 Basic Constraints: 41 | CA:TRUE 42 | Signature Algorithm: sha256WithRSAEncryption 43 | af:59:52:c1:d8:96:da:15:b1:fc:15:50:4a:72:e1:eb:ac:07: 44 | a9:e1:d4:39:29:ad:81:4b:f2:41:8a:39:d4:ce:96:0e:6a:7d: 45 | 14:e0:98:a9:d5:3e:49:a5:e1:ed:2d:3c:bf:a2:77:44:ed:de: 46 | 80:30:4d:cb:0a:0f:1c:be:50:f1:55:49:e1:af:39:ca:6f:1c: 47 | de:64:9e:b0:6d:57:47:48:1d:d3:a7:11:3c:6f:05:55:2b:42: 48 | 22:58:07:a2:4c:04:51:e6:d7:b6:dc:29:2f:f7:d0:ba:ee:36: 49 | 27:ea:2a:45:c1:55:d7:19:d4:5c:9a:a8:8d:2e:cf:10:9f:4e: 50 | 07:e0:11:04:f3:98:53:6a:52:41:89:6b:9f:92:a7:c7:ce:24: 51 | dc:de:c5:26:38:9c:03:c4:52:9d:a1:c4:7d:6d:b8:8f:6c:f3: 52 | cb:20:e4:8a:68:00:4a:e1:e0:12:dd:e3:e2:47:af:3d:fb:31: 53 | bc:bc:56:51:b5:79:9e:3a:87:11:e2:92:51:80:81:fa:72:07: 54 | 46:5e:dc:db:8a:e0:e9:39:d6:05:c7:ea:f8:ca:24:c9:c5:76: 55 | 4a:98:7c:f7:cd:52:d3:5f:6a:ea:5c:3a:5b:74:82:93:a6:eb: 56 | a1:f4:18:22:e2:cf:83:74:18:93:e4:3f:19:f4:b9:8c:53:bd: 57 | 4c:ee:3c:62 58 | -----BEGIN CERTIFICATE----- 59 | MIIDxzCCAq+gAwIBAgIJAJT9Pvt6PRU+MA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV 60 | BAYTAlVTMQswCQYDVQQIDAJDQTEaMBgGA1UECgwRUHVwcHkgQnVyZWF1Y3JhY3kx 61 | JzAlBgNVBAsMHkRpc3RyaWJ1dGVkIFN5c3RlbXMgRGVwYXJ0bWVudDEYMBYGA1UE 62 | AwwPdGVzdC5yaWVtYW5uLmlvMCAXDTE1MDcxMzIxNDExOVoYDzIyODkwNDI3MjE0 63 | MTE5WjB5MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExGjAYBgNVBAoMEVB1cHB5 64 | IEJ1cmVhdWNyYWN5MScwJQYDVQQLDB5EaXN0cmlidXRlZCBTeXN0ZW1zIERlcGFy 65 | dG1lbnQxGDAWBgNVBAMMD3Rlc3QucmllbWFubi5pbzCCASIwDQYJKoZIhvcNAQEB 66 | BQADggEPADCCAQoCggEBALBnI/EK27+5Mu9tfAHYO7BYzQLUwgFAgaDWv6dXCYhW 67 | y5WJLhjgrqG5cI2q1zVNG1QLVpo/ubHZGOJOQWAAAi/fu4DCm8/76xN4KOkOQsyz 68 | Ie/z3j4ZzqOs8md4mL/74KPPv/cM3TU/OSe+Es4ZZgqiVYR8FErM9ShpdALlye7z 69 | dt844wT9CJno0db8f/fpI9sjeVAdZwp8OmaNiSl5/ccbAOYJg6HlZ3fcWL5pnXpk 70 | p9PM5tYydjflX1BpjGs8aTMK8mi7DfIWSILGEZ3aNn06ug07PB1yPr/6u/54PrB6 71 | yBUbTyqZDI/oIFt+bgKBSNTEcNPqx4xRwxglzlylc7MCAwEAAaNQME4wHQYDVR0O 72 | BBYEFMQZcP7126L2UrXCjjBssMmckHgYMB8GA1UdIwQYMBaAFMQZcP7126L2UrXC 73 | jjBssMmckHgYMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAK9ZUsHY 74 | ltoVsfwVUEpy4eusB6nh1DkprYFL8kGKOdTOlg5qfRTgmKnVPkml4e0tPL+id0Tt 75 | 3oAwTcsKDxy+UPFVSeGvOcpvHN5knrBtV0dIHdOnETxvBVUrQiJYB6JMBFHm17bc 76 | KS/30LruNifqKkXBVdcZ1FyaqI0uzxCfTgfgEQTzmFNqUkGJa5+Sp8fOJNzexSY4 77 | nAPEUp2hxH1tuI9s88sg5IpoAErh4BLd4+JHrz37Mby8VlG1eZ46hxHiklGAgfpy 78 | B0Ze3NuK4Ok51gXH6vjKJMnFdkqYfPfNUtNfaupcOlt0gpOm66H0GCLiz4N0GJPk 79 | Pxn0uYxTvUzuPGI= 80 | -----END CERTIFICATE----- 81 | -------------------------------------------------------------------------------- /src/riemann/pool.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.pool 2 | "A generic thread-safe resource pool." 3 | (:use clojure.tools.logging 4 | [slingshot.slingshot :only [throw+]]) 5 | (:import [java.util.concurrent LinkedBlockingQueue TimeUnit])) 6 | 7 | ; THIS IS A MUTABLE STATE OF AFFAIRS. WHICH IS TO SAY, IT IS FUCKING TERRIBLE. 8 | 9 | (defprotocol Pool 10 | (grow [pool] 11 | "Adds an element to the pool.") 12 | (claim [pool] [pool timeout] 13 | "Take a thingy from the pool. Timeout in seconds; if unspecified, 0. 14 | Returns nil if no thingy available.") 15 | (release [pool thingy] 16 | "Returns a thingy to the pool.") 17 | (invalidate [pool thingy] 18 | "Tell the pool a thingy is no longer valid.")) 19 | 20 | (defrecord FixedQueuePool [queue open close regenerate-interval] 21 | Pool 22 | (grow [this] 23 | (loop [] 24 | (if-let [thingy (try (open) (catch Throwable t nil))] 25 | (.put ^LinkedBlockingQueue queue thingy) 26 | (do 27 | (Thread/sleep (* 1000 regenerate-interval)) 28 | (recur))))) 29 | 30 | (claim [this] 31 | (claim this nil)) 32 | 33 | (claim [this timeout] 34 | (let [timeout (* 1000 (or timeout 0))] 35 | (or 36 | (try 37 | (.poll ^LinkedBlockingQueue queue timeout TimeUnit/MILLISECONDS) 38 | (catch java.lang.InterruptedException e 39 | nil)) 40 | (throw+ 41 | {:type ::timeout 42 | :message (str "Couldn't claim a resource from the pool within " 43 | timeout " ms")})))) 44 | 45 | (release [this thingy] 46 | (when thingy 47 | (.put ^LinkedBlockingQueue queue thingy))) 48 | 49 | (invalidate [this thingy] 50 | (when thingy 51 | (try (close thingy) 52 | (catch Throwable t 53 | (warn t "Closing" thingy "threw"))) 54 | (future (grow this))))) 55 | 56 | (defn fixed-pool 57 | "A fixed pool of thingys. (open) is called to generate a thingy. (close 58 | thingy) is called when a thingy is invalidated. When thingys are invalidated, 59 | the pool will immediately try to open a new one; if open throws or returns 60 | nil, the pool will sleep for regenerate-interval seconds before retrying 61 | (open). 62 | 63 | :regenerate-interval How long to wait between retrying (open). 64 | :size Number of thingys in the pool. 65 | :block-start Should (fixed-pool) wait until the pool is full 66 | before returning? 67 | 68 | Note that fixed-pool is correct only if every successful (claim) is followed 69 | by exactly one of either (invalidate) or (release). If calls are unbalanced; 70 | e.g. resources are not released, doubly released, or released *and* 71 | invalidated, starvation or unbounded blocking could occur. (with-pool) 72 | provides this guarantee." 73 | ([open] 74 | (fixed-pool open {})) 75 | ([open opts] 76 | (fixed-pool open identity opts)) 77 | ([open close opts] 78 | (let [^int size (or (:size opts) (* 2 (.availableProcessors 79 | (Runtime/getRuntime)))) 80 | regenerate-interval (or (:regenerate-interval opts) 5) 81 | block-start (get opts :block-start true) 82 | pool (FixedQueuePool. 83 | (LinkedBlockingQueue. size) 84 | open 85 | close 86 | regenerate-interval) 87 | openers (doall 88 | (map (fn open-pool [_] 89 | (future (grow pool))) 90 | (range size)))] 91 | (when block-start 92 | (doseq [worker openers] @worker)) 93 | pool))) 94 | 95 | (defmacro with-pool 96 | "Evaluates body in a try expression with a symbol 'thingy claimed from the 97 | given pool, with specified claim timeout. Releases thingy at the end of the 98 | body, or if an exception is thrown, invalidates them and rethrows. Example: 99 | 100 | ; With client, taken from connection-pool, waiting 5 seconds to claim, send 101 | ; client a message. 102 | (with-pool [client connection-pool 5] 103 | (send client a-message))" 104 | [[thingy pool timeout] & body] 105 | ; Destructuring bind could change nil to a, say, vector, and cause 106 | ; unbalanced claim/release. 107 | `(let [thingy# (claim ~pool ~timeout) 108 | ~thingy thingy#] 109 | (try 110 | (let [res# (do ~@body)] 111 | (release ~pool thingy#) 112 | res#) 113 | (catch Throwable t# 114 | (invalidate ~pool thingy#) 115 | (throw t#))))) 116 | -------------------------------------------------------------------------------- /src/riemann/kairosdb.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.kairosdb 2 | "Forwards events to KairosDB." 3 | (:refer-clojure :exclude [replace]) 4 | (:import 5 | (java.net Socket 6 | DatagramSocket 7 | DatagramPacket 8 | InetAddress) 9 | (java.io Writer OutputStreamWriter)) 10 | (:use [clojure.string :only [split join replace]] 11 | clojure.tools.logging 12 | riemann.pool 13 | riemann.common)) 14 | 15 | (defprotocol KairosDBClient 16 | (open [client] 17 | "Creates a KairosDB client") 18 | (send-line [client line] 19 | "Sends a formatted line to KairosDB") 20 | (close [client] 21 | "Cleans up (closes sockets etc.)")) 22 | 23 | (defrecord KairosDBTelnetClient [^String host ^int port] 24 | KairosDBClient 25 | (open [this] 26 | (let [sock (Socket. host port)] 27 | (assoc this 28 | :socket sock 29 | :out (OutputStreamWriter. (.getOutputStream sock))))) 30 | (send-line [this line] 31 | (let [out (:out this)] 32 | (.write ^OutputStreamWriter out ^String line) 33 | (.flush ^OutputStreamWriter out))) 34 | (close [this] 35 | (.close ^OutputStreamWriter (:out this)) 36 | (.close ^Socket (:socket this)))) 37 | 38 | (defn kairosdb-metric-name 39 | "Constructs a metric-name for an event." 40 | [event] 41 | (let [service (:service event) 42 | split-service (if service (split service #" ") [])] 43 | (join "." split-service))) 44 | 45 | (defn kairosdb-tags 46 | "Constructs tags from an event. 47 | Fqdn in kairosdb is usually passed as a tag." 48 | [event] 49 | (if (contains? event :host) 50 | {:fqdn (:host event)} 51 | {})) 52 | 53 | (defn kairosdb 54 | "Returns a function which accepts an event and sends it to KairosDB. 55 | Silently drops events when KairosDB is down. Attempts to reconnect 56 | automatically every five seconds. Use: 57 | 58 | (kairosdb {:host \"kairosdb.local\" :port 4242}) 59 | 60 | Options: 61 | 62 | :metric-name A function which, given an event, returns the string describing 63 | the path of that event in kairosdb. kairosdb-metric-name by 64 | default. 65 | 66 | :tags A function which, given an event, returns the hash-map for the tags. 67 | kairosdb-tags by default. 68 | 69 | :pool-size The number of connections to keep open. Default 4. 70 | 71 | :reconnect-interval How many seconds to wait between attempts to connect. 72 | Default 5. 73 | 74 | :claim-timeout How many seconds to wait for a kairosdb connection from 75 | the pool. Default 0.1. 76 | 77 | :block-start Wait for the pool's initial connections to open 78 | before returning." 79 | [opts] 80 | (let [opts (merge {:host "127.0.0.1" 81 | :port 4242 82 | :protocol :tcp 83 | :claim-timeout 0.1 84 | :pool-size 4 85 | :tags kairosdb-tags 86 | :metric-name kairosdb-metric-name} opts) 87 | pool (fixed-pool 88 | (fn [] 89 | (info "Connecting to " (select-keys opts [:host :port])) 90 | (let [host (:host opts) 91 | port (:port opts) 92 | client (open (KairosDBTelnetClient. host port))] 93 | (info "Connected") 94 | client)) 95 | (fn [client] 96 | (info "Closing connection to " 97 | (select-keys opts [:host :port])) 98 | (close client)) 99 | (-> opts 100 | (select-keys [:block-start]) 101 | (assoc :size (:pool-size opts)) 102 | (assoc :regenerate-interval (:reconnect-interval opts)))) 103 | metric-name (:metric-name opts) 104 | tags (:tags opts)] 105 | 106 | (fn [event] 107 | (when (:metric event) 108 | (when (:service event) 109 | (with-pool [client pool (:claim-timeout opts)] 110 | (let [string (str (join " " (concat ["put" 111 | (metric-name event) 112 | (long (* 1000 (:time event))) 113 | (float (:metric event))] 114 | (map 115 | (fn [e] (format "%s=%s" (name (key e)) (val e))) 116 | (tags event)))) 117 | "\n")] 118 | (send-line client string)))))))) 119 | -------------------------------------------------------------------------------- /src/riemann/opentsdb.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.opentsdb 2 | "Forwards events to OpenTSDB." 3 | (:refer-clojure :exclude [replace]) 4 | (:import 5 | (java.net Socket 6 | DatagramSocket 7 | DatagramPacket 8 | InetAddress) 9 | (java.io Writer OutputStreamWriter)) 10 | (:use [clojure.string :only [split join replace]] 11 | clojure.tools.logging 12 | riemann.pool 13 | riemann.common)) 14 | 15 | (defprotocol OpenTSDBClient 16 | (open [client] 17 | "Creates a OpenTSDB client") 18 | (send-line [client line] 19 | "Sends a formatted line to OpenTSDB") 20 | (close [client] 21 | "Cleans up (closes sockets etc.)")) 22 | 23 | (defrecord OpenTSDBTelnetClient [^String host ^int port] 24 | OpenTSDBClient 25 | (open [this] 26 | (let [sock (Socket. host port)] 27 | (assoc this 28 | :socket sock 29 | :out (OutputStreamWriter. (.getOutputStream sock))))) 30 | (send-line [this line] 31 | (let [out (:out this)] 32 | (.write ^OutputStreamWriter out ^String line) 33 | (.flush ^OutputStreamWriter out))) 34 | (close [this] 35 | (.close ^OutputStreamWriter (:out this)) 36 | (.close ^Socket (:socket this)))) 37 | 38 | (defn opentsdb-metric-name 39 | "Constructs a metric-name for an event." 40 | [event] 41 | (let [service (:service event) 42 | split-service (if service (split service #" ") [])] 43 | (join "." split-service))) 44 | 45 | (defn opentsdb-tags 46 | "Constructs a tag-key named host from an event. 47 | tcollector also generates tag-key named host" 48 | [event] 49 | (if (contains? event :host) 50 | {:host (:host event)} 51 | {})) 52 | 53 | (defn opentsdb 54 | "Returns a function which accepts an event and sends it to OpenTSDB. 55 | Silently drops events when OpenTSDB is down. Attempts to reconnect 56 | automatically every five seconds. Use: 57 | 58 | (opentsdb {:host \"opentsdb.local\" :port 4242}) 59 | 60 | Options: 61 | 62 | :metric-name A function which, given an event, returns the string describing 63 | the path of that event in opentsdb. opentsdb-metric-name by 64 | default. 65 | 66 | :tags A function which, given an event, returns the hash-map for the tags. 67 | opentsdb-tags by default. 68 | 69 | :pool-size The number of connections to keep open. Default 4. 70 | 71 | :reconnect-interval How many seconds to wait between attempts to connect. 72 | Default 5. 73 | 74 | :claim-timeout How many seconds to wait for a opentsdb connection from 75 | the pool. Default 0.1. 76 | 77 | :block-start Wait for the pool's initial connections to open 78 | before returning." 79 | [opts] 80 | (let [opts (merge {:host "127.0.0.1" 81 | :port 4242 82 | :protocol :tcp 83 | :claim-timeout 0.1 84 | :pool-size 4 85 | :tags opentsdb-tags 86 | :metric-name opentsdb-metric-name} opts) 87 | pool (fixed-pool 88 | (fn [] 89 | (info "Connecting to " (select-keys opts [:host :port])) 90 | (let [host (:host opts) 91 | port (:port opts) 92 | client (open (OpenTSDBTelnetClient. host port))] 93 | (info "Connected") 94 | client)) 95 | (fn [client] 96 | (info "Closing connection to " 97 | (select-keys opts [:host :port])) 98 | (close client)) 99 | (-> opts 100 | (select-keys [:block-start]) 101 | (assoc :size (:pool-size opts)) 102 | (assoc :regenerate-interval (:reconnect-interval opts)))) 103 | metric-name (:metric-name opts) 104 | tags (:tags opts)] 105 | 106 | (fn [event] 107 | (when (:metric event) 108 | (when (:service event) 109 | (with-pool [client pool (:claim-timeout opts)] 110 | (let [string (str (join " " (concat ["put" 111 | (metric-name event) 112 | (long (:time event)) 113 | (float (:metric event))] 114 | (map 115 | (fn [e] (format "%s=%s" (name (key e)) (val e))) 116 | (tags event)))) 117 | "\n")] 118 | (send-line client string)))))))) 119 | -------------------------------------------------------------------------------- /pkg/deb/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: riemann 4 | # Required-Start: $remote_fs $syslog 5 | # Required-Stop: $remote_fs $syslog 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Riemann server 9 | # Description: The Riemann monitoring system's event processor. 10 | ### END INIT INFO 11 | 12 | # Author: Kyle Kingsbury 13 | 14 | # Do NOT "set -e" 15 | 16 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 17 | PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin 18 | DESC="Riemann" 19 | NAME=riemann 20 | DAEMON=/usr/bin/riemann 21 | DAEMON_USER=riemann 22 | PIDFILE=/var/run/$NAME.pid 23 | SCRIPTNAME=/etc/init.d/$NAME 24 | 25 | # Exit if the package is not installed 26 | [ -x "$DAEMON" ] || exit 0 27 | 28 | # Read configuration variable file if it is present 29 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 30 | 31 | RIEMANN_PATH_CONF=${RIEMANN_PATH_CONF:-/etc/${NAME}} 32 | RIEMANN_CONFIG=${RIEMANN_CONFIG:-${RIEMANN_PATH_CONF}/riemann.config} 33 | 34 | DAEMON_OPTS="${RIEMANN_OPTS} ${RIEMANN_CONFIG}" 35 | 36 | # Load the VERBOSE setting and other rcS variables 37 | . /lib/init/vars.sh 38 | 39 | # Define LSB log_* functions. 40 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present 41 | # and status_of_proc is working. 42 | . /lib/lsb/init-functions 43 | 44 | # Function that starts the daemon/service 45 | do_start() 46 | { 47 | # Return 48 | # 0 if daemon has been started 49 | # 1 if daemon was already running 50 | # 2 if daemon could not be started 51 | pid=$( pidofproc -p $PIDFILE "$NAME") 52 | if [ -n "$pid" ] ; then 53 | log_daemon_msg "Riemann is already running (PID `cat ${PIDFILE}`)" 54 | return 1 55 | fi 56 | start-stop-daemon --start --quiet --chuid $DAEMON_USER --chdir / --make-pidfile --background --pidfile $PIDFILE --exec $DAEMON -- \ 57 | $DAEMON_OPTS \ 58 | || return 2 59 | # Add code here, if necessary, that waits for the process to be ready 60 | # to handle requests from services started subsequently which depend 61 | # on this one. As a last resort, sleep for some time. 62 | } 63 | 64 | # Function that stops the daemon/service 65 | do_stop() 66 | { 67 | # Return 68 | # 0 if daemon has been stopped 69 | # 1 if daemon was already stopped 70 | # 2 if daemon could not be stopped 71 | # other if a failure occurred 72 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE 73 | RETVAL="$?" 74 | [ "$RETVAL" = 2 ] && return 2 75 | # Wait for children to finish too if this is a daemon that forks 76 | # and if the daemon is only ever run from this initscript. 77 | # If the above conditions are not satisfied then add some other code 78 | # that waits for the process to drop all resources that could be 79 | # needed by services started subsequently. A last resort is to 80 | # sleep for some time. 81 | start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON 82 | [ "$?" = 2 ] && return 2 83 | # Many daemons don't delete their pidfiles when they exit. 84 | rm -f $PIDFILE 85 | return "$RETVAL" 86 | } 87 | 88 | # Function that sends a SIGHUP to the daemon/service 89 | do_reload() { 90 | # 91 | # If the daemon can reload its configuration without 92 | # restarting (for example, when it is sent a SIGHUP), 93 | # then implement that here. 94 | # 95 | start-stop-daemon --stop --quiet --signal HUP --pidfile $PIDFILE 96 | return $? 97 | } 98 | 99 | case "$1" in 100 | start) 101 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" 102 | do_start 103 | case "$?" in 104 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 105 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 106 | esac 107 | ;; 108 | stop) 109 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 110 | do_stop 111 | case "$?" in 112 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 113 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 114 | esac 115 | ;; 116 | status) 117 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 118 | ;; 119 | reload|force-reload) 120 | log_daemon_msg "Reloading $DESC" "$NAME" 121 | do_reload 122 | log_end_msg $? 123 | ;; 124 | restart) 125 | log_daemon_msg "Restarting $DESC" "$NAME" 126 | do_stop 127 | case "$?" in 128 | 0|1) 129 | do_start 130 | case "$?" in 131 | 0) log_end_msg 0 ;; 132 | 1) log_end_msg 1 ;; # Old process is still running 133 | *) log_end_msg 1 ;; # Failed to start 134 | esac 135 | ;; 136 | *) 137 | # Failed to stop 138 | log_end_msg 1 139 | ;; 140 | esac 141 | ;; 142 | *) 143 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload|force-reload}" >&2 144 | exit 3 145 | ;; 146 | esac 147 | 148 | : 149 | -------------------------------------------------------------------------------- /test/riemann/slack_test.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.slack-test 2 | (:use clojure.test) 3 | (:require [riemann.logging :as logging] 4 | [riemann.slack :as slack] 5 | [clj-http.client :as client])) 6 | 7 | 8 | (def api-key (System/getenv "SLACK_API_KEY")) 9 | (def room (System/getenv "SLACK_ALERT_ROOM")) 10 | (def account (System/getenv "SLACK_ALERT_ACCOUNT")) 11 | (def user "Riemann_Slack_Test") 12 | 13 | (when-not api-key 14 | (println "export SLACK_API_KEY=\"...\" to run these tests.")) 15 | 16 | (when-not room 17 | (println "export SLACK_ALERT_ROOM=\"...\" to run these tests.")) 18 | 19 | (when-not account 20 | (println "export SLACK_ALERT_ACCOUNT=\"...\" to run these tests.")) 21 | 22 | (logging/init) 23 | 24 | (deftest ^:slack ^:integration test_event 25 | (let [slack_connect (slack/slack account api-key user room)] 26 | (slack_connect {:host "localhost" 27 | :service "good event test" 28 | :description "Testing slack.com alerts from riemann" 29 | :metric 42 30 | :state "ok"}))) 31 | 32 | (defn- capture-post [result-atom url params] 33 | (reset! result-atom {:url url, :body (get-in params [:form-params :payload])})) 34 | 35 | (def ^:private any-account {:account "any", :token "any"}) 36 | (defn- with-formatter [formatter-fn] 37 | {:username "any", :channel "any", :formatter formatter-fn}) 38 | 39 | (deftest slack 40 | (let [post-request (atom {})] 41 | (with-redefs [client/post (partial capture-post post-request)] 42 | 43 | (testing "forms correct slack URL" 44 | (let [slacker (slack/slack "test-account" "test-token" "any" "any")] 45 | (slacker {}) 46 | (is (= (:url @post-request) 47 | "https://test-account.slack.com/services/hooks/incoming-webhook?token=test-token")))) 48 | 49 | (testing "formats event by default" 50 | (let [slacker (slack/slack "any" "any" "test-user" "#test-channel")] 51 | (slacker {:host "localhost", :service "mailer", :state "error", 52 | :description "Mailer failed", :metric 42, :tags ["first", "second"]}) 53 | (is (= (:body @post-request) 54 | "{\"attachments\":[{\"fields\":[{\"title\":\"Riemann Event\",\"value\":\"Host: localhost\\nService: mailer\\nState: error\\nDescription: Mailer failed\\nMetric: 42\\nTag: -\\n\",\"short\":true}]}],\"channel\":\"#test-channel\",\"username\":\"test-user\",\"icon_emoji\":\":warning:\"}")))) 55 | 56 | (testing "allows formatting characters in main message text with custom formatter" 57 | (let [formatter (fn [e] {:text (str "")}) 58 | slacker (slack/slack any-account (with-formatter formatter))] 59 | (slacker {:service "my-service"}) 60 | (is (seq (re-seq #"" (:body @post-request)))))) 61 | 62 | (testing "allows for empty message text" 63 | (let [slacker (slack/slack any-account (with-formatter (constantly {})))] 64 | (slacker {:host "empty"}) 65 | (is (= (:body @post-request) 66 | (str "{\"channel\":\"any\"," 67 | "\"username\":\"any\"," 68 | "\"icon_emoji\":\":warning:\"}"))))) 69 | 70 | (testing "specifies username, channel and icon when initializing slacker" 71 | (let [slacker (slack/slack any-account 72 | {:username "test-user", :channel "#test-channel", :icon ":ogre:" 73 | :formatter (constantly {})})] 74 | (slacker [{:host "localhost", :service "mailer"}]) 75 | (is (= (:body @post-request) 76 | (str "{\"channel\":\"#test-channel\"," 77 | "\"username\":\"test-user\"," 78 | "\"icon_emoji\":\":ogre:\"}"))))) 79 | 80 | (testing "formats multiple events with a custom formatter" 81 | (let [slacker (slack/slack 82 | {:account "any", :token "any"} 83 | {:username "test-user", :channel "#test-channel", :icon ":ogre:" 84 | :formatter (fn [events] 85 | {:text (apply str (map #(str (:tags %)) events)) 86 | :icon ":ship:" 87 | :username "another-user" 88 | :channel "#another-channel" 89 | :attachments [{:pretext "pretext"}]})})] 90 | (slacker [{:host "localhost", :service "mailer", :tags ["first" "second"]} 91 | {:host "localhost", :service "mailer", :tags ["third" "fourth"]}]) 92 | (is (= (:body @post-request) 93 | (str "{\"attachments\":[{\"pretext\":\"pretext\"}]," 94 | "\"text\":\"[\\\"first\\\" \\\"second\\\"][\\\"third\\\" \\\"fourth\\\"]\"," 95 | "\"channel\":\"#another-channel\"," 96 | "\"username\":\"another-user\"," 97 | "\"icon_emoji\":\":ship:\"}")))))))) 98 | -------------------------------------------------------------------------------- /src/riemann/librato.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.librato 2 | "Forwards events to Librato Metrics." 3 | (:require [clojure.string :as string]) 4 | (:use [clj-librato.metrics :only [collate annotate connection-manager 5 | update-annotation]] 6 | clojure.math.numeric-tower)) 7 | 8 | (defn safe-name 9 | "Converts a string into a safe name for Librato's metrics and streams. 10 | Converts spaces to periods, preserves only A-Za-z0-9.:-_, and cuts to 255 11 | characters." 12 | [s] 13 | (when s 14 | (let [s (string/replace s " " ".") 15 | s (string/replace s #"[^-.:_\w]" "")] 16 | (subs s 0 (min 255 (count s)))))) 17 | 18 | (defn event->gauge 19 | "Converts an event to a gauge." 20 | [event] 21 | {:name (safe-name (:service event)) 22 | :source (safe-name (:host event)) 23 | :value (:metric event) 24 | :measure-time (round (:time event))}) 25 | 26 | (def event->counter event->gauge) 27 | 28 | (defn event->annotation 29 | "Converts an event to an annotation." 30 | [event] 31 | (into {} 32 | (filter second 33 | {:name (safe-name (:service event)) 34 | :title (string/join 35 | " " [(:service event) (:state event)]) 36 | :source (safe-name (:host event)) 37 | :description (:description event) 38 | :start-time (round (:time event)) 39 | :end-time (when (:end-time event) (round (:end-time event)))} 40 | ))) 41 | 42 | (defn librato-metrics 43 | "Creates a librato metrics adapter. Takes your username and API key, and 44 | returns a map of streams: 45 | 46 | :gauge 47 | :counter 48 | :annotation 49 | :start-annotation 50 | :end-annotation 51 | 52 | Gauge and counter submit events as measurements. Annotation creates an 53 | annotation from the given event; it will have only a start time unless 54 | :end-time is given. :start-annotation will *start* an annotation; the 55 | annotation ID for that host and service will be remembered. :end-annotation 56 | will submit an end-time for the most recent annotation submitted with 57 | :start-annotation. 58 | 59 | Example: 60 | 61 | (def librato (librato-metrics \"aphyr@aphyr.com\" \"abcd01234...\")) 62 | 63 | (tagged \"latency\" 64 | (fixed-event-window 50 (librato :gauge))) 65 | 66 | (where (service \"www\") 67 | (changed-state 68 | (where (state \"ok\") 69 | (:start-annotation librato) 70 | (else 71 | (:end-annotation librato)))))" 72 | ([user api-key] 73 | (librato-metrics user api-key {:threads 4})) 74 | ([user api-key connection-mgr-options] 75 | (let [annotation-ids (atom {}) 76 | http-options {:connection-manager 77 | (connection-manager connection-mgr-options)}] 78 | {::http-options http-options 79 | :gauge (fn [& args] 80 | (let [data (first args) 81 | events (if (vector? data) data [data]) 82 | gauges (map event->gauge events)] 83 | (collate user api-key gauges [] http-options) 84 | (last gauges))) 85 | 86 | :counter (fn [& args] 87 | (let [data (first args) 88 | events (if (vector? data) data [data]) 89 | counters (map event->counter events)] 90 | (collate user api-key [] counters http-options) 91 | (last counters))) 92 | 93 | :annotation (fn [event] 94 | (let [a (event->annotation event)] 95 | (annotate user api-key (:name a) 96 | (dissoc a :name) 97 | http-options))) 98 | 99 | :start-annotation (fn [event] 100 | (let [a (event->annotation event) 101 | res (annotate user api-key (:name a) 102 | (dissoc a :name) 103 | http-options)] 104 | (swap! annotation-ids assoc 105 | [(:host event) (:service event)] (:id res)) 106 | res)) 107 | 108 | :end-annotation (fn [event] 109 | (let [id ((deref annotation-ids) 110 | [(:host event) (:service event)]) 111 | a (event->annotation event)] 112 | (when id 113 | (let [res (update-annotation 114 | user api-key (:name a) id 115 | {:end-time (round (:time event))} 116 | http-options)] 117 | (swap! annotation-ids dissoc 118 | [(:host event) (:service event)]) 119 | res))))}))) 120 | -------------------------------------------------------------------------------- /src/riemann/slack.clj: -------------------------------------------------------------------------------- 1 | (ns riemann.slack 2 | "Post alerts to slack.com" 3 | (:require [clj-http.client :as client] 4 | [cheshire.core :as json]) 5 | (:use [clojure.string :only [escape join upper-case]])) 6 | 7 | (defn slack-escape [message] 8 | "Escape message according to slack formatting spec." 9 | (escape message {\< "<" \> ">" \& "&"})) 10 | 11 | (defn default-formatter [events] 12 | {:attachments 13 | [{:fields 14 | [{:title "Riemann Event" 15 | :value (slack-escape 16 | (str "Host: " (or (:host events) "-") "\n" 17 | "Service: " (or (:service events) "-") "\n" 18 | "State: " (or (:state events) "-") "\n" 19 | "Description: " (or (:description events) "-") "\n" 20 | "Metric: " (or (:metric events) "-") "\n" 21 | "Tag: " (or (:tag events) "-") "\n")) 22 | :short true}]}]}) 23 | 24 | (defn extended-formatter [events] 25 | {:text "This event requires your attention!", 26 | :attachments 27 | [{:fallback 28 | (slack-escape 29 | (str 30 | "*Service:* " 31 | (:service events) 32 | "*Description:* " 33 | (:description events) 34 | " *Host:* " 35 | (:host events) 36 | " *Metric:* " 37 | (:metric events) 38 | " *State:* " 39 | (:state events))), 40 | :text (slack-escape (or (:description events) "")), 41 | :pretext "Event Details:", 42 | :color 43 | (case (:state events) "ok" "good" "critical" "danger" "warning"), 44 | :fields 45 | [{:title "Host", 46 | :value (slack-escape (or (:host events) "-")), 47 | :short true} 48 | {:title "Service", 49 | :value (slack-escape (or (:service events) "-")), 50 | :short true} 51 | {:title "Metric", :value (or (:metric events) "-"), :short true} 52 | {:title "State", 53 | :value (slack-escape (or (:state events) "-")), 54 | :short true} 55 | {:title "Description", 56 | :value (slack-escape (or (:description events) "-")) 57 | :short true} 58 | {:title "Tags", 59 | :value (slack-escape (or (:tag events) "-")) 60 | :short true}]}]}) 61 | 62 | (defn slack 63 | "Posts events into a slack.com channel using Incoming Webhooks. 64 | Takes your account name, webhook token, bot username and channel name. 65 | Returns a function that will post a message into slack.com channel: 66 | 67 | (def credentials {:account \"some_org\", :token \"53CR3T\"}) 68 | (def slacker (slack credentials {:username \"Riemann bot\" 69 | :channel \"#monitoring\" 70 | :icon \":smile:\"})) 71 | 72 | (by [:service] slacker) 73 | 74 | Hint: token is the last part of the webhook URL that Slack gives you. 75 | https://hooks.slack.com/services/QWERSAFG0/AFOIUYTQ48/120984SAFJSFR 76 | Token in this case would be 120984SAFJSFR 77 | 78 | You can also supply a custom formatter for formatting events into Slack 79 | messages. Formatter result may contain: 80 | 81 | * `username` - overrides the username provided upon construction 82 | * `channel` - overrides the channel provided upon construction 83 | * `icon` - overrides the icon provided upon construction 84 | * `text` - main text formatted using Slack markup 85 | * `attachments` - array of attachments according to https://api.slack.com/docs/attachments 86 | 87 | (def slacker (slack credentials {:username \"Riemann bot\", :channel \"#monitoring\" 88 | :formatter (fn [e] {:text (:state e) 89 | :icon \":happy:\"}))) 90 | 91 | You can use `slack` inside of a grouping function which produces a seq of 92 | events, like `rollup`: 93 | 94 | (def slacker (slack credentials {:username \"Riemann bot\", :channel \"#monitoring\" 95 | :formatter (fn [es] {:text (apply str (map :state es))}))) 96 | 97 | (rollup 5 60 slacker) 98 | " 99 | ([account_name token username channel] (slack {:account account_name, :token token} 100 | {:username username, :channel channel})) 101 | ([{:keys [webhook_uri account token]} 102 | {:keys [username channel icon formatter] :or {formatter default-formatter}}] 103 | (fn [events] 104 | (let [{:keys [text attachments] :as result} (formatter events) 105 | icon (:icon result (or icon ":warning:")) 106 | channel (:channel result channel) 107 | username (:username result username)] 108 | (client/post (if webhook_uri 109 | webhook_uri 110 | (str "https://" account ".slack.com/services/hooks/incoming-webhook?token=" token)) 111 | {:form-params 112 | {:payload (json/generate-string 113 | (merge 114 | {:channel channel, :username username, :icon_emoji icon} 115 | (when text {:text text}) 116 | (dissoc result :icon :text)))}}))))) 117 | --------------------------------------------------------------------------------