IRC notifications with logstash

I have spent some time in the last weeks to learn more about logstash and used the kind of bad state of my IRC notifications as the fun side project to get into it. I now have a pretty useful (well for me) setup which I thought I’d share.

The IRC setup

My basic setup revolves around using the ZNC bouncer which keeps me always connected. I still use weechat in a remote tmux session most of the time, but like to have the option to switch clients without losing my connection or backlog. I also use Growl pretty heavily in combination with OSX notification center to alert me of special keywords or all messages in certain channels. Past solutions included running the IRC client locally with a growl plugin or remote tail-ing a notification logfile. Those solutions were close to what I wanted but tied too much to the client, when I really wanted to have notifications directly from my bouncer. And since znc has a module to log all messages to various logfiles, I decided to get my notifications from there.

Enter logstash

I had read about logstash before and decided to give it a try for this. I won’t go into detail about installing and running it here, but check out the getting started for a good introduction.

For the first important step, we need logstash to listen to changes in the bouncer’s logfiles. This is pretty easy and can be accomplished with the following logstash configuration bits:

input {
  file {
    path => "/home/username/.znc/users/zncuser/moddata/log/*"
    type => "znclog"
  }
}

Per default the log module puts all log files under users/youruser/moddata/log/ and creates a logfile per day which is named after the channel name and date. The logstash input just reads all files that are in there and adds a type to the captured logs to be able to better identify them in subsequent filters. The pattern is not really ideal since older logfiles are not interesting for notifications but are also kept open. So at the moment I work around that by moving my logfiles to a backup partition every night, but there might be a better way to do it.

The next step is to remove lines which I’m never interested in for notifications, like my own messages and JOIN/QUIT messages for example. For this the logstash grep filter definitions are very useful:

filter {
  grep {
    type => "znclog"
    match => ["@message", "\[[0-9:]{8}\](.+?)<USERNAME>"]
    negate => true
  }
  grep {
    type => "znclog"
    match => ["@message", "\*\*\* (Quits|Joins|Parts|.+ sets mode: |.+ is now known as)"]
    negate => true
  }
}

The grep filter is also very useful for another criterion on which I want notifications, namely for all of my private messages. Since all channel names per IRC convention have a # in the name, we can just assume that logfiles without that sign are for private messages. It is important to set drop => false here since we don’t want grep to drop the log line (which is default behaviour).

grep {
  type => "znclog"
  match => ["@source", "#"]
  add_tag => ["pmnotification"]
  negate => true
  drop => false
}

This also needs to be added to the filter section and tags all messages coming from logfiles without a # in the name with "pmnotifcation". Now let’s go to the actual parsing of log events. Since there are going to be some repeated patterns and I wanted to have an easy way to add new ones, I have a ‘pattern library file’ which is included in the configuration.

NOTIFYME (pizza|cupcakes|fire)
IRCNOTIFY %{DATA}%{NOTIFYME}%{GREEDYDATA}
IRCTIME [0-9:]{8}
IRCCHANNELS (nunagios|chef|food)

The terms in capital letters can be used as regex placeholders. The interesting ones are NOTIFYME/IRCNOTIFY which are used as a collection of regexes on which I want to show a notification and IRCCHANNELS which are basically the channel names for which I want notifications for all messages. In order to get those notifications I set up a set of grok filters.

grok {
  match => ["@source", "%{IRCCHANNELS}"]
  add_tag => ["channelnotification"]
  exclude_tags => ["pmnotification"]
  patterns_dir => '/home/username/logstash-patterns'
}

This grok ruleset grabs all events from the channels based on the IRCCHANNELS match and tags them with the "channelnotification" tag. PMs are excluded from that match because they have already matched.

grok {
  pattern => "\[%{IRCTIME:irctime}\](.+?)<%{DATA:ircsender}>%{GREEDYDATA:ircmessage}"
  tags => ["channelnotification"]
  patterns_dir => '/home/username/logstash-patterns'
}
grok {
  pattern => "\[%{IRCTIME:irctime}\](.+?)<%{DATA:ircsender}>%{GREEDYDATA:ircmessage}"
  tags => ["pmnotification"]
  patterns_dir => '/home/username/logstash-patterns'
}

These rulesets extract the timestamp, sender and message data for the notifications into separate fields so they are easily accessible later on. I have the same ruleset for channel notifications and private messages, because I didn’t find a way to match any tag (the tags setting requires an event to match all given tags) so I couldn’t combine them into one rule. Though this seems like something that should be fixable.

grok {
  pattern => "\[%{IRCTIME:irctime}\](.+?)<%{DATA:ircsender}>%{IRCNOTIFY:ircmessage}"
  add_tag => ["notification"]
  exclude_tags => ["pmnotification"]
  patterns_dir => '/home/username/logstash-patterns'
}

And finally the last pattern ruleset matches the regexes that are defined for all events and parses them into the fields mentioned before. Notice that all rulesets include a patterns_dir section which points to the folder with the regex defintions file described above.

The last part of the logstash ruleset is defining an output for the notifications. For a while I just appended them to a logfile and tail-ed that from my laptop over ssh. This worked ok, but I had problems with duplicate notifications when restarting the polling script and wasn’t really happy with this solution. And since I already had Redis running on that host, I thought I’d give that a try.

output {
  redis {
    host => 'localhost'
    data_type => 'list'
    key => 'notifications'
    tags => ["pmnotification"]
    password => 'secret'
  }
  redis {
    host => 'localhost'
    data_type => 'list'
    key => 'notifications'
    tags => ["channelnotification"]
    password => 'secret'
  }
  redis {
    host => 'localhost'
    data_type => 'list'
    key => 'notifications'
    tags => ["notification"]
    password => 'secret'
  }
}

The output config basically just says the for every type of notification log event, append it to a Redis list with the name 'notifications' on the instance running on localhost.

The client side

The last part now is actually getting the notifications into growl on the OSX side of things. For this I have Growl setup to forward everything to notification center and run the following script on my Mac:

import sys
import gntp
import json
import redis
import gntp.notifier

r = redis.StrictRedis(host='ircserver',
                      port=6379, db=0,
                      password="secret"
)
app = "irc-growl"

while 1:
    key, logline = r.blpop("notifications")
    try:
        log = json.loads(logline)
    except Exception as e:
        title = "Failure loading logline: " + str(logline)
        message = "error({0})".format(e)
        gntp.notifier.mini(message, applicationName=app, title=title)
        continue

    try:
        channel = "-".join(log["@source"].split("/")[-1].split("_")[1:-1])
    except Exception as e:
        title = "Failure parsing channel name in: " + str(log["@source"])
        message = "error({0})".format(e)
        gntp.notifier.mini(message, applicationName=app, title=title)
        continue

    try:
        title = ("%s in %s" % (log["@fields"]["ircsender"][0],
                  channel.encode("utf-8")))
    except Exception as e:
        title = "Failure parsing ircsender in: " + str(log)
        message = "error({0})".format(e)
        print title
        print message
        gntp.notifier.mini(message, applicationName=app, title=title)
        continue

    message = (log["@fields"]["ircmessage"][0]).encode("utf-8")
    gntp.notifier.mini(message, applicationName=app, title=title)

This uses the python gntp library to talk to Growl and the redis client to talk to Redis. Specifically for the Redis connection I use blpop, which pops an element (in our case a notification) from the list and if there is none waits for the next one to come in. For every notification it parses out the timestamp, channel, sender and message from the fields I set in the logstash grok rules, formats it nicely, sends it to growl and then gets the next one or waits for new notifications to come in.

Verdict

There are still some improvements I want to make. Mostly around moving the old log files or only reading the newest one. And improving the script so it survives network disconnects and possibly run it under launchd. Also if I’m not running the script to pull notifications, they are piling up in Redis at the moment. So next time I connect, I get an abundance of new notifications. Notification center batches them nicely to not litter the whole screen and only the last 20 are in the sidebar. So it’s not really a problem, but I thought about running a cron to prune the list to a maximum of 20 notifications or so.

I now have a setup where I get my notifications directly from the bouncer logs and can display them on any (OSX) host which has the script set up. It should also be fairly simple to adapt this to other notification display systems. The setup is no longer bound to which IRC client I use or whether or not I constantly have it running on a server. Plus the alerting keywords and channels are easily extended because I only have to add patterns to the library file and not touch the config itself.

Getting started with monitoring on the cheap and easy

This post started out as a writeup of tools and services I use to monitor my small (currently 3) set of personal servers. However thinking about it, it made more sense to me to structure it as a small guide on how to get started with monitoring without having to invest too much time, effort and money. Since I don’t use that at the moment, I won’t cover instrumentation and monitoring of application metrics but go more into general service availabilty and machine level metrics. The prices I mention are (to my best knowledge) up to date for the current time, but are of course subject to change.

My setup

I have a small set of servers which I’m using for basic services. These include mail server, IMAP, backup MX, IRC bouncer and general remote shell for running mutt, weechat, newsbeuter and other terminal based applications. I recently got around to more or less properly create cookbooks for this as I am running chef for configuration management. This also prompted me to finally set up monitoring and alerting for the services I care about.

External service monitoring

Servers are not very useful when their services are not accessible from the outside world. So you want to monitor this from an external source which usually tries to establish a connection to specified TCP ports. The general first service to use is pingdom. They provide a great service with great statistics. However since I want to monitor more than the free plan offers (and possibly more than the cheapest paid plan also), I was looking into an alternative. Since I already have an account at zerigo for some DNS services, I decided to give their Watchdog service a try. It’s $15 per 3 months and allows 50 service checks for 10 hosts with checking time down to every 5 minutes. This is more than enough for my needs and comes down to $5 a month. The only drawback is that they only provide email notifications (which can be somewhat mitigated with ifttt or the mail to text gateway of your mobile provider) to one user and a not really great statistics overview. Otherwise it works pretty great.

Process monitoring

The next step is to monitor the processes which are actually providing those services. For this I’m running a Sensu instance on Heroku in the setup I described before. Sensu is an awesome monitoring framework which provides a lot of flexibility, so it’s definitely worth checking out. Since it runs on two small Heroku instances I can host the server and API for free which works pretty well. As basic checks I test for running sendmail, cron and dovecot processes. If the checks fail the given threshold, an alert is pushed to an IRC channel on my grove.io organization. Admittingly this is a little bit overkill since the basic plans for grove.io start at $10, but I like to play and experiment with chat based interfaces to infrastructure automation and monitoring. An alternative would be to use Campfire which is free for a small amount of users. I am also playing with the idea of having a Boxcar handler either for Sensu itself or alerting to Boxcar from IRC. Boxcar is a pretty sweet service which handles push notifications to mobile phones and I’m already using it for notifications from my IRC bouncer and ifttt.com. And since I’m also running an instance of Hubot (also on a free Heroku instance) it should be rather trivial to have the bot listen for patterns and send Boxcar notifications upon match.

Log processing

Since I don’t want to log into several servers to quickly check different logfiles, I’m sending all of my log data to Papertrail. They provide an easy endpoint to send log lines from various systems such as syslog, rsyslog or directly from an application with an rsyslog handler. Their basic free plan allows for 100MB of log data per month with a searchable archive of 1 week. This amount should be enough for a small set of systems with average log data. After that you get 1GB of log lines in the first stage of paid plans for $7, which is still a decent trade. The big advantage is that I can now log into a web interface and see specific log information (for example about chef runs) across all of my servers.

Machine level metrics

Additionally I also gather machine level metrics for all of my servers. These include basic information about CPU and memory usage, disk space and uptime. All of these metrics are gathered by collectd and its various plugins and are sent to Librato Metrics for graphing. This is a lot easier and less hassle than managing your own Graphite instance. And you only pay for the metrics you actually send. The data I currently send there are basic metrics from 2 servers and the number of Sensu check occurrences and it adds up to something around $5 a month.

Verdict

This setup gives me (in my opinion) a pretty good monitoring solution for my personal infrastructure. Since I don’t consume a lot of resources for the services I depend on, I can usually use the free or cheapest plan available. With the cheapest options it’s around $10 a month and even adding grove.io and paid Papretrail into the mix only brings you to a bit more than $25 a month. Of course depending heavily on 3rd party services opens a whole new discussion about availability which you should be aware of.

For configuration examples for the services mentioned above, you can check out my chef cookbooks. They are mostly run on FreeBSD but should be somewhat easy to adapt to a different environment.

Deploying Sensu monitoring on Heroku

Sensu - trying to unsuck monitoring

Some months ago I wanted to set up monitoring for a handful of servers I use for personal stuff. As a first solution Nagios came to mind. However for several reasons I didn’t want to set it up and configure it. And I really didn’t want to dedicate an existing server to do monitoring or get a new one just for that purpose. Around that time I also read about Sensu, a new approach to monitoring, which is a result of Nagios not being a good fit for the monitoring needs at Sonian. Its technology stack is Ruby, Redis and AMQP. I immediately thought it should be possible to put this on the Heroku Cedar stack and run it on an instance there, which would make a nice solution for monitoring a small number of systems. So I hacked away and with a lot of help (and patience) from Sean Porter, the adaptions to make the server and API part of Sensu deployable on Heroku are in the new 0.9.6 release.

Setting up the Sensu repository

In order to get started and configure your Sensu instance, clone the example repository from Github.

    git clone https://github.com/mrtazz/sensu-heroku-example

The example includes a basic folder layout for running a server or API instance on Heroku. All configuration files can be dropped in the config/ folder. They will be picked up by the process when Sensu starts. The example repo also includes a basic handler (bin/showme.rb), which prints event data to STDOUT. There are a lot more handlers in the Sensu community plugins repository on Github. Since handlers are just ruby scripts, you can download the handlers you want and also put it in the bin/ directory. Don’t forget to add the correct configuration file for the handler in the config/ directory also. A great overview how to configure Sensu can be found on Joe Miller’s blog and there is also an official install guide.

Deployment

In order to deploy Sensu to Heroku, you need to create two apps. One will be the Sensu API instance and the other one the Sensu server. It doesn’t really matter, which one you start with. The important thing is, that you only need to add the RabbitMQ and Redis plugins once and can then reuse the settings on the second instance.

So create the first instance on the cedar stack from within the example repo and add the plugins:

    heroku create --stack cedar awesome-sensu-server
    heroku plugins:install redistogo
    heroku plugins:install rabbitmq
    heroku config:add API_PORT=80

You have to add the API_PORT environment variable to the server instance, since otherwise it will assume it’s running the API itself and assign the instance locale port from the PORT environment variable to use as the API port. After that is done, push the code to Heroku and scale up a worker process:

    git push heroku master
    heroku ps:scale app=1

For the API instance create a new branch in the repo or clone the example repo into a new location. Then initialize the API:

    heroku create --stack cedar awesome-sensu-api
    heroku config:add REDISTOGO_URL="value from server instance"
    heroku config:add RABBITMQ_URL="value from server instance"

Now change the Procfile to start up the API instead of the Sensu server like this:

    app: sensu-api -v -c config/config.json -d config/

Commit the changes and push it to the Heroku app:

    git push heroku-api master
    heroku ps:scale app=1

Now all you have to do is set up clients and voila, you have Heroku hosted monitoring. If you’re not yet familiar with setting up clients, I highly recommend Joe Miller’s blog again. He’s a strong contributor to Sensu and has written an abundance of blog posts and tutorials about it. And of course there is also the sensu wiki.

Further improvements

A definite improvement for plugins and handlers would be to be able to also read configuration from environment variables. At the moment the way to go is to add a configuration JSON file in the config folder. This is fine except for the fact that you’d also have API keys commited to the repo.

And obviously more bugs will probably come up, once more people run Sensu on Heroku. I’ve been running a low volume instance for a couple of weeks now and it works pretty great so far.