Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
hubot-grafana/src/grafana.coffee
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
331 lines (290 sloc)
12.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Description: | |
# Query Grafana dashboards | |
# | |
# Examples: | |
# - `hubot graf db graphite-carbon-metrics` - Get all panels in the dashboard | |
# - `hubot graf db graphite-carbon-metrics:3` - Get only the third panel of a particular dashboard | |
# - `hubot graf db graphite-carbon-metrics:cpu` - Get only the panels containing "cpu" (case insensitive) in the title | |
# - `hubot graf db graphite-carbon-metrics now-12hr` - Get a dashboard with a window of 12 hours ago to now | |
# - `hubot graf db graphite-carbon-metrics now-24hr now-12hr` - Get a dashboard with a window of 24 hours ago to 12 hours ago | |
# - `hubot graf db graphite-carbon-metrics:3 now-8d now-1d` - Get only the third panel of a particular dashboard with a window of 8 days ago to yesterday | |
# | |
# Configuration: | |
# HUBOT_GRAFANA_HOST - Host for your Grafana 2.0 install, e.g. 'http://play.grafana.org' | |
# HUBOT_GRAFANA_API_KEY - API key for a particular user (leave unset if unauthenticated) | |
# HUBOT_GRAFANA_QUERY_TIME_RANGE - Optional; Default time range for queries (defaults to 6h) | |
# HUBOT_GRAFANA_S3_BUCKET - Optional; Name of the S3 bucket to copy the graph into | |
# HUBOT_GRAFANA_S3_ACCESS_KEY_ID - Optional; Access key ID for S3 | |
# HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY - Optional; Secret access key for S3 | |
# HUBOT_GRAFANA_S3_PREFIX - Optional; Bucket prefix (useful for shared buckets) | |
# HUBOT_GRAFANA_S3_REGION - Optional; Bucket region (defaults to us-standard) | |
# HUBOT_USE_GRAFANA_IMAGES - true|false, whether to use grafana-images | |
# HUBOT_GRAFANA_IMAGES_HOST - The host where grafana images resides, e.g. 'http://grafana.example.com' | |
# | |
# Dependencies: | |
# "knox": "^0.9.2" | |
# "request": "~2" | |
# | |
# Commands: | |
# hubot graf db <dashboard slug>[:<panel id>][ <template variables>][ <from clause>][ <to clause>] - Show grafana dashboard graphs | |
# hubot graf list <tag> - Lists all dashboards available (optional: <tag>) | |
# hubot graf search <keyword> - Search available dashboards by <keyword> | |
# hubot graf help - Display a list of sample commands | |
# | |
crypto = require 'crypto' | |
knox = require 'knox' | |
request = require 'request' | |
module.exports = (robot) -> | |
# Various configuration options stored in environment variables | |
grafana_host = process.env.HUBOT_GRAFANA_HOST | |
grafana_api_key = process.env.HUBOT_GRAFANA_API_KEY | |
grafana_query_time_range = process.env.HUBOT_GRAFANA_QUERY_TIME_RANGE or '6h' | |
s3_bucket = process.env.HUBOT_GRAFANA_S3_BUCKET | |
s3_access_key = process.env.HUBOT_GRAFANA_S3_ACCESS_KEY_ID | |
s3_secret_key = process.env.HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY | |
s3_prefix = process.env.HUBOT_GRAFANA_S3_PREFIX | |
s3_region = 'us-standard' | |
s3_region = process.env.HUBOT_GRAFANA_S3_REGION if process.env.HUBOT_GRAFANA_S3_REGION | |
use_grafana_images = process.env.HUBOT_USE_GRAFANA_IMAGES | |
grafana_images_host = if process.env.HUBOT_GRAFANA_IMAGES_HOST then process.env.HUBOT_GRAFANA_IMAGES_HOST else grafana_host | |
# Get a specific dashboard with options | |
robot.respond /(?:grafana|graph|graf) (?:dash|dashboard|db) ([A-Za-z0-9\-\:_]+)(.*)?/i, (msg) -> | |
slug = msg.match[1].trim() | |
remainder = msg.match[2] | |
timespan = { | |
from: "now-#{grafana_query_time_range}" | |
to: 'now' | |
} | |
variables = '' | |
template_params = [] | |
pid = false | |
pname = false | |
# Parse out a specific panel | |
if /\:/.test slug | |
parts = slug.split(':') | |
slug = parts[0] | |
pid = parseInt parts[1], 10 | |
if isNaN pid | |
pid = false | |
pname = parts[1].toLowerCase() | |
# Check if we have any extra fields | |
if remainder | |
# The order we apply non-variables in | |
timeFields = ['from', 'to'] | |
for part in remainder.trim().split ' ' | |
# Check if it's a variable or part of the timespan | |
if part.indexOf('=') >= 0 | |
variables = "#{variables}&var-#{part}" | |
template_params.push { "name": part.split('=')[0], "value": part.split('=')[1] } | |
# Only add to the timespan if we haven't already filled out from and to | |
else if timeFields.length > 0 | |
timespan[timeFields.shift()] = part.trim() | |
robot.logger.debug msg.match | |
robot.logger.debug slug | |
robot.logger.debug timespan | |
robot.logger.debug variables | |
robot.logger.debug template_params | |
robot.logger.debug pid | |
robot.logger.debug pname | |
# Call the API to get information about this dashboard | |
callGrafana "dashboards/db/#{slug}", (dashboard) -> | |
robot.logger.debug dashboard | |
# Check dashboard information | |
if !dashboard | |
return sendError 'An error ocurred. Check your logs for more details.', msg | |
if dashboard.message | |
return sendError dashboard.message, msg | |
# Handle refactor done for version 2.0.2+ | |
if dashboard.dashboard | |
# 2.0.2+: Changed in https://github.com/grafana/grafana/commit/e5c11691203fe68958e66693e429f6f5a3c77200 | |
data = dashboard.dashboard | |
# The URL was changed in https://github.com/grafana/grafana/commit/35cc0a1cc0bca453ce789056f6fbd2fcb13f74cb | |
apiEndpoint = 'dashboard-solo' | |
else | |
# 2.0.2 and older | |
data = dashboard.model | |
apiEndpoint = 'dashboard/solo' | |
# Support for templated dashboards | |
robot.logger.debug data.templating.list | |
if data.templating.list | |
template_map = [] | |
for template in data.templating.list | |
robot.logger.debug template | |
continue unless template.current | |
for _param in template_params | |
if template.name == _param.name | |
template_map['$' + template.name] = _param.value | |
else | |
template_map['$' + template.name] = template.current.text | |
# Return dashboard rows | |
panelNumber = 0 | |
for row in data.rows | |
for panel in row.panels | |
robot.logger.debug panel | |
panelNumber += 1 | |
# Skip if panel ID was specified and didn't match | |
if pid && pid != panelNumber | |
continue | |
# Skip if panel name was specified any didn't match | |
if pname && panel.title.toLowerCase().indexOf(pname) is -1 | |
continue | |
# Build links for message sending | |
title = formatTitleWithTemplate(panel.title, template_map) | |
imageUrl = "#{grafana_host}/render/#{apiEndpoint}/db/#{slug}/?panelId=#{panel.id}&width=1000&height=500&from=#{timespan.from}&to=#{timespan.to}#{variables}" | |
link = "#{grafana_host}/dashboard/db/#{slug}/?panelId=#{panel.id}&fullscreen&from=#{timespan.from}&to=#{timespan.to}#{variables}" | |
# Fork here for S3-based upload and non-S3 | |
if (s3_bucket && s3_access_key && s3_secret_key) | |
s3FetchAndUpload msg, title, imageUrl, link | |
else if (use_grafana_images == "true") | |
customFetchAndUpload msg, title, imageUrl, link | |
else | |
sendRobotResponse msg, title, imageUrl, link | |
# Get a list of available dashboards | |
robot.respond /(?:grafana|graph|graf) list\s?(.+)?/i, (msg) -> | |
if msg.match[1] | |
tag = msg.match[1].trim() | |
callGrafana "search?tag=#{tag}", (dashboards) -> | |
robot.logger.debug dashboards | |
response = "Dashboards tagged `#{tag}`:\n" | |
sendDashboardList dashboards, response, msg | |
else | |
callGrafana 'search', (dashboards) -> | |
robot.logger.debug dashboards | |
response = "Available dashboards:\n" | |
sendDashboardList dashboards, response, msg | |
# Search dashboards | |
robot.respond /(?:grafana|graph|graf) search (.+)/i, (msg) -> | |
query = msg.match[1].trim() | |
robot.logger.debug query | |
callGrafana "search?query=#{query}", (dashboards) -> | |
robot.logger.debug dashboards | |
response = "Dashboards matching `#{query}`:\n" | |
sendDashboardList dashboards, response, msg | |
# Send Dashboard list | |
sendDashboardList = (dashboards, response, msg) -> | |
# Handle refactor done for version 2.0.2+ | |
if dashboards.dashboards | |
list = dashboards.dashboards | |
else | |
list = dashboards | |
robot.logger.debug list | |
unless list.length > 0 | |
return | |
for dashboard in list | |
# Handle refactor done for version 2.0.2+ | |
if dashboard.uri | |
slug = dashboard.uri.replace /^db\//, '' | |
else | |
slug = dashboard.slug | |
response = response + "- #{slug}: #{dashboard.title}\n" | |
# Remove trailing newline | |
response.trim() | |
msg.send response | |
# Show help text | |
robot.respond /(?:grafana|graph|graf) help$/i, (msg) -> | |
response = "grafana usage:\n" | |
response += " hubot graf list internal\n" | |
response += " hubot graf search mon\n" | |
response += " hubot graf db cm-monitoring-default\n" | |
response += " hubot graf db cm-monitoring-default:8\n" | |
response += " hubot graf db cm-monitoring-default:swap\n" | |
response += " hubot graf db cm-monitoring-default now-12hr\n" | |
response += " hubot graf db cm-monitoring-default now-24hr now-12hr\n" | |
response += " hubot graf db cm-monitoring-default:8 now-8d now-1d\n" | |
response += " hubot graf db cm-monitoring-default:swap server=ncv-dashing now-1w\n" | |
msg.send response | |
# Handle generic errors | |
sendError = (message, msg) -> | |
robot.logger.error message | |
msg.send message | |
# Format the title with template vars | |
formatTitleWithTemplate = (title, template_map) -> | |
title.replace /\$\w+/g, (match) -> | |
if template_map[match] | |
return template_map[match] | |
else | |
return match | |
# Send robot response | |
sendRobotResponse = (msg, title, image, link) -> | |
msg.send "#{title}: #{image} - #{link}" | |
# Call off to Grafana | |
callGrafana = (url, callback) -> | |
if grafana_api_key | |
authHeader = { | |
'Accept': 'application/json', | |
'Authorization': "Bearer #{grafana_api_key}" | |
} | |
else | |
authHeader = { | |
'Accept': 'application/json' | |
} | |
robot.http("#{grafana_host}/api/#{url}").headers(authHeader).get() (err, res, body) -> | |
if (err) | |
robot.logger.error err | |
return callback(false) | |
data = JSON.parse(body) | |
return callback(data) | |
customFetchAndUpload = (msg, title, url, link) -> | |
requestHeaders = { | |
encoding: "utf8", | |
Authorization: "Bearer #{grafana_api_key}", | |
Accept: "application/json" | |
} | |
req_opts = { | |
method: "POST", | |
url: "#{grafana_images_host}/grafana-images", | |
headers: requestHeaders, | |
json: { | |
imageUrl: url | |
} | |
} | |
# post to grafana-images | |
request req_opts, (err, res, json) -> | |
robot.logger.debug "grafana-images POST: #{req_opts.url}, content-type[#{res.headers['content-type']}]" | |
if res.statusCode == 200 | |
sendRobotResponse msg, title, json.pubImg, link | |
else | |
robot.logger.debug res | |
robot.logger.error "Upload Error Code: #{res.statusCode}" | |
msg.send "#{title} - [Access Error] - #{link}" | |
# Pick a random filename | |
s3UploadPath = () -> | |
prefix = s3_prefix || 'grafana' | |
"#{prefix}/#{crypto.randomBytes(20).toString('hex')}.png" | |
# Fetch an image from provided URL, upload it to S3, returning the resulting URL | |
s3FetchAndUpload = (msg, title, url, link) -> | |
if grafana_api_key | |
requestHeaders = | |
encoding: null, | |
auth: | |
bearer: grafana_api_key | |
else | |
requestHeaders = | |
encoding: null | |
request url, requestHeaders, (err, res, body) -> | |
robot.logger.debug "Uploading file: #{body.length} bytes, content-type[#{res.headers['content-type']}]" | |
uploadToS3(msg, title, link, body, body.length, res.headers['content-type']) | |
# Upload image to S3 | |
uploadToS3 = (msg, title, link, content, length, content_type) -> | |
client = knox.createClient { | |
key : s3_access_key | |
secret : s3_secret_key, | |
bucket : s3_bucket, | |
region : s3_region | |
} | |
headers = { | |
'Content-Length' : length, | |
'Content-Type' : content_type, | |
'x-amz-acl' : 'public-read', | |
'encoding' : null | |
} | |
filename = s3UploadPath() | |
req = client.put(filename, headers) | |
req.on 'response', (res) -> | |
if (200 == res.statusCode) | |
sendRobotResponse msg, title, client.https(filename), link | |
else | |
robot.logger.debug res | |
robot.logger.error "Upload Error Code: #{res.statusCode}" | |
msg.send "#{title} - [Upload Error] - #{link}" | |
req.end(content); |