Improving responsiveness of hubot-grafana

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 => {
    const listeners = (<any>robot).listeners
    const listener = listeners.find(x => x.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...` })
      ;(<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:

  1. The dashboard links contain relative times like now and now-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.
  2. The &fullscreen gives are redirect tot localhost:3000. No clue why.
  3. The panelId is not enough, my installation needs a viewPanel 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 => {
  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 payload = context.strings as any

    if (payload.length && payload.length > 0 && payload[0].attachments) {
      payload = payload[0]

      const 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")

      const message = <any>context.response.message

      if (message.msg) {
        const used = message.msg.used

        if (used == 1) {
          let msg = createUpdatableMessage(context.response)
          msg.send(payload)
        } else {
          message.msg.used = 1
          message.msg.send(payload)
        }

        return false
      }
    } else {
      const message = <any>context.response.message
      if (message.msg) {
        message.msg.send(payload.join("\n"))
        return false
      }
    }
  }

  return true
})

Enjoy!

Changelog

  • 2024-01-22: sending error messages as well.
  • 2024-01-18: implemented the latest Hubot middleware signatures.
  • 2024-01-18: fixed a bug where multiple responses were overwriting each other.
expand_less