We 🧡 the combination of Grafana, Hubot & Slack. We use it all the time to visualize dashboards in Slack. But the interaction with certain dashboards might not be as fast as one expect from a chat bot, so let's see what we can do to improve the hubot-grafana interaction.
The end result we're going for is:
UpdatableMessage to the rescue
We've been working with an UpdatableMessage for a while now. It makes it easier to post a message on Slack and update it later on. This lets the user know that the request has been received and we're working on it. The class has since 2018 evolved and is still used throughout the bot-zero project.
Now, let's create some receive middleware that uses the UpdatableMessage to send a loading message:
function rewriteGrafanaCommands(robot: Hubot.Robot) {
if (!robot) throw "Argument 'robot' is empty."
robot.receiveMiddleware(async (context, next, done) => {
const text = context.response.message.text
let match = /(?:grafana|graph|graf) (?:dash|dashboard|db) ([^ :]+)/i.exec(text)
if (match) {
let msg = createUpdatableMessage(context.response)
await msg.send({ text: `:loading: Getting dashboard...` })
;(<any>context.response.message).msg = msg
}
next(done)
})
}
module.exports = (robot: Hubot.Robot) => rewriteGrafanaCommands(robot)
Fixing responses
So I have some issues with the hubot-grafana response:
- The dashboard links contain relative times like
now
andnow-8h
. This is fine when the dashboard is clicked now, but if I read an older message, I would like those relative times to be replaced by the historic times. - The
&fullscreen
gives are redirect totlocalhost:3000
. No clue why. - The
panelId
is not enough, my installation needs aviewPanel
to show the right panel.
Fixing relative expressions
Let's use Luxon to process those relative dates and substitute them for the "real" dates:
import { DateTime } from "luxon"
export function replaceTimeExpressions(str: string) {
const timeStringExpression = /^(now)(\+?)(\-?\d+)([smhdMy])$/
function parseTimeExpression(expression: string): string {
if (expression == "now") {
return new Date().getTime().toString()
}
if (!timeStringExpression.test(expression)) {
return expression
}
let match = timeStringExpression.exec(expression)
let amount = parseInt(match[3])
let unit: any = match[4]
switch (unit) {
case "s":
unit = "seconds"
break
case "m":
unit = "minute"
break
case "h":
unit = "hour"
break
case "d":
unit = "days"
break
case "M":
unit = "months"
break
case "y":
unit = "years"
break
default:
return expression
}
let duration = {}
duration[unit] = amount
return DateTime.now().plus(duration).toJSDate().getTime().toString()
}
return str.replace(/now(\-\d+\w)?/g, function (match) {
return parseTimeExpression(match)
})
}
Putting it all together
Now that we can replace those dates, let's also fix the URLs and and use the UpdatableMessage to send the actual message:
robot.responseMiddleware(async (context, next, done) => {
const text = context.response.message.text
let match = /(?:grafana|graph|graf) (?:dash|dashboard|db) ([^ :]+)/i.exec(text)
if (match) {
let payload = context.strings as any
if (payload.length && payload.length > 0 && payload[0].attachments) {
payload = payload[0]
let item = payload.attachments[0]
item.fallback = item.fallback.replace("&fullscreen", "")
item.fallback = replaceTimeExpressions(item.fallback)
item.fallback = item.fallback.replace(/panelId=(\d+)/, "panelId=$1&viewPanel=$1")
item.title_link = item.title_link.replace("&fullscreen", "")
item.title_link = replaceTimeExpressions(item.title_link)
item.title_link = item.title_link.replace(/panelId=(\d+)/, "panelId=$1&viewPanel=$1")
if ((<any>context.response.message).msg) {
;(<any>context.response.message).msg.send(payload)
done()
return
}
}
}
next(done)
})