@@ -8,10 +8,13 @@ import (
88 "errors"
99 "fmt"
1010 "log/slog"
11+ "os"
1112 "os/exec"
13+ "path/filepath"
1214 "strings"
1315 "time"
1416
17+ "github.com/chromedp/cdproto/browser"
1518 "github.com/chromedp/cdproto/emulation"
1619 "github.com/chromedp/cdproto/network"
1720 "github.com/chromedp/cdproto/page"
@@ -208,12 +211,8 @@ type renderingOptions struct {
208211 landscape bool
209212}
210213
211- func (s * BrowserService ) Render (ctx context.Context , url string , optionFuncs ... RenderingOption ) ([]byte , string , error ) {
212- if url == "" {
213- return nil , "text/plain" , fmt .Errorf ("url must not be empty" )
214- }
215-
216- opts := & renderingOptions { // set sensible defaults here; we want all values filled in to show explicit intent
214+ func defaultRenderingOptions () * renderingOptions {
215+ return & renderingOptions { // set sensible defaults here; we want all values filled in to show explicit intent
217216 gpu : false , // assume no GPU: this can be heavy, and if it exists, it likely exists for AI/ML/transcoding/... purposes, not for us
218217 sandbox : false , // FIXME: enable this; <https://github.com/grafana/grafana-operator-experience-squad/issues/1460>
219218 timezone : time .UTC , // UTC ensures consistency when it is not configured but the users' servers are in multiple locations
@@ -227,6 +226,14 @@ func (s *BrowserService) Render(ctx context.Context, url string, optionFuncs ...
227226 printer : defaultPDFPrinter (), // print as PDF if no other format is requested
228227 landscape : true ,
229228 }
229+ }
230+
231+ func (s * BrowserService ) Render (ctx context.Context , url string , optionFuncs ... RenderingOption ) ([]byte , string , error ) {
232+ if url == "" {
233+ return nil , "text/plain" , fmt .Errorf ("url must not be empty" )
234+ }
235+
236+ opts := defaultRenderingOptions ()
230237 for _ , f := range s .defaultRenderingOptions {
231238 if err := f (opts ); err != nil {
232239 return nil , "text/plain" , fmt .Errorf ("failed to apply default rendering option: %w" , err )
@@ -284,6 +291,79 @@ func (s *BrowserService) Render(ctx context.Context, url string, optionFuncs ...
284291 }
285292}
286293
294+ // RenderCSV visits a web page and downloads the CSV inside.
295+ //
296+ // You may be thinking: what the hell are we doing? Why are we using a browser for this?
297+ // The CSV endpoint just returns HTML. The actual query is done by the browser, and then a script _in the webpage_ downloads it as a CSV file.
298+ // This SHOULD be replaced at some point, such that the Grafana server does all the work; this is just not acceptable behaviour...
299+ func (s * BrowserService ) RenderCSV (ctx context.Context , url , renderKey , domain string ) ([]byte , error ) {
300+ if url == "" {
301+ return nil , fmt .Errorf ("url must not be empty" )
302+ }
303+
304+ traceID , err := getTraceID (ctx )
305+ if err != nil {
306+ return nil , fmt .Errorf ("failed to get trace ID: %w" , err )
307+ }
308+ log := s .log .With ("trace_id" , traceID )
309+
310+ allocatorOptions , err := s .createAllocatorOptions (defaultRenderingOptions ())
311+ if err != nil {
312+ return nil , fmt .Errorf ("failed to create allocator options: %w" , err )
313+ }
314+ allocatorCtx , cancelAllocator := chromedp .NewExecAllocator (ctx , allocatorOptions ... )
315+ defer cancelAllocator ()
316+ browserCtx , cancelBrowser := chromedp .NewContext (allocatorCtx , browserLoggers (ctx , log ))
317+ defer cancelBrowser ()
318+
319+ tmpDir , err := os .MkdirTemp ("" , "gir-csv-" + traceID + "-*" )
320+ if err != nil {
321+ return nil , fmt .Errorf ("failed to create temporary directory: %w" , err )
322+ }
323+ defer func () {
324+ if err := os .RemoveAll (tmpDir ); err != nil {
325+ log .WarnContext (ctx , "failed to remove temporary directory" , "path" , tmpDir , "error" , err )
326+ }
327+ }()
328+
329+ actions := []chromedp.Action {
330+ setCookies ([]* network.SetCookieParams {
331+ {
332+ Name : "renderKey" ,
333+ Value : renderKey ,
334+ Domain : domain ,
335+ },
336+ }),
337+ browser .SetDownloadBehavior (browser .SetDownloadBehaviorBehaviorAllow ).WithDownloadPath (tmpDir ),
338+ chromedp .Navigate (url ),
339+ waitForViz (),
340+ waitForDuration (time .Second ),
341+ }
342+ if err := chromedp .Run (browserCtx , actions ... ); err != nil {
343+ return nil , fmt .Errorf ("failed to run browser: %w" , err )
344+ }
345+
346+ // Wait for the file to be downloaded.
347+ var entries []os.DirEntry
348+ for {
349+ if err := ctx .Err (); err != nil {
350+ return nil , err
351+ }
352+
353+ entries , err = os .ReadDir (tmpDir )
354+ if err == nil && len (entries ) > 0 {
355+ break // file exists now
356+ }
357+ }
358+
359+ fileContents , err := os .ReadFile (filepath .Join (tmpDir , entries [0 ].Name ()))
360+ if err != nil {
361+ return nil , fmt .Errorf ("failed to read temporary file: %w" , err )
362+ }
363+
364+ return fileContents , nil
365+ }
366+
287367func getTraceID (context.Context ) (string , error ) {
288368 // TODO: Use OTEL trace ID from context
289369 id , err := uuid .NewRandom ()
0 commit comments