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 use the UpdatableMessage to inject a loading message:
import { createUpdatableMessage } from "../common/UpdatableMessage"
function rewriteGrafanaCommands(robot: Hubot.Robot) {
if (!robot) throw "Argument 'robot' is empty."
robot.receiveMiddleware(async context => {
const listeners = (<any>robot).listeners
const listener = listeners.find(l => l.regex.toString().includes("(?:dash|dashboard|db)"))
if (!listener) {
return true
}
const text = context.response.message.text
if (listener.regex.test(text)) {
let msg = createUpdatableMessage(context.response)
await msg.send({ text: `:loading: Getting dashboard...` })
// attach for later use
;(<any>context.response.message).msg = msg
}
return true
})
}
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
nowandnow-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
&fullscreengives are redirect totlocalhost:3000. No clue why. - The
panelIdis not enough, my installation needs aviewPanelto 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])$/
const unitMapping = {
s: "seconds",
m: "minutes",
h: "hours",
d: "days",
M: "months",
y: "years",
}
function parseTimeExpression(expression: string): string {
if (expression === "now") {
return new Date().getTime().toString()
}
if (!timeStringExpression.test(expression)) {
return expression
}
const match = timeStringExpression.exec(expression)
const amount = parseInt(match[3])
const unit = unitMapping[match[4]]
if (!unit) {
return expression
}
let duration = {}
duration[unit] = amount
return DateTime.local().plus(duration).toMillis().toString()
}
return str.replace(/now(\-\d+\w)?/g, function (match) {
return parseTimeExpression(match)
})
}Custom Responder to the rescue
In version 6 of the Hubot Grafana package, we've introduced a custom responder object. You can use this feature to override the way the package writes the Slack message. Let's create a custom dashboard responder that works with the UpdatableMessage object that is attached to our context.
import { replaceTimeExpressions } from "../common/grafana/replaceTimeExpressions"
import { setResponder } from "hubot-grafana/src/adapters/Adapter"
import { Responder } from "hubot-grafana/src/adapters/Responder"
class WehkampDashboardResponder extends Responder {
send(res: Hubot.Response, title: string, imageUrl: string, dashboardLink: string) {
imageUrl = this.fixUrl(imageUrl)
dashboardLink = this.fixUrl(dashboardLink)
let msg: any = {
attachments: [
{
fallback: `${title}: ${imageUrl} - ${dashboardLink}`,
title,
title_link: dashboardLink,
image_url: imageUrl,
},
],
unfurl_links: false,
}
this.sendToSlack(res, msg)
}
private fixUrl(url: string) {
url = url.replace("api.grafana.my.domain", "dashboards.my.domain")
url = url.replace("&fullscreen", "")
url = replaceTimeExpressions(url)
url = url.replace(/panelId=(\d+)/, "panelId=$1&viewPanel=$1")
return url
}
sendError(res: Hubot.Response, error: string) {
if (error == "Dashboard not found") {
error = ":warning: Dashboard not found!"
}
let msg = {
blocks: [
{
type: "section",
text: {
type: "plain_text",
text: error,
emoji: true,
},
},
],
text: error,
}
this.sendToSlack(res, msg)
}
private sendToSlack(res: Hubot.Response, msg: any) {
const useThreads = process.env.HUBOT_GRAFANA_USE_THREADS || false
const message = <any>res.message
if (useThreads) {
msg.thread_ts = message.rawMessage.ts
}
if (!message.msg || message.msg.used == 1) {
res.send(msg)
return
}
message.msg.used = 1
message.msg.send(msg)
}
}
module.exports = () => {
const responder = new WehkampDashboardResponder()
setResponder(responder)
}We may only use the UpdatableMessage once, otherwise we'll update the dashboard that was previously send. res.send will suffice for the other dashboards.
Final thoughts
Sending Grafana dashboards is fun using the Grafana Hubot package. Hubot + Grafana = open source for the win! 🥳
Changelog
- Implement the
sendErrorfor theWehkampDashboardResponderso we don't send 2 separate messages when the dashboard is not found (check GitHub for more info). - Simplify code for
replaceTimeExpressions - The Hubot Grafana package now supports custom responders. I've implemented the changes in a
WehkampDashboardResponderclass. - Sending error messages as well.
- Implemented the latest Hubot middleware signatures.
- Fixed a bug where multiple responses were overwriting each other.
- Initial article.