A notifications gateway for helping your little robot friends to be heard.
You need sbcl
, Steel Bank Common Lisp. Once you have that:
sbcl --script run.lisp
Production? Well why didn't you say so? I recommend our BOSH release; it's very popular this time of year.
We also have a Docker image, huntprod/shout, if that's your cup of tea.
Don't get mad. We use curl.
First, upload the rules:
curl -X POST http://username:password@localhost:7109/rules \
--data-binary @path/to/rules-file
After that, you can create events:
curl -X POST http://username:password@localhost:7109/events --data-binary '{
"topic" : "some-pipeline",
"ok" : true,
"message" : "Pipeline build #367 succeeded",
"link" : "https://ci/p/some-pipeline/367"
}'
Shout! keeps track of the ok
-ness of each topic you create.
Whenever transitions occur, either a failure (ok -> not ok) or a recovery (the
opposite), a notification is sent.
By default, the username:password
pair is shout:shout
, but this can be
overridden by setting the SHOUT_OPS_AUTH
environment variable for the
operations endpoints (/events, /state, and /states) and SHOUT_ADMIN_AUTH
for
admin endpoint (/rules) before starting the Shout! process.
You sure do like production.
For real-world use, you probably just want to hook this up to your pipelines via our Concourse resource. It's the chef's specialty.
It looks a little something like this:
resource_types:
- name: shout
type: docker-image
source:
repository: huntprod/shout-resource
tag: latest
resources:
- name: shout-out
type: shout
source:
url: http://your-shout-ip:7109
and then, whenever you need to notify (i.e. on_failure
):
on_failure:
put: shout-out
params:
topic: $BUILD_PIPELINE_NAME
message: Pipeline failed.
link: https://ci/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME
And you're off to the races! Err... pipelines.
Shout! isn't terribly useful without some notification rules. The
api:run
function takes a keyword argument called :rules
that
lets you specify the path to a file containing these rules:
(api:run :rules #p"/path/to/rules")
(That #p
bit before the string is just Common Lisp's way of
saying what follows is a filesystem path, for handling diverse
operating systems like Windows and UNIX)
The contents of the rules file is a bunch of Lisp forms that implement a script in a small domain-specific language. It looks like this:
; comments start with ';' and continue
; until the end of the line.
; (blank lines like ^^ are ignored)
((set a-variable "some value")
(set webhook "https://slack.web.hook/...")
; SET creates a new variable and sets its value.
; you can refer to these values later, in handlers.
(for "a-topic" ; the following rules only apply to the
; topic 'a-topic'
(when ((on weekdays)
(from 0800 am to 0500 pm))
; the rest of this WHEN clause identifies
; how to notify, if the FOR and WHEN parameters
; are met.
;; if this is still broken in a half hour,
;; notify me again (re-evaluating rules)
(remind 30 minutes)
;; send a slack notification
(slack :webhook "$webhook"
:text "$topic is $status: $message"
:color "#127abd")))
(for * ; matches everything
(when * ; matches any time
;; a fancier slack message
(slack :webhook "$webhook"
:text "$topic is $status"
:color (if ok? "good" "danger")
:attach (if link "$message <$link>"
"$message")))))
Shout! stops looking for FOR matches once it finds one. Likewise, it stops evaluating WHEN rules once a match is found.
Shout! interpolates two embedded forms of variable references in strings, under certain conditions.
"$link"
This is called a builtin variable, and there are fixed number of
these. For break/fix notifications, $message
, $topic
,
$status
, and $link
.
"$[meta-data]"
This references metadata supplied to an event or announcement by the end user.
-
(for topic-condition when-clause...)
Creates a topic-match handler, which will fire if and when topic-condition is met. If the condition is a quoted string, the handler matches that topic and that topic only.
The condition can be a compound expression, using logic operators like
(or ...)
,(and ...)
and(not ...)
. -
(set variable-name value)
Defines a variable, named variable-name, and assigns it the given value. value can be a complex expression, or a map value.
-
"a literal string"
(top-level only)Shorthand for
(is "a literal string")
. This cannot be combined with any other topic condition form. -
*
(top-level only)Matches everything, implicitly. This cannot be combined with any other topic condition form.
-
expr
A general expression, which is evaluated. If the expression returns true, this FOR handler will fire, and no other handlers will be considered.
A FOR handler can have any number of WHEN clauses, which associate a time-of-day assertion with one or more calls to notification methods, like Slack or email.
The first argument to a (when ...)
form is a list of general expressions,
implicitly wrapped in an (and ...)
.
(for "stuff"
(when ((on weekdays)
(from 0800 am to 0500 pm))
(slack ...)))
More complicated guards are possible:
(for "stuff"
(when ((and (or (after 0500 pm)
(before 1000 am))
(or (on monday)
(on wednesday)
(on friday))))
(slack ...)))
Inside of the argument list of a notification form, like (slack ...)
,
you can use expressions to do more than just settle for hard-coded
values.
The following expressions are defined:
-
(if test then-form else-form)
Evaluates test; if it evaluates to true, the then-form is evaluated; otherwise the else-form is evaluated (if present). The value of the
(if ...)
expression is the value of the evaluated sub-form.For example:
(if ok? "good" "bad")
Will either be
"good"
(ifok?
is true) or"bad"
. -
(not expr)
Evaluates the given expr and then negates its value. True becomes false; false becomes true.
-
(and expr...)
Evaluate each expr in turn, and return true if all expressions evaluate to true. Evaluation stops on the first non-true expression, and false is returned.
-
(or expr ...)
Evaluates each expr in turn, and return true if any expression evaluates to true. Evaluation stops on the first true expression. If none of the expressions evaluate to true, false is returned.
-
(value variable-name)
Evaluates to the value of the variable variable-name.
An error is signaled if the variable is not
set
. -
(lookup map-var-name key...)
Evaluates each
key
(there can be multiple), in order, and attempts to look up the corresponding value in the map variable map-var-name.The first found value will be returned.
Errors are signaled if map-var-name isn't a map variable, or if no value could be found for any of the given keys.
-
(metadata? var-name)
Returns true if the event in question came with the metadata variable var-name set.
-
(metadata var-name)
Returns the value of the metadata variable var-name, or the empty string, if not set.
-
(is quoted-literal)
A literal and explicit match against the current topic will be made. If the match fails, returns false.
-
(~ quoted-glob)
A UNIX glob pattern match will be made against the current topic. If the match fails, returns false.
The glob language is as follows:
?
- Matches any single character.\*
- Matches zero or more characters.[abc] - Matches one instance of the characters
a,
b, or
c`.[a-z]
- Matches one instance of the characters that are greater than or equal toa
, and less than or equal toz
(per ASCII / Unicode sets).
Globs are implicitly anchored. To unanchor them, start or end your pattern with a
\*
. -
(re quoted-regex)
A regular expressions match will be made against the current topic. If the match fails, return false.
For the semantics of the language, refer to any reference on Perl-Compatible Regular Expressions.
-
(on day-spec...)
Matches a given subset of the days of the week. You can list one or more days by name,
sunday
,monday
,tuesday
,wednesday
,thursday
,friday
, orsaturday
. You can also useweekdays
andweekends
as shorthand for monday - friday, and saturday + sunday, respectively. -
(from hour am/pm to hour am/pm)
Matches a given time-of-day, using the current timezone (as determined by the value of the
$TZ
environment variable). The hour values must be specified inHHMM
format, i.e.0830
for 8:30. -
(after hour am/pm)
Equivalent to a
(from ...)
expression, if theto
clause were ignored. -
(before hour am/pm)
Equivalent to a
(from ...)
expression, if thefrom
clause were ignored.
Here's a bunch of examples that aim to be both instructive in terms of theory, and usable via copy-and-paste-and-tweak.
The map variable type creates an association map of names to values,
which can be used by a (lookup ...)
form to get a final value.
(set webhooks (map "shield" "https://slack.web.hook/shield..."
"safe" "https://slack.web.hook/safe..."
"default" "https://slack.web.hook/default...")
Later, during a notification handler body, we can refer to the
webhooks
map and lookup values in it:
; yields the SHIELD webhook
(lookup webhooks "shield")
; yields the DEFAULT webhook, since "genesis"
; is not in the map
(lookup webhooks "genesis" "default")
; also yields the SHIELD webhook
(lookup webhooks "shield" "default")
; causes an error
(lookup webhooks "not-defined")
You will normally have more topics than you have different avenues of notification, so the default case of one FOR per topic can become tiresome quickly.
You can use an (or ...)
form to match multiple topics explicitly:
(for (or (is "topic-1")
(is "topic-2"))
(when ...))
You can also use a (not ...)
form to match everything except a
specific topic:
(for (not (is "that-other-topic"))
(when ...))
The (not ...)
form makes more sense when coupled with an (and ...)
operator to handle everything but a subset of topic:
(for (and (not (is "this-topic"))
(not (is "that-other-topic")))
(when ...))
Normal boolean-logic laws apply, so AND-ing a bunch of NOTs together is the same as NOT-ing a bunch of ORs:
(for (not (or (is "this-topic"))
(or (is "that-other-topic")))
(when ...))
The Shout! FOR evaluator support regular expressions via the
(~ ...)
and (re ...)
forms:
(for (~ "*-pipeline")
(when ...))
... or if you prefer Perl-compatible Regular Expression syntax:
(for (re ".*-pipeline$")
(when ...))
These can be combined with (and ...)
, (or ...)
, and (not ...)
:
(for (or (~ "shield*")
(~ "safe*")
(~ "genesis*"))
(when ...))
If most of your notification needs are uniform, with only a few
exceptions, you can use a fallback handler, defined by (for * ...)
:
(for "special-topic"
(when ...))
(for "other-special-topic"
(when ...))
(for *
(when ...))
The (for * ...)
will handle anything that falls through the other
FOR definitions. Shout! stops looking for FOR matches once it finds
one.
Often, you will want to send notifications via one method, during business hours, and a different way the rest of the time. Here's an example that uses slack from 9am - 5pm on weekdays, and otherwise resorts to email.
(for *
(when ((on weekdays)
(from 0900 am to 0500 pm))
(slack ...))
(when *
(email ...)))
Note that the (when * ...)
clause doesn't trigger during business
hours because Shout! stops evaluating WHEN clauses after the first
match.
- Fork this repo
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Added some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request in Github
- Profit!