Skip to content

Instantly share code, notes, and snippets.

@svet-b
Last active May 17, 2024 21:01
Show Gist options
  • Save svet-b/1ad0656cd3ce0e1a633e16eb20f66425 to your computer and use it in GitHub Desktop.
Save svet-b/1ad0656cd3ce0e1a633e16eb20f66425 to your computer and use it in GitHub Desktop.
PDF export of Grafana dashboard using puppeteer
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Automated PDF export of Grafana dashboard using Puppeteer

Prerequisites

General:

  • Grafana server with dashboards that are to be exported, and datasources in "Server" (proxy) mode.
  • User account on Grafana server that has Viewer access to the required dashboards
  • This has been tested on Ubuntu 16.04 and a Mac

Packages:

  • NodeJS, and the puppeteer package (npm install puppeteer), which is used to run headless Chrome
  • In Linux, Puppeteer has the following library/tool dependencies (primarily related to libx11 - see this post). I found that I didn't need extra packages on a Mac.
sudo apt-get install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

Scripts:

  • The grafana_pdf.js file attached here, which carries out the PDF conversion using Puppeteer

Process

Environment: Set the Grafana server URL, username, and password, and the output filename as environment variables.

export GF_DASH_URL="http://localhost:3000/d/x3g4Wx5ik/new-dashboard?kiosk"
export GF_USER=pdf_export
export GF_PASSWORD=StrongPassword1
export OUTPUT_PDF=output.pdf

Now export to PDF by calling the NodeJS script with the corresponding arguments:

node grafana_pdf.js $GF_DASH_URL $GF_USER:$GF_PASSWORD $OUTPUT_PDF

Notes and caveats

  • The focus here is on single-page output. Getting "tall" dashboards to paginate nicely is an altogether separate endeavor.
  • In its present form, the script adjusts the PDF and aspect ratio to fit the dashboard, with no regard for fitting on an actual page. It's also possible to get the output to be Letter or A4 sized - see comments in the code on how to achieve that; if the intent is to print, you'll probably also want to add a margin (TODO: Add a switch in the code)
  • When you have a single pdf_export user that is a member of multiple organizations, if you try to exporting dashboards belonging to different organizations one after the other, you will occasionally get a "login" screen instead of a dashboard during the org switch. When that happens, I found that simply retrying does the trick.

Example output

Attached below are two example output PDFs (bigdashboard_output.pdf and output_energy.pdf). The former is based on https://play.grafana.org/d/000000003/big-dashboard, and the latter is from our own energy monitoring project (www.ammp.io).

'use strict';
const puppeteer = require('puppeteer');
// URL to load should be passed as first parameter
const url = process.argv[2];
// Username and password (with colon separator) should be second parameter
const auth_string = process.argv[3];
// Output file name should be third parameter
const outfile = process.argv[4];
// TODO: Output an error message if number of arguments is not right or arguments are invalid
// Set the browser width in pixels. The paper size will be calculated on the basus of 96dpi,
// so 1200 corresponds to 12.5".
const width_px = 1200;
// Note that to get an actual paper size, e.g. Letter, you will want to *not* simply set the pixel
// size here, since that would lead to a "mobile-sized" screen (816px), and mess up the rendering.
// Instead, set e.g. double the size here (1632px), and call page.pdf() with format: 'Letter' and
// scale = 0.5.
// Generate authorization header for basic auth
const auth_header = 'Basic ' + new Buffer.from(auth_string).toString('base64');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Set basic auth headers
await page.setExtraHTTPHeaders({'Authorization': auth_header});
// Increase timeout from the default of 30 seconds to 120 seconds, to allow for slow-loading panels
await page.setDefaultNavigationTimeout(120000);
// Increasing the deviceScaleFactor gets a higher-resolution image. The width should be set to
// the same value as in page.pdf() below. The height is not important
await page.setViewport({
width: width_px,
height: 800,
deviceScaleFactor: 2,
isMobile: false
})
// Wait until all network connections are closed (and none are opened withing 0.5s).
// In some cases it may be appropriate to change this to {waitUntil: 'networkidle2'},
// which stops when there are only 2 or fewer connections remaining.
await page.goto(url, {waitUntil: 'networkidle0'});
// Hide all panel description (top-left "i") pop-up handles and, all panel resize handles
// Annoyingly, it seems you can't concatenate the two object collections into one
await page.evaluate(() => {
let infoCorners = document.getElementsByClassName('panel-info-corner');
for (el of infoCorners) { el.hidden = true; };
let resizeHandles = document.getElementsByClassName('react-resizable-handle');
for (el of resizeHandles) { el.hidden = true; };
});
// Get the height of the main canvas, and add a margin
var height_px = await page.evaluate(() => {
return document.getElementsByClassName('react-grid-layout')[0].getBoundingClientRect().bottom;
}) + 20;
await page.pdf({
path: outfile,
width: width_px + 'px',
height: height_px + 'px',
// format: 'Letter', <-- see note above for generating "paper-sized" outputs
scale: 1,
displayHeaderFooter: false,
margin: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},
});
await browser.close();
})();
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@rafalkrupinski
Copy link

Ah, it's the kiosk mode disabling scrolling...

@zach-betz-hln
Copy link

Thank you for this example @svet-b.
It allowed us to create a somewhat printer friendly single page pdf.
For future readers, here's a Playwright snippet that worked for us, see code comments.

// Setup...

// Our tallest known dashboard is 3010px
// So set an arbitrarily high height so that all panels are loaded, which will be lowered later to fit the content
await page.setViewportSize({ width: 1920, height: 6000 });

// Enable kiosk mode in case the user is an admin
dashboardUrl.searchParams.set('kiosk', '1');
await page.goto(dashboardUrl.href);
await page.waitForLoadState('networkidle');

const dashboardHeight = await page.evaluate(() => {
  return document.querySelector('.react-grid-layout')?.getBoundingClientRect().bottom ?? 0;
});
// Fit the page to the content
await page.setViewportSize({ width: 1920, height: dashboardHeight });
await page.waitForLoadState('networkidle');

const margin = '10px';
const pdf = await page.pdf({
  scale: 0.4, // Tweak this to your needs
  format: 'Letter',
  printBackground: true,
  margin: {
    top: margin,
    bottom: margin,
    left: margin,
    right: margin
  }
});

// Do stuff with the pdf...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment