# Improving responsiveness of hubot-grafana

**Date:** 2023-08-22  
**Author:** Kees C. Bakker  
**Categories:** Chatops  
**Tags:** Hubot  
**Original:** https://keestalkstech.com/improving-responsiveness-of-hubot-grafana/

![Improving responsiveness of hubot-grafana](https://keestalkstech.com/wp-content/uploads/2023/08/julian-hochgesang-qNKfj9mgraI-unsplash.jpg)

---

We 🧡 the combination of Grafana, Hubot & Slack. [We use it all the time](https://keestalkstech.com/tag/hubot/) 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](https://www.npmjs.com/package/hubot-grafana) interaction.

The end result we're going for is:

## UpdatableMessage to the rescue

We've been working with an [UpdatableMessage](https://keestalkstech.com/2018/10/building-an-updatable-slack-message/) 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](https://github.com/wehkamp/bot-zero/blob/master/src/common/UpdatableMessage.ts) and is still used throughout the [bot-zero project](https://github.com/wehkamp/bot-zero).

Now, let's create some *receive* middleware that use the UpdatableMessage to inject a loading message:

```ts
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:

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](https://www.npmjs.com/package/luxon) to process those relative dates and substitute them for the "real" dates:

```ts
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.

```ts
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

- 2024-12-11: Implement the `sendError` for the `WehkampDashboardResponder` so we don't send 2 separate messages when the dashboard is not found ([check GitHub for more info](https://github.com/stephenyeargin/hubot-grafana/issues/193)).
- 2024-04-29: Simplify code for `replaceTimeExpressions`
- 2024-04-29: The Hubot Grafana package now supports custom responders. I've implemented the changes in a `WehkampDashboardResponder` class.
- 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.
- 2023-08-22: Initial article.
