2022-01-10 10:21:35 -06:00
package thumbs
import (
"context"
"encoding/json"
2022-02-18 11:29:18 -06:00
"fmt"
2022-01-10 10:21:35 -06:00
"os"
2022-03-29 07:40:11 -05:00
"sort"
2022-01-10 10:21:35 -06:00
"strings"
"sync"
"time"
2022-04-24 16:55:10 -05:00
"github.com/grafana/grafana/pkg/setting"
2022-02-10 12:45:00 -06:00
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/infra/log"
2022-01-10 10:21:35 -06:00
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/rendering"
)
type simpleCrawler struct {
2022-04-24 16:55:10 -05:00
renderService rendering . Service
threadCount int
concurrentLimit int
renderingTimeout time . Duration
2022-02-09 03:23:32 -06:00
2022-03-29 07:40:11 -05:00
glive * live . GrafanaLive
thumbnailRepo thumbnailRepo
mode CrawlerMode
2022-09-27 07:46:03 -05:00
thumbnailKind ThumbnailKind
2022-04-12 12:34:04 -05:00
auth CrawlerAuth
2022-03-29 07:40:11 -05:00
opts rendering . Opts
status crawlStatus
statusMutex sync . RWMutex
2022-09-27 07:46:03 -05:00
queue [ ] * DashboardWithStaleThumbnail
2022-03-29 07:40:11 -05:00
queueMutex sync . Mutex
log log . Logger
renderingSessionByOrgId map [ int64 ] rendering . Session
2022-07-28 07:40:26 -05:00
dsUidsLookup getDatasourceUidsForDashboard
2022-01-10 10:21:35 -06:00
}
2022-07-28 07:40:26 -05:00
func newSimpleCrawler ( renderService rendering . Service , gl * live . GrafanaLive , repo thumbnailRepo , cfg * setting . Cfg , settings setting . DashboardPreviewsSettings , dsUidsLookup getDatasourceUidsForDashboard ) dashRenderer {
2022-04-24 16:55:10 -05:00
threadCount := int ( settings . CrawlThreadCount )
2022-01-10 10:21:35 -06:00
c := & simpleCrawler {
2022-04-24 16:55:10 -05:00
// temporarily increases the concurrentLimit from the 'cfg.RendererConcurrentRequestLimit' to 'cfg.RendererConcurrentRequestLimit + crawlerThreadCount'
concurrentLimit : cfg . RendererConcurrentRequestLimit + threadCount ,
renderingTimeout : settings . RenderingTimeout ,
renderService : renderService ,
threadCount : threadCount ,
glive : gl ,
2022-07-28 07:40:26 -05:00
dsUidsLookup : dsUidsLookup ,
2022-04-24 16:55:10 -05:00
thumbnailRepo : repo ,
log : log . New ( "thumbnails_crawler" ) ,
2022-01-10 10:21:35 -06:00
status : crawlStatus {
2022-02-09 03:23:32 -06:00
State : initializing ,
2022-01-10 10:21:35 -06:00
Complete : 0 ,
Queue : 0 ,
} ,
2022-03-29 07:40:11 -05:00
renderingSessionByOrgId : make ( map [ int64 ] rendering . Session ) ,
queue : nil ,
2022-01-10 10:21:35 -06:00
}
c . broadcastStatus ( )
return c
}
2022-09-27 07:46:03 -05:00
func ( r * simpleCrawler ) next ( ctx context . Context ) ( * DashboardWithStaleThumbnail , rendering . Session , rendering . AuthOpts , error ) {
2022-02-09 03:23:32 -06:00
r . queueMutex . Lock ( )
defer r . queueMutex . Unlock ( )
if r . queue == nil || len ( r . queue ) < 1 {
2022-03-29 07:40:11 -05:00
return nil , nil , rendering . AuthOpts { } , nil
2022-01-10 10:21:35 -06:00
}
v := r . queue [ 0 ]
r . queue = r . queue [ 1 : ]
2022-03-29 07:40:11 -05:00
authOpts := rendering . AuthOpts {
OrgID : v . OrgId ,
2022-04-12 12:34:04 -05:00
UserID : r . auth . GetUserId ( v . OrgId ) ,
OrgRole : r . auth . GetOrgRole ( ) ,
2022-03-29 07:40:11 -05:00
}
if renderingSession , ok := r . renderingSessionByOrgId [ v . OrgId ] ; ok {
return v , renderingSession , authOpts , nil
}
renderingSession , err := r . renderService . CreateRenderingSession ( ctx , authOpts , rendering . SessionOpts {
Expiry : 5 * time . Minute ,
RefreshExpiryOnEachRequest : true ,
} )
if err != nil {
return nil , nil , authOpts , err
}
r . renderingSessionByOrgId [ v . OrgId ] = renderingSession
return v , renderingSession , authOpts , nil
2022-01-10 10:21:35 -06:00
}
func ( r * simpleCrawler ) broadcastStatus ( ) {
s , err := r . Status ( )
if err != nil {
2022-02-10 12:45:00 -06:00
r . log . Warn ( "Error reading status" , "err" , err )
2022-01-10 10:21:35 -06:00
return
}
msg , err := json . Marshal ( s )
if err != nil {
2022-02-10 12:45:00 -06:00
r . log . Warn ( "Error making message" , "err" , err )
2022-01-10 10:21:35 -06:00
return
}
err = r . glive . Publish ( r . opts . OrgID , "grafana/broadcast/crawler" , msg )
if err != nil {
2022-02-10 12:45:00 -06:00
r . log . Warn ( "Error Publish message" , "err" , err )
2022-01-10 10:21:35 -06:00
return
}
}
2022-09-27 07:46:03 -05:00
type byOrgId [ ] * DashboardWithStaleThumbnail
2022-03-29 07:40:11 -05:00
func ( d byOrgId ) Len ( ) int { return len ( d ) }
func ( d byOrgId ) Less ( i , j int ) bool { return d [ i ] . OrgId > d [ j ] . OrgId }
func ( d byOrgId ) Swap ( i , j int ) { d [ i ] , d [ j ] = d [ j ] , d [ i ] }
2022-09-27 07:46:03 -05:00
func ( r * simpleCrawler ) Run ( ctx context . Context , auth CrawlerAuth , mode CrawlerMode , theme models . Theme , thumbnailKind ThumbnailKind ) error {
2022-09-02 07:20:10 -05:00
res , err := r . renderService . HasCapability ( ctx , rendering . ScalingDownImages )
2022-02-18 11:29:18 -06:00
if err != nil {
return err
}
if ! res . IsSupported {
return fmt . Errorf ( "cant run dashboard crawler - rendering service needs to be updated. " +
"current version: %s, requiredVersion: %s" , r . renderService . Version ( ) , res . SemverConstraint )
}
2022-05-16 20:09:46 -05:00
runStarted := time . Now ( )
2022-02-09 03:23:32 -06:00
r . queueMutex . Lock ( )
2022-02-10 12:45:00 -06:00
if r . IsRunning ( ) {
r . queueMutex . Unlock ( )
r . log . Info ( "Already running" )
return nil
}
2022-01-10 10:21:35 -06:00
2022-02-10 12:45:00 -06:00
items , err := r . thumbnailRepo . findDashboardsWithStaleThumbnails ( ctx , theme , thumbnailKind )
2022-01-10 10:21:35 -06:00
if err != nil {
2022-02-10 12:45:00 -06:00
r . log . Error ( "Error when fetching dashboards with stale thumbnails" , "err" , err . Error ( ) )
r . queueMutex . Unlock ( )
return err
2022-01-10 10:21:35 -06:00
}
2022-02-09 03:23:32 -06:00
if len ( items ) == 0 {
2022-02-10 12:45:00 -06:00
r . queueMutex . Unlock ( )
return nil
2022-01-10 10:21:35 -06:00
}
2022-03-29 07:40:11 -05:00
// sort the items so that we render all items from each org before moving on to the next one
// helps us avoid having to maintain multiple active rendering sessions
sort . Sort ( byOrgId ( items ) )
2022-01-10 10:21:35 -06:00
r . mode = mode
2022-02-09 03:23:32 -06:00
r . thumbnailKind = thumbnailKind
2022-04-12 12:34:04 -05:00
r . auth = auth
2022-01-10 10:21:35 -06:00
r . opts = rendering . Opts {
2022-01-26 16:02:19 -06:00
TimeoutOpts : rendering . TimeoutOpts {
2022-04-24 16:55:10 -05:00
Timeout : r . renderingTimeout ,
2022-01-26 16:02:19 -06:00
RequestTimeoutMultiplier : 3 ,
} ,
2022-01-10 10:21:35 -06:00
Theme : theme ,
2022-04-24 16:55:10 -05:00
ConcurrentLimit : r . concurrentLimit ,
2022-01-10 10:21:35 -06:00
}
2022-02-09 03:23:32 -06:00
2022-03-29 07:40:11 -05:00
r . renderingSessionByOrgId = make ( map [ int64 ] rendering . Session )
2022-02-09 03:23:32 -06:00
r . queue = items
2022-01-10 10:21:35 -06:00
r . status = crawlStatus {
2022-05-16 20:09:46 -05:00
Started : runStarted ,
2022-02-09 03:23:32 -06:00
State : running ,
2022-01-10 10:21:35 -06:00
Complete : 0 ,
}
r . broadcastStatus ( )
2022-02-10 12:45:00 -06:00
r . queueMutex . Unlock ( )
2022-01-10 10:21:35 -06:00
2022-05-16 20:09:46 -05:00
r . log . Info ( "Starting dashboard crawler" , "threadCount" , r . threadCount , "dashboardsToCrawl" , len ( items ) , "mode" , string ( mode ) , "theme" , string ( theme ) , "kind" , string ( thumbnailKind ) , "crawlerSetupTime" , time . Since ( runStarted ) )
2022-02-09 03:23:32 -06:00
2022-02-10 12:45:00 -06:00
group , gCtx := errgroup . WithContext ( ctx )
2022-01-10 10:21:35 -06:00
// create a pool of workers
for i := 0 ; i < r . threadCount ; i ++ {
2022-02-10 12:45:00 -06:00
walkerId := i
group . Go ( func ( ) error {
r . walk ( gCtx , walkerId )
return nil
} )
2022-01-10 10:21:35 -06:00
}
2022-02-10 12:45:00 -06:00
err = group . Wait ( )
2022-05-16 20:09:46 -05:00
status , _ := r . Status ( )
r . log . Info ( "Crawl finished" , "completedCount" , status . Complete , "errorCount" , status . Errors , "threadCount" , r . threadCount , "dashboardsToCrawl" , len ( items ) , "mode" , string ( mode ) , "theme" , string ( theme ) , "kind" , string ( thumbnailKind ) , "crawlerRunTime" , time . Since ( runStarted ) )
2022-02-10 12:45:00 -06:00
if err != nil {
r . log . Error ( "Crawl ended with an error" , "err" , err )
}
r . crawlFinished ( )
r . broadcastStatus ( )
return err
}
func ( r * simpleCrawler ) IsRunning ( ) bool {
r . statusMutex . Lock ( )
defer r . statusMutex . Unlock ( )
return r . status . State == running
2022-01-10 10:21:35 -06:00
}
func ( r * simpleCrawler ) Stop ( ) ( crawlStatus , error ) {
2022-02-09 03:23:32 -06:00
r . statusMutex . Lock ( )
if r . status . State == running {
r . status . State = stopping
2022-01-10 10:21:35 -06:00
}
2022-02-09 03:23:32 -06:00
r . statusMutex . Unlock ( )
2022-01-10 10:21:35 -06:00
return r . Status ( )
}
func ( r * simpleCrawler ) Status ( ) ( crawlStatus , error ) {
2022-02-09 03:23:32 -06:00
r . statusMutex . RLock ( )
defer r . statusMutex . RUnlock ( )
2022-01-10 10:21:35 -06:00
status := crawlStatus {
State : r . status . State ,
Started : r . status . Started ,
Complete : r . status . Complete ,
Errors : r . status . Errors ,
Queue : len ( r . queue ) ,
Last : r . status . Last ,
}
return status , nil
}
2022-02-09 03:23:32 -06:00
func ( r * simpleCrawler ) newErrorResult ( ) {
r . statusMutex . Lock ( )
defer r . statusMutex . Unlock ( )
r . status . Errors ++
r . status . Last = time . Now ( )
}
func ( r * simpleCrawler ) newSuccessResult ( ) {
r . statusMutex . Lock ( )
defer r . statusMutex . Unlock ( )
r . status . Complete ++
r . status . Last = time . Now ( )
}
2022-02-10 12:45:00 -06:00
func ( r * simpleCrawler ) crawlFinished ( ) {
2022-02-09 03:23:32 -06:00
r . statusMutex . Lock ( )
defer r . statusMutex . Unlock ( )
r . status . State = stopped
r . status . Finished = time . Now ( )
}
func ( r * simpleCrawler ) shouldWalk ( ) bool {
r . statusMutex . RLock ( )
defer r . statusMutex . RUnlock ( )
return r . status . State == running
}
2022-02-10 12:45:00 -06:00
func ( r * simpleCrawler ) walk ( ctx context . Context , id int ) {
2022-05-16 20:09:46 -05:00
walkerStarted := time . Now ( )
2022-01-10 10:21:35 -06:00
for {
2022-02-09 03:23:32 -06:00
if ! r . shouldWalk ( ) {
2022-01-10 10:21:35 -06:00
break
}
2022-05-16 20:09:46 -05:00
itemStarted := time . Now ( )
2022-03-29 07:40:11 -05:00
item , renderingSession , authOpts , err := r . next ( ctx )
if err != nil {
r . log . Error ( "Render item retrieval error" , "walkerId" , id , "error" , err )
break
}
if item == nil || renderingSession == nil {
2022-01-10 10:21:35 -06:00
break
}
2022-02-10 12:45:00 -06:00
url := models . GetKioskModeDashboardUrl ( item . Uid , item . Slug , r . opts . Theme )
r . log . Info ( "Getting dashboard thumbnail" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url )
2022-01-10 10:21:35 -06:00
2022-07-28 07:40:26 -05:00
dsUids , err := r . dsUidsLookup ( ctx , item . Uid , item . OrgId )
if err != nil {
r . log . Warn ( "Error getting datasource uids" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url , "err" , err )
r . newErrorResult ( )
continue
}
2022-02-10 12:45:00 -06:00
res , err := r . renderService . Render ( ctx , rendering . Opts {
2022-01-10 10:21:35 -06:00
Width : 320 ,
Height : 240 ,
2022-02-09 03:23:32 -06:00
Path : strings . TrimPrefix ( url , "/" ) ,
2022-03-29 07:40:11 -05:00
AuthOpts : authOpts ,
2022-01-26 16:02:19 -06:00
TimeoutOpts : r . opts . TimeoutOpts ,
2022-02-09 03:23:32 -06:00
ConcurrentLimit : r . opts . ConcurrentLimit ,
2022-01-10 10:21:35 -06:00
Theme : r . opts . Theme ,
2022-02-09 03:23:32 -06:00
DeviceScaleFactor : - 5 , // negative numbers will render larger and then scale down.
2022-03-29 07:40:11 -05:00
} , renderingSession )
2022-01-10 10:21:35 -06:00
if err != nil {
2022-02-10 12:45:00 -06:00
r . log . Warn ( "Error getting image" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url , "err" , err )
2022-02-09 03:23:32 -06:00
r . newErrorResult ( )
2022-01-10 10:21:35 -06:00
} else if res . FilePath == "" {
2022-02-10 12:45:00 -06:00
r . log . Warn ( "Error getting image... no response" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url )
2022-02-09 03:23:32 -06:00
r . newErrorResult ( )
2022-01-10 10:21:35 -06:00
} else if strings . Contains ( res . FilePath , "public/img" ) {
2022-02-10 12:45:00 -06:00
r . log . Warn ( "Error getting image... internal result" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url , "img" , res . FilePath )
2022-02-09 03:23:32 -06:00
// rendering service returned a static error image - we should not remove that file
r . newErrorResult ( )
2022-01-10 10:21:35 -06:00
} else {
2022-02-09 03:23:32 -06:00
func ( ) {
defer func ( ) {
err := os . Remove ( res . FilePath )
if err != nil {
2022-02-10 12:45:00 -06:00
r . log . Error ( "Failed to remove thumbnail temp file" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url , "err" , err )
2022-02-09 03:23:32 -06:00
}
} ( )
2022-09-27 07:46:03 -05:00
thumbnailId , err := r . thumbnailRepo . saveFromFile ( ctx , res . FilePath , DashboardThumbnailMeta {
2022-02-09 03:23:32 -06:00
DashboardUID : item . Uid ,
OrgId : item . OrgId ,
Theme : r . opts . Theme ,
Kind : r . thumbnailKind ,
2022-07-28 07:40:26 -05:00
} , item . Version , dsUids )
2022-01-10 10:21:35 -06:00
2022-02-09 03:23:32 -06:00
if err != nil {
2022-05-16 20:09:46 -05:00
r . log . Warn ( "Error saving image image" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url , "err" , err , "itemTime" , time . Since ( itemStarted ) )
2022-02-09 03:23:32 -06:00
r . newErrorResult ( )
} else {
2022-05-16 20:09:46 -05:00
r . log . Info ( "Saved thumbnail" , "walkerId" , id , "dashboardUID" , item . Uid , "url" , url , "thumbnailId" , thumbnailId , "itemTime" , time . Since ( itemStarted ) )
2022-02-09 03:23:32 -06:00
r . newSuccessResult ( )
}
} ( )
}
2022-01-10 10:21:35 -06:00
r . broadcastStatus ( )
}
2022-05-16 20:09:46 -05:00
r . log . Info ( "Walker finished" , "walkerId" , id , "walkerTime" , time . Since ( walkerStarted ) )
2022-01-10 10:21:35 -06:00
}