Skip to content

Commit

Permalink
Split to a Grafana Service (#180)
Browse files Browse the repository at this point in the history
* Refactor the creation of the Grafana Client to the Grafan Client Factory.

* Remove only

* Make sure we forbid only on commit and push

* Refactor response to be easier to read. Fail fast.

* Small refactoring to make code more readable.

* refactored pausing / resuming of alerts to the service.

* refactor single alert

* refactor alert querying into the service

* remove send alerts, as it is refactored.

* refactor search out

* refactor get dashboard out

* Version bump & make failing tests execute faster.

* Add prettier support to dev container.

* formatting

* harmonize with the rest of the code

* add missing ';'

* Move object around and refactor parseToGrafanaDashboardRequest and getScreenshotUrls

* Harmonize code

* Fix fetch testing.

* Was missing co-pilot in the dev env.

* Implementation of the bot.

* Refactor types.

* Combine the processing of the string to Grafana dashboards.

* Fix return type.

* Improve documentation with types. Move `nock.cleanAll` to the right `setupNock`.

* Add tests for pausing and unpausing all alerts. Improves code coverage.

* Add tests for Grafana service processing and response handling. Improves code coverage.

* The "should respond with a png graph in the default s3 region" test takes a bit longer at my machine, so add some more timeout to prevent false negatives.

* Fix bug with template values not showing up in the title. Improved test coverage. The test template dashboard now has a parameterized title for the Graph panel (Graph for $server).

* Add getUidBySlug for the Grafana Service.

* Fix typing.

* Rename `DashboardResponse` to `DashboardChart` so it does not look like `GrafanaDashboardResponse.Response`.

* These defaults work better with TypeScript.

* Make it possible to change the output by overriding the system with a custom responder.

* Naming.

* No need for the GrafanaClientFactory now that we have the Bot.

* Rename bot.js to Bot.js

* Restore naming

* Restore strict

* Restore http

* Restore http

* Change formatting

* ship types.d.ts as well
  • Loading branch information
KeesCBakker authored Apr 26, 2024
1 parent ad009c8 commit c932fe4
Show file tree
Hide file tree
Showing 20 changed files with 1,533 additions and 646 deletions.
10 changes: 9 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,13 @@
"mounts": [
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
],
"postCreateCommand": "sudo chown node node_modules"
"postCreateCommand": "sudo chown node node_modules",
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"GitHub.copilot"
]
}
}
}
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm test
npm test -- --forbid-only --forbid-pending
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm test
npm test -- --forbid-only --forbid-pending
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hubot-grafana",
"description": "Query Grafana dashboards",
"version": "5.1.2",
"version": "6.0.0",
"author": "Stephen Yeargin <[email protected]>",
"license": "MIT",
"keywords": [
Expand Down Expand Up @@ -39,7 +39,7 @@
},
"main": "index.js",
"scripts": {
"test": "mocha \"test/**/*.js\" --reporter spec",
"test": "mocha \"test/**/*.js\" --reporter spec --no-experiemental-fetch --timeout 200",
"test-with-coverage": "nyc --reporter=text mocha \"test/**/*.js\" --reporter spec",
"bootstrap": "script/bootstrap",
"prepare": "husky install",
Expand All @@ -49,7 +49,8 @@
"src/**/*.js",
"CONTRIBUTING.md",
"LICENSE",
"index.js"
"index.js",
"types.d.ts"
],
"volta": {
"node": "18.19.0"
Expand Down
101 changes: 101 additions & 0 deletions src/Bot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const { Adapter } = require('./adapters/Adapter');
const { GrafanaService } = require('./service/GrafanaService');
const { GrafanaClient } = require('./grafana-client');

/**
* The bot brings the Adapter and the Grafana Service together.
* It can be used for uploading charts and sending responses out.
*/
class Bot {
/**
* Represents the Bot class.
* @constructor
* @param {Hubot.Robot} robot - The robot instance.
*/
constructor(robot) {
/** @type {Adapter} */
this.adapter = new Adapter(robot);

/** @type {Hubot.Log} */
this.logger = robot.logger;
}

/**
* Creates a new Grafana service based on the provided message.
* @param {Hubot.Response} context - The context object.
* @returns {GrafanaService|null} - The created Grafana service or null if the client is not available.
*/
createService(context) {

const robot = context.robot;
let host = process.env.HUBOT_GRAFANA_HOST;
let apiKey = process.env.HUBOT_GRAFANA_API_KEY;

if (process.env.HUBOT_GRAFANA_PER_ROOM === '1') {
const room = this.getRoom(context);
host = robot.brain.get(`grafana_host_${room}`);
apiKey = robot.brain.get(`grafana_api_key_${room}`);
}

if (host == null) {
this.sendError('No Grafana endpoint configured.', context);
return null;
}

let client = new GrafanaClient(robot.http, robot.logger, host, apiKey);
return new GrafanaService(client);
}

/**
* Sends a dashboard chart.
*
* @param {Hubot.Response} context - The context object.
* @param {DashboardChart} dashboard - The dashboard object.
* @returns {Promise<void>} - A promise that resolves when the chart is sent.
*/
async sendDashboardChart(context, dashboard) {
if (!this.adapter.isUploadSupported()) {
this.adapter.responder.send(context, dashboard.title, dashboard.imageUrl, dashboard.grafanaChartLink);
return;
}

const service = this.createService(context);
if (service == null) return;

/** @type {DownloadedFile|null} */
let file = null;

try {
file = await service.client.download(dashboard.imageUrl);
} catch (err) {
this.sendError(err, context);
return;
}

this.logger.debug(`Uploading file: ${file.body.length} bytes, content-type[${file.contentType}]`);
this.adapter.uploader.upload(context, dashboard.title || 'Image', file, dashboard.grafanaChartLink);
}

/**
* *Sends an error message.
* @param {string} message the error message.
* @param {Hubot.Response} context The context.
*/
sendError(message, context) {
context.robot.logger.error(message);
context.send(message);
}

/**
* Gets the room from the context.
* @param {Hubot.Response} context The context.
* @returns {string}
*/
getRoom(context) {
// placeholder for further adapter support (i.e. MS Teams) as then room also
// contains thread conversation id
return context.envelope.room;
}
}

exports.Bot = Bot;
31 changes: 29 additions & 2 deletions src/adapters/Adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const { RocketChatUploader } = require('./implementations/RocketChatUploader');
const { TelegramUploader } = require('./implementations/TelegramUploader');
const { SlackUploader } = require('./implementations/SlackUploader');

/**
* The override responder is used to override the default responder.
* This can be used to inject a custom responder to influence the message formatting.
* @type {Responder|null}
*/
let overrideResponder = null;

/**
* The Adapter will hide away platform specific details for file upload and
* response messages. When an S3 bucket is configured, it will always take
Expand Down Expand Up @@ -53,6 +60,11 @@ class Adapter {
*/
/** @type {Responder} */
get responder() {

if(overrideResponder){
return overrideResponder;
}

if (/slack/i.test(this.robot.adapterName)) {
return new SlackResponder();
}
Expand All @@ -66,7 +78,7 @@ class Adapter {
return new Responder();
}

/**
/**
* The responder is responsible for doing a (platform specific) upload.
* If an upload is not supported, the method will throw an error.
*/
Expand All @@ -80,7 +92,7 @@ class Adapter {
case 'slack':
return new SlackUploader(this.robot, this.robot.logger);
case 'telegram':
return new TelegramUploader()
return new TelegramUploader();
}

throw new Error(`Upload not supported for '${this.robot.adapterName}'`);
Expand All @@ -95,4 +107,19 @@ class Adapter {
}
}

/**
* Overrides the responder.
* @param {Responder} responder The responder to use.
*/
exports.setResponder = function(responder) {
overrideResponder = responder;
}

/**
* Clears the override responder.
*/
exports.clearResponder = function() {
overrideResponder = null;
}

exports.Adapter = Adapter;
Loading

0 comments on commit c932fe4

Please sign in to comment.