Rendering: add CSV support (#33729)

* Rendering: add CSV rendering support

* Rendering: save csv files into a separate folder

* add missing field

* Renderer: get filename from renderer plugin

* apply PR suggestions

* Rendering: remove old PhantomJS error

* Rendering: separate RenderCSV and Render functions

* fix alerting test

* Rendering: fix handling error in HTTP mode

* apply PR feedback

* Update pkg/services/rendering/http_mode.go

Co-authored-by: Joan López de la Franca Beltran <joanjan14@gmail.com>

* apply PR feedback

* Update rendering metrics with type label

* Rendering: return error if not able to parse header

* Rendering: update grpc generated file

* Rendering: use context.WithTimeout to render CSV too

Co-authored-by: Joan López de la Franca Beltran <joanjan14@gmail.com>
pull/34011/head^2
Agnès Toulet 5 years ago committed by GitHub
parent 81ad9769fa
commit ec71919e7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      pkg/api/render.go
  2. 10
      pkg/infra/metrics/metrics.go
  3. 290
      pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go
  4. 16
      pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto
  5. 2
      pkg/services/alerting/notifier.go
  6. 4
      pkg/services/alerting/notifier_test.go
  7. 21
      pkg/services/cleanup/cleanup.go
  8. 128
      pkg/services/rendering/http_mode.go
  9. 33
      pkg/services/rendering/interface.go
  10. 57
      pkg/services/rendering/plugin_mode.go
  11. 96
      pkg/services/rendering/rendering.go
  12. 2
      pkg/setting/setting.go

@ -4,9 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"runtime"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
@ -57,8 +55,8 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) {
Width: width, Width: width,
Height: height, Height: height,
Timeout: time.Duration(timeout) * time.Second, Timeout: time.Duration(timeout) * time.Second,
OrgId: c.OrgId, OrgID: c.OrgId,
UserId: c.UserId, UserID: c.UserId,
OrgRole: c.OrgRole, OrgRole: c.OrgRole,
Path: c.Params("*") + queryParams, Path: c.Params("*") + queryParams,
Timezone: queryReader.Get("tz", ""), Timezone: queryReader.Get("tz", ""),
@ -72,14 +70,6 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) {
c.Handle(hs.Cfg, 500, err.Error(), err) c.Handle(hs.Cfg, 500, err.Error(), err)
return return
} }
if errors.Is(err, rendering.ErrPhantomJSNotInstalled) {
if strings.HasPrefix(runtime.GOARCH, "arm") {
c.Handle(hs.Cfg, 500, "Rendering failed - PhantomJS isn't included in arm build per default", err)
} else {
c.Handle(hs.Cfg, 500, "Rendering failed - PhantomJS isn't installed correctly", err)
}
return
}
c.Handle(hs.Cfg, 500, "Rendering failed.", err) c.Handle(hs.Cfg, 500, "Rendering failed.", err)
return return

@ -367,25 +367,25 @@ func init() {
MRenderingRequestTotal = prometheus.NewCounterVec( MRenderingRequestTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "rendering_request_total", Name: "rendering_request_total",
Help: "counter for image rendering requests", Help: "counter for rendering requests",
Namespace: ExporterName, Namespace: ExporterName,
}, },
[]string{"status"}, []string{"status", "type"},
) )
MRenderingSummary = prometheus.NewSummaryVec( MRenderingSummary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{ prometheus.SummaryOpts{
Name: "rendering_request_duration_milliseconds", Name: "rendering_request_duration_milliseconds",
Help: "summary of image rendering request duration", Help: "summary of rendering request duration",
Objectives: objectiveMap, Objectives: objectiveMap,
Namespace: ExporterName, Namespace: ExporterName,
}, },
[]string{"status"}, []string{"status", "type"},
) )
MRenderingQueue = prometheus.NewGauge(prometheus.GaugeOpts{ MRenderingQueue = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "rendering_queue_size", Name: "rendering_queue_size",
Help: "size of image rendering queue", Help: "size of rendering queue",
Namespace: ExporterName, Namespace: ExporterName,
}) })

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.26.0 // protoc-gen-go v1.26.0
// protoc v3.15.6 // protoc v3.15.8
// source: rendererv2.proto // source: rendererv2.proto
package pluginextensionv2 package pluginextensionv2
@ -237,6 +237,156 @@ func (x *RenderResponse) GetError() string {
return "" return ""
} }
type RenderCSVRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
FilePath string `protobuf:"bytes,2,opt,name=filePath,proto3" json:"filePath,omitempty"`
RenderKey string `protobuf:"bytes,3,opt,name=renderKey,proto3" json:"renderKey,omitempty"`
Domain string `protobuf:"bytes,4,opt,name=domain,proto3" json:"domain,omitempty"`
Timeout int32 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
Timezone string `protobuf:"bytes,6,opt,name=timezone,proto3" json:"timezone,omitempty"`
Headers map[string]*StringList `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *RenderCSVRequest) Reset() {
*x = RenderCSVRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_rendererv2_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RenderCSVRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RenderCSVRequest) ProtoMessage() {}
func (x *RenderCSVRequest) ProtoReflect() protoreflect.Message {
mi := &file_rendererv2_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RenderCSVRequest.ProtoReflect.Descriptor instead.
func (*RenderCSVRequest) Descriptor() ([]byte, []int) {
return file_rendererv2_proto_rawDescGZIP(), []int{3}
}
func (x *RenderCSVRequest) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *RenderCSVRequest) GetFilePath() string {
if x != nil {
return x.FilePath
}
return ""
}
func (x *RenderCSVRequest) GetRenderKey() string {
if x != nil {
return x.RenderKey
}
return ""
}
func (x *RenderCSVRequest) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
func (x *RenderCSVRequest) GetTimeout() int32 {
if x != nil {
return x.Timeout
}
return 0
}
func (x *RenderCSVRequest) GetTimezone() string {
if x != nil {
return x.Timezone
}
return ""
}
func (x *RenderCSVRequest) GetHeaders() map[string]*StringList {
if x != nil {
return x.Headers
}
return nil
}
type RenderCSVResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
FileName string `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"`
}
func (x *RenderCSVResponse) Reset() {
*x = RenderCSVResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_rendererv2_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RenderCSVResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RenderCSVResponse) ProtoMessage() {}
func (x *RenderCSVResponse) ProtoReflect() protoreflect.Message {
mi := &file_rendererv2_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RenderCSVResponse.ProtoReflect.Descriptor instead.
func (*RenderCSVResponse) Descriptor() ([]byte, []int) {
return file_rendererv2_proto_rawDescGZIP(), []int{4}
}
func (x *RenderCSVResponse) GetError() string {
if x != nil {
return x.Error
}
return ""
}
func (x *RenderCSVResponse) GetFileName() string {
if x != nil {
return x.FileName
}
return ""
}
var File_rendererv2_proto protoreflect.FileDescriptor var File_rendererv2_proto protoreflect.FileDescriptor
var file_rendererv2_proto_rawDesc = []byte{ var file_rendererv2_proto_rawDesc = []byte{
@ -273,15 +423,46 @@ var file_rendererv2_proto_rawDesc = []byte{
0x32, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x26, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x26, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x64, 0x65,
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72,
0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22,
0x59, 0x0a, 0x08, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x65, 0x72, 0x12, 0x4d, 0x0a, 0x06, 0x52, 0xd3, 0x02, 0x0a, 0x10, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71,
0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61,
0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61,
0x74, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4b, 0x65, 0x79,
0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65,
0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f,
0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x4a,
0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x30, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f,
0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x59, 0x0a, 0x0c, 0x48, 0x65,
0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6c,
0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e,
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43,
0x53, 0x56, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x32, 0xb1, 0x01, 0x0a,
0x08, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x65, 0x72, 0x12, 0x4d, 0x0a, 0x06, 0x52, 0x65, 0x6e,
0x64, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65,
0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78,
0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x09, 0x52, 0x65, 0x6e, 0x64,
0x65, 0x72, 0x43, 0x53, 0x56, 0x12, 0x23, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78,
0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6c, 0x75,
0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52,
0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x16, 0x5a, 0x14, 0x2e, 0x2f, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x3b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x16, 0x5a, 0x14, 0x2e, 0x2f, 0x3b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74,
0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@ -296,23 +477,30 @@ func file_rendererv2_proto_rawDescGZIP() []byte {
return file_rendererv2_proto_rawDescData return file_rendererv2_proto_rawDescData
} }
var file_rendererv2_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_rendererv2_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_rendererv2_proto_goTypes = []interface{}{ var file_rendererv2_proto_goTypes = []interface{}{
(*StringList)(nil), // 0: pluginextensionv2.StringList (*StringList)(nil), // 0: pluginextensionv2.StringList
(*RenderRequest)(nil), // 1: pluginextensionv2.RenderRequest (*RenderRequest)(nil), // 1: pluginextensionv2.RenderRequest
(*RenderResponse)(nil), // 2: pluginextensionv2.RenderResponse (*RenderResponse)(nil), // 2: pluginextensionv2.RenderResponse
nil, // 3: pluginextensionv2.RenderRequest.HeadersEntry (*RenderCSVRequest)(nil), // 3: pluginextensionv2.RenderCSVRequest
(*RenderCSVResponse)(nil), // 4: pluginextensionv2.RenderCSVResponse
nil, // 5: pluginextensionv2.RenderRequest.HeadersEntry
nil, // 6: pluginextensionv2.RenderCSVRequest.HeadersEntry
} }
var file_rendererv2_proto_depIdxs = []int32{ var file_rendererv2_proto_depIdxs = []int32{
3, // 0: pluginextensionv2.RenderRequest.headers:type_name -> pluginextensionv2.RenderRequest.HeadersEntry 5, // 0: pluginextensionv2.RenderRequest.headers:type_name -> pluginextensionv2.RenderRequest.HeadersEntry
0, // 1: pluginextensionv2.RenderRequest.HeadersEntry.value:type_name -> pluginextensionv2.StringList 6, // 1: pluginextensionv2.RenderCSVRequest.headers:type_name -> pluginextensionv2.RenderCSVRequest.HeadersEntry
1, // 2: pluginextensionv2.Renderer.Render:input_type -> pluginextensionv2.RenderRequest 0, // 2: pluginextensionv2.RenderRequest.HeadersEntry.value:type_name -> pluginextensionv2.StringList
2, // 3: pluginextensionv2.Renderer.Render:output_type -> pluginextensionv2.RenderResponse 0, // 3: pluginextensionv2.RenderCSVRequest.HeadersEntry.value:type_name -> pluginextensionv2.StringList
3, // [3:4] is the sub-list for method output_type 1, // 4: pluginextensionv2.Renderer.Render:input_type -> pluginextensionv2.RenderRequest
2, // [2:3] is the sub-list for method input_type 3, // 5: pluginextensionv2.Renderer.RenderCSV:input_type -> pluginextensionv2.RenderCSVRequest
2, // [2:2] is the sub-list for extension type_name 2, // 6: pluginextensionv2.Renderer.Render:output_type -> pluginextensionv2.RenderResponse
2, // [2:2] is the sub-list for extension extendee 4, // 7: pluginextensionv2.Renderer.RenderCSV:output_type -> pluginextensionv2.RenderCSVResponse
0, // [0:2] is the sub-list for field type_name 6, // [6:8] is the sub-list for method output_type
4, // [4:6] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
} }
func init() { file_rendererv2_proto_init() } func init() { file_rendererv2_proto_init() }
@ -357,6 +545,30 @@ func file_rendererv2_proto_init() {
return nil return nil
} }
} }
file_rendererv2_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RenderCSVRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_rendererv2_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RenderCSVResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@ -364,7 +576,7 @@ func file_rendererv2_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_rendererv2_proto_rawDesc, RawDescriptor: file_rendererv2_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 4, NumMessages: 7,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
@ -391,6 +603,7 @@ const _ = grpc.SupportPackageIsVersion6
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type RendererClient interface { type RendererClient interface {
Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error)
RenderCSV(ctx context.Context, in *RenderCSVRequest, opts ...grpc.CallOption) (*RenderCSVResponse, error)
} }
type rendererClient struct { type rendererClient struct {
@ -410,9 +623,19 @@ func (c *rendererClient) Render(ctx context.Context, in *RenderRequest, opts ...
return out, nil return out, nil
} }
func (c *rendererClient) RenderCSV(ctx context.Context, in *RenderCSVRequest, opts ...grpc.CallOption) (*RenderCSVResponse, error) {
out := new(RenderCSVResponse)
err := c.cc.Invoke(ctx, "/pluginextensionv2.Renderer/RenderCSV", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// RendererServer is the server API for Renderer service. // RendererServer is the server API for Renderer service.
type RendererServer interface { type RendererServer interface {
Render(context.Context, *RenderRequest) (*RenderResponse, error) Render(context.Context, *RenderRequest) (*RenderResponse, error)
RenderCSV(context.Context, *RenderCSVRequest) (*RenderCSVResponse, error)
} }
// UnimplementedRendererServer can be embedded to have forward compatible implementations. // UnimplementedRendererServer can be embedded to have forward compatible implementations.
@ -422,6 +645,9 @@ type UnimplementedRendererServer struct {
func (*UnimplementedRendererServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { func (*UnimplementedRendererServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Render not implemented") return nil, status.Errorf(codes.Unimplemented, "method Render not implemented")
} }
func (*UnimplementedRendererServer) RenderCSV(context.Context, *RenderCSVRequest) (*RenderCSVResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RenderCSV not implemented")
}
func RegisterRendererServer(s *grpc.Server, srv RendererServer) { func RegisterRendererServer(s *grpc.Server, srv RendererServer) {
s.RegisterService(&_Renderer_serviceDesc, srv) s.RegisterService(&_Renderer_serviceDesc, srv)
@ -445,6 +671,24 @@ func _Renderer_Render_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Renderer_RenderCSV_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RenderCSVRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RendererServer).RenderCSV(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pluginextensionv2.Renderer/RenderCSV",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RendererServer).RenderCSV(ctx, req.(*RenderCSVRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Renderer_serviceDesc = grpc.ServiceDesc{ var _Renderer_serviceDesc = grpc.ServiceDesc{
ServiceName: "pluginextensionv2.Renderer", ServiceName: "pluginextensionv2.Renderer",
HandlerType: (*RendererServer)(nil), HandlerType: (*RendererServer)(nil),
@ -453,6 +697,10 @@ var _Renderer_serviceDesc = grpc.ServiceDesc{
MethodName: "Render", MethodName: "Render",
Handler: _Renderer_Render_Handler, Handler: _Renderer_Render_Handler,
}, },
{
MethodName: "RenderCSV",
Handler: _Renderer_RenderCSV_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "rendererv2.proto", Metadata: "rendererv2.proto",

@ -24,6 +24,22 @@ message RenderResponse {
string error = 1; string error = 1;
} }
message RenderCSVRequest {
string url = 1;
string filePath = 2;
string renderKey = 3;
string domain = 4;
int32 timeout = 5;
string timezone = 6;
map<string, StringList> headers = 7;
}
message RenderCSVResponse {
string error = 1;
string fileName = 2;
}
service Renderer { service Renderer {
rpc Render(RenderRequest) returns (RenderResponse); rpc Render(RenderRequest) returns (RenderResponse);
rpc RenderCSV(RenderCSVRequest) returns (RenderCSVResponse);
} }

@ -201,7 +201,7 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout
Width: 1000, Width: 1000,
Height: 500, Height: 500,
Timeout: timeout, Timeout: timeout,
OrgId: evalCtx.Rule.OrgID, OrgID: evalCtx.Rule.OrgID,
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
ConcurrentLimit: setting.AlertingRenderLimit, ConcurrentLimit: setting.AlertingRenderLimit,
} }

@ -361,6 +361,10 @@ func (s *testRenderService) Render(ctx context.Context, opts rendering.Opts) (*r
return &rendering.RenderResult{FilePath: "image.png"}, nil return &rendering.RenderResult{FilePath: "image.png"}, nil
} }
func (s *testRenderService) RenderCSV(ctx context.Context, opts rendering.CSVOpts) (*rendering.RenderCSVResult, error) {
return nil, nil
}
func (s *testRenderService) RenderErrorImage(err error) (*rendering.RenderResult, error) { func (s *testRenderService) RenderErrorImage(err error) (*rendering.RenderResult, error) {
if s.renderErrorImageProvider != nil { if s.renderErrorImageProvider != nil {
return s.renderErrorImageProvider(err) return s.renderErrorImageProvider(err)

@ -74,13 +74,24 @@ func (srv *CleanUpService) cleanUpOldAnnotations(ctx context.Context) {
} }
func (srv *CleanUpService) cleanUpTmpFiles() { func (srv *CleanUpService) cleanUpTmpFiles() {
if _, err := os.Stat(srv.Cfg.ImagesDir); os.IsNotExist(err) { folders := []string{
srv.Cfg.ImagesDir,
srv.Cfg.CSVsDir,
}
for _, f := range folders {
srv.cleanUpTmpFolder(f)
}
}
func (srv *CleanUpService) cleanUpTmpFolder(folder string) {
if _, err := os.Stat(folder); os.IsNotExist(err) {
return return
} }
files, err := ioutil.ReadDir(srv.Cfg.ImagesDir) files, err := ioutil.ReadDir(folder)
if err != nil { if err != nil {
srv.log.Error("Problem reading image dir", "error", err) srv.log.Error("Problem reading dir", "folder", folder, "error", err)
return return
} }
@ -94,14 +105,14 @@ func (srv *CleanUpService) cleanUpTmpFiles() {
} }
for _, file := range toDelete { for _, file := range toDelete {
fullPath := path.Join(srv.Cfg.ImagesDir, file.Name()) fullPath := path.Join(folder, file.Name())
err := os.Remove(fullPath) err := os.Remove(fullPath)
if err != nil { if err != nil {
srv.log.Error("Failed to delete temp file", "file", file.Name(), "error", err) srv.log.Error("Failed to delete temp file", "file", file.Name(), "error", err)
} }
} }
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "kept", len(files)) srv.log.Debug("Found old rendered file to delete", "folder", folder, "deleted", len(toDelete), "kept", len(files))
} }
func (srv *CleanUpService) shouldCleanupTempFile(filemtime time.Time, now time.Time) bool { func (srv *CleanUpService) shouldCleanupTempFile(filemtime time.Time, now time.Time) bool {

@ -5,14 +5,14 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"mime"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/grafana/grafana/pkg/setting"
) )
var netTransport = &http.Transport{ var netTransport = &http.Transport{
@ -27,18 +27,18 @@ var netClient = &http.Client{
Transport: netTransport, Transport: netTransport,
} }
func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
filePath, err := rs.getFilePathForNewImage() filePath, err := rs.getNewFilePath(RenderPNG)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rendererUrl, err := url.Parse(rs.Cfg.RendererUrl) rendererURL, err := url.Parse(rs.Cfg.RendererUrl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
queryParams := rendererUrl.Query() queryParams := rendererURL.Query()
queryParams.Add("url", rs.getURL(opts.Path)) queryParams.Add("url", rs.getURL(opts.Path))
queryParams.Add("renderKey", renderKey) queryParams.Add("renderKey", renderKey)
queryParams.Add("width", strconv.Itoa(opts.Width)) queryParams.Add("width", strconv.Itoa(opts.Width))
@ -48,32 +48,61 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string,
queryParams.Add("encoding", opts.Encoding) queryParams.Add("encoding", opts.Encoding)
queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds()))) queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds())))
queryParams.Add("deviceScaleFactor", fmt.Sprintf("%f", opts.DeviceScaleFactor)) queryParams.Add("deviceScaleFactor", fmt.Sprintf("%f", opts.DeviceScaleFactor))
rendererUrl.RawQuery = queryParams.Encode()
req, err := http.NewRequest("GET", rendererUrl.String(), nil) rendererURL.RawQuery = queryParams.Encode()
// gives service some additional time to timeout and return possible errors.
reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
defer cancel()
resp, err := rs.doRequest(reqContext, rendererURL, opts.Headers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) // save response to file
defer func() {
if err := resp.Body.Close(); err != nil {
rs.log.Warn("Failed to close response body", "err", err)
}
}()
for k, v := range opts.Headers { err = rs.readFileResponse(reqContext, resp, filePath)
req.Header[k] = v if err != nil {
return nil, err
} }
return &RenderResult{FilePath: filePath}, nil
}
func (rs *RenderingService) renderCSVViaHTTP(ctx context.Context, renderKey string, opts CSVOpts) (*RenderCSVResult, error) {
filePath, err := rs.getNewFilePath(RenderCSV)
if err != nil {
return nil, err
}
rendererURL, err := url.Parse(rs.Cfg.RendererUrl + "/csv")
if err != nil {
return nil, err
}
queryParams := rendererURL.Query()
queryParams.Add("url", rs.getURL(opts.Path))
queryParams.Add("renderKey", renderKey)
queryParams.Add("domain", rs.domain)
queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone))
queryParams.Add("encoding", opts.Encoding)
queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds())))
rendererURL.RawQuery = queryParams.Encode()
// gives service some additional time to timeout and return possible errors. // gives service some additional time to timeout and return possible errors.
reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
defer cancel() defer cancel()
req = req.WithContext(reqContext) resp, err := rs.doRequest(reqContext, rendererURL, opts.Headers)
rs.log.Debug("calling remote rendering service", "url", rendererUrl)
// make request to renderer server
resp, err := netClient.Do(req)
if err != nil { if err != nil {
rs.log.Error("Failed to send request to remote rendering service.", "error", err) return nil, err
return nil, fmt.Errorf("failed to send request to remote rendering service: %w", err)
} }
// save response to file // save response to file
@ -83,42 +112,83 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string,
} }
}() }()
_, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
if err != nil {
return nil, err
}
downloadFileName := params["filename"]
err = rs.readFileResponse(reqContext, resp, filePath)
if err != nil {
return nil, err
}
return &RenderCSVResult{FilePath: filePath, FileName: downloadFileName}, nil
}
func (rs *RenderingService) doRequest(ctx context.Context, url *url.URL, headers map[string][]string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", rs.Cfg.BuildVersion))
for k, v := range headers {
req.Header[k] = v
}
rs.log.Debug("calling remote rendering service", "url", url)
// make request to renderer server
resp, err := netClient.Do(req)
if err != nil {
rs.log.Error("Failed to send request to remote rendering service", "error", err)
return nil, fmt.Errorf("failed to send request to remote rendering service: %w", err)
}
return resp, nil
}
func (rs *RenderingService) readFileResponse(ctx context.Context, resp *http.Response, filePath string) error {
// check for timeout first // check for timeout first
if errors.Is(reqContext.Err(), context.DeadlineExceeded) { if errors.Is(ctx.Err(), context.DeadlineExceeded) {
rs.log.Info("Rendering timed out") rs.log.Info("Rendering timed out")
return nil, ErrTimeout return ErrTimeout
} }
// if we didn't get a 200 response, something went wrong. // if we didn't get a 200 response, something went wrong.
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
rs.log.Error("Remote rendering request failed", "error", resp.Status) rs.log.Error("Remote rendering request failed", "error", resp.Status)
return nil, fmt.Errorf("remote rendering request failed, status code: %d, status: %s", resp.StatusCode, return fmt.Errorf("remote rendering request failed, status code: %d, status: %s", resp.StatusCode,
resp.Status) resp.Status)
} }
out, err := os.Create(filePath) out, err := os.Create(filePath)
if err != nil { if err != nil {
return nil, err return err
} }
defer func() { defer func() {
if err := out.Close(); err != nil { if err := out.Close(); err != nil && !errors.Is(err, fs.ErrClosed) {
// We already close the file explicitly in the non-error path, so shouldn't be a problem // We already close the file explicitly in the non-error path, so shouldn't be a problem
rs.log.Warn("Failed to close file", "path", filePath, "err", err) rs.log.Warn("Failed to close file", "path", filePath, "err", err)
} }
}() }()
_, err = io.Copy(out, resp.Body) _, err = io.Copy(out, resp.Body)
if err != nil { if err != nil {
// check that we didn't timeout while receiving the response. // check that we didn't timeout while receiving the response.
if errors.Is(reqContext.Err(), context.DeadlineExceeded) { if errors.Is(ctx.Err(), context.DeadlineExceeded) {
rs.log.Info("Rendering timed out") rs.log.Info("Rendering timed out")
return nil, ErrTimeout return ErrTimeout
} }
rs.log.Error("Remote rendering request failed", "error", err) rs.log.Error("Remote rendering request failed", "error", err)
return nil, fmt.Errorf("remote rendering request failed: %w", err) return fmt.Errorf("remote rendering request failed: %w", err)
} }
if err := out.Close(); err != nil { if err := out.Close(); err != nil {
return nil, fmt.Errorf("failed to write to %q: %w", filePath, err) return fmt.Errorf("failed to write to %q: %w", filePath, err)
} }
return &RenderResult{FilePath: filePath}, err return nil
} }

@ -9,14 +9,22 @@ import (
) )
var ErrTimeout = errors.New("timeout error - you can set timeout in seconds with &timeout url parameter") var ErrTimeout = errors.New("timeout error - you can set timeout in seconds with &timeout url parameter")
var ErrPhantomJSNotInstalled = errors.New("PhantomJS executable not found") var ErrConcurrentLimitReached = errors.New("rendering concurrent limit reached")
var ErrRenderUnavailable = errors.New("rendering plugin not available")
type RenderType string
const (
RenderCSV RenderType = "csv"
RenderPNG RenderType = "png"
)
type Opts struct { type Opts struct {
Width int Width int
Height int Height int
Timeout time.Duration Timeout time.Duration
OrgId int64 OrgID int64
UserId int64 UserID int64
OrgRole models.RoleType OrgRole models.RoleType
Path string Path string
Encoding string Encoding string
@ -26,15 +34,34 @@ type Opts struct {
Headers map[string][]string Headers map[string][]string
} }
type CSVOpts struct {
Timeout time.Duration
OrgID int64
UserID int64
OrgRole models.RoleType
Path string
Encoding string
Timezone string
ConcurrentLimit int
Headers map[string][]string
}
type RenderResult struct { type RenderResult struct {
FilePath string FilePath string
} }
type RenderCSVResult struct {
FilePath string
FileName string
}
type renderFunc func(ctx context.Context, renderKey string, options Opts) (*RenderResult, error) type renderFunc func(ctx context.Context, renderKey string, options Opts) (*RenderResult, error)
type renderCSVFunc func(ctx context.Context, renderKey string, options CSVOpts) (*RenderCSVResult, error)
type Service interface { type Service interface {
IsAvailable() bool IsAvailable() bool
Render(ctx context.Context, opts Opts) (*RenderResult, error) Render(ctx context.Context, opts Opts) (*RenderResult, error)
RenderCSV(ctx context.Context, opts CSVOpts) (*RenderCSVResult, error)
RenderErrorImage(error error) (*RenderResult, error) RenderErrorImage(error error) (*RenderResult, error)
GetRenderUser(key string) (*RenderUser, bool) GetRenderUser(key string) (*RenderUser, bool)
} }

@ -27,7 +27,7 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey strin
} }
func (rs *RenderingService) renderViaPluginV1(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { func (rs *RenderingService) renderViaPluginV1(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
pngPath, err := rs.getFilePathForNewImage() filePath, err := rs.getNewFilePath(RenderPNG)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -36,7 +36,7 @@ func (rs *RenderingService) renderViaPluginV1(ctx context.Context, renderKey str
Url: rs.getURL(opts.Path), Url: rs.getURL(opts.Path),
Width: int32(opts.Width), Width: int32(opts.Width),
Height: int32(opts.Height), Height: int32(opts.Height),
FilePath: pngPath, FilePath: filePath,
Timeout: int32(opts.Timeout.Seconds()), Timeout: int32(opts.Timeout.Seconds()),
RenderKey: renderKey, RenderKey: renderKey,
Encoding: opts.Encoding, Encoding: opts.Encoding,
@ -57,11 +57,11 @@ func (rs *RenderingService) renderViaPluginV1(ctx context.Context, renderKey str
return nil, fmt.Errorf("rendering failed: %v", rsp.Error) return nil, fmt.Errorf("rendering failed: %v", rsp.Error)
} }
return &RenderResult{FilePath: pngPath}, nil return &RenderResult{FilePath: filePath}, nil
} }
func (rs *RenderingService) renderViaPluginV2(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { func (rs *RenderingService) renderViaPluginV2(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
pngPath, err := rs.getFilePathForNewImage() filePath, err := rs.getNewFilePath(RenderPNG)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -79,7 +79,7 @@ func (rs *RenderingService) renderViaPluginV2(ctx context.Context, renderKey str
Width: int32(opts.Width), Width: int32(opts.Width),
Height: int32(opts.Height), Height: int32(opts.Height),
DeviceScaleFactor: float32(opts.DeviceScaleFactor), DeviceScaleFactor: float32(opts.DeviceScaleFactor),
FilePath: pngPath, FilePath: filePath,
Timeout: int32(opts.Timeout.Seconds()), Timeout: int32(opts.Timeout.Seconds()),
RenderKey: renderKey, RenderKey: renderKey,
Timezone: isoTimeOffsetToPosixTz(opts.Timezone), Timezone: isoTimeOffsetToPosixTz(opts.Timezone),
@ -100,5 +100,50 @@ func (rs *RenderingService) renderViaPluginV2(ctx context.Context, renderKey str
return nil, fmt.Errorf("rendering failed: %s", rsp.Error) return nil, fmt.Errorf("rendering failed: %s", rsp.Error)
} }
return &RenderResult{FilePath: pngPath}, err return &RenderResult{FilePath: filePath}, err
}
func (rs *RenderingService) renderCSVViaPlugin(ctx context.Context, renderKey string, opts CSVOpts) (*RenderCSVResult, error) {
// gives plugin some additional time to timeout and return possible errors.
ctx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
defer cancel()
filePath, err := rs.getNewFilePath(RenderCSV)
if err != nil {
return nil, err
}
headers := map[string]*pluginextensionv2.StringList{}
for k, values := range opts.Headers {
headers[k] = &pluginextensionv2.StringList{
Values: values,
}
}
req := &pluginextensionv2.RenderCSVRequest{
Url: rs.getURL(opts.Path),
FilePath: filePath,
RenderKey: renderKey,
Domain: rs.domain,
Timeout: int32(opts.Timeout.Seconds()),
Timezone: isoTimeOffsetToPosixTz(opts.Timezone),
Headers: headers,
}
rs.log.Debug("Calling renderer plugin", "req", req)
rsp, err := rs.pluginInfo.GrpcPluginV2.RenderCSV(ctx, req)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
rs.log.Info("Rendering timed out")
return nil, ErrTimeout
}
return nil, err
}
if rsp.Error != "" {
return nil, fmt.Errorf("rendering failed: %s", rsp.Error)
}
return &RenderCSVResult{FilePath: filePath, FileName: rsp.FileName}, nil
} }

@ -44,6 +44,7 @@ type RenderingService struct {
log log.Logger log log.Logger
pluginInfo *plugins.RendererPlugin pluginInfo *plugins.RendererPlugin
renderAction renderFunc renderAction renderFunc
renderCSVAction renderCSVFunc
domain string domain string
inProgressCount int inProgressCount int
@ -61,6 +62,12 @@ func (rs *RenderingService) Init() error {
return fmt.Errorf("failed to create images directory %q: %w", rs.Cfg.ImagesDir, err) return fmt.Errorf("failed to create images directory %q: %w", rs.Cfg.ImagesDir, err)
} }
// ensure CSVsDir exists
err = os.MkdirAll(rs.Cfg.CSVsDir, 0700)
if err != nil {
return fmt.Errorf("failed to create CSVs directory %q: %w", rs.Cfg.CSVsDir, err)
}
// set value used for domain attribute of renderKey cookie // set value used for domain attribute of renderKey cookie
switch { switch {
case rs.Cfg.RendererUrl != "": case rs.Cfg.RendererUrl != "":
@ -80,7 +87,8 @@ func (rs *RenderingService) Run(ctx context.Context) error {
if rs.remoteAvailable() { if rs.remoteAvailable() {
rs.log = rs.log.New("renderer", "http") rs.log = rs.log.New("renderer", "http")
rs.log.Info("Backend rendering via external http server") rs.log.Info("Backend rendering via external http server")
rs.renderAction = rs.renderViaHttp rs.renderAction = rs.renderViaHTTP
rs.renderCSVAction = rs.renderCSVViaHTTP
<-ctx.Done() <-ctx.Done()
return nil return nil
} }
@ -94,6 +102,7 @@ func (rs *RenderingService) Run(ctx context.Context) error {
} }
rs.renderAction = rs.renderViaPlugin rs.renderAction = rs.renderViaPlugin
rs.renderCSVAction = rs.renderCSVViaPlugin
<-ctx.Done() <-ctx.Done()
return nil return nil
} }
@ -136,23 +145,12 @@ func (rs *RenderingService) renderUnavailableImage() *RenderResult {
func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) { func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) {
startTime := time.Now() startTime := time.Now()
elapsedTime := time.Since(startTime).Milliseconds()
result, err := rs.render(ctx, opts) result, err := rs.render(ctx, opts)
if err != nil {
if errors.Is(err, ErrTimeout) {
metrics.MRenderingRequestTotal.WithLabelValues("timeout").Inc()
metrics.MRenderingSummary.WithLabelValues("timeout").Observe(float64(elapsedTime))
} else {
metrics.MRenderingRequestTotal.WithLabelValues("failure").Inc()
metrics.MRenderingSummary.WithLabelValues("failure").Observe(float64(elapsedTime))
}
return nil, err elapsedTime := time.Since(startTime).Milliseconds()
} saveMetrics(elapsedTime, err, RenderPNG)
metrics.MRenderingRequestTotal.WithLabelValues("success").Inc() return result, err
metrics.MRenderingSummary.WithLabelValues("success").Observe(float64(elapsedTime))
return result, nil
} }
func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResult, error) { func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResult, error) {
@ -173,7 +171,7 @@ func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResul
if math.IsInf(opts.DeviceScaleFactor, 0) || math.IsNaN(opts.DeviceScaleFactor) || opts.DeviceScaleFactor <= 0 { if math.IsInf(opts.DeviceScaleFactor, 0) || math.IsNaN(opts.DeviceScaleFactor) || opts.DeviceScaleFactor <= 0 {
opts.DeviceScaleFactor = 1 opts.DeviceScaleFactor = 1
} }
renderKey, err := rs.generateAndStoreRenderKey(opts.OrgId, opts.UserId, opts.OrgRole) renderKey, err := rs.generateAndStoreRenderKey(opts.OrgID, opts.UserID, opts.OrgRole)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -190,6 +188,43 @@ func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResul
return rs.renderAction(ctx, renderKey, opts) return rs.renderAction(ctx, renderKey, opts)
} }
func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts) (*RenderCSVResult, error) {
startTime := time.Now()
result, err := rs.renderCSV(ctx, opts)
elapsedTime := time.Since(startTime).Milliseconds()
saveMetrics(elapsedTime, err, RenderCSV)
return result, err
}
func (rs *RenderingService) renderCSV(ctx context.Context, opts CSVOpts) (*RenderCSVResult, error) {
if rs.inProgressCount > opts.ConcurrentLimit {
return nil, ErrConcurrentLimitReached
}
if !rs.IsAvailable() {
return nil, ErrRenderUnavailable
}
rs.log.Info("Rendering", "path", opts.Path)
renderKey, err := rs.generateAndStoreRenderKey(opts.OrgID, opts.UserID, opts.OrgRole)
if err != nil {
return nil, err
}
defer rs.deleteRenderKey(renderKey)
defer func() {
rs.inProgressCount--
metrics.MRenderingQueue.Set(float64(rs.inProgressCount))
}()
rs.inProgressCount++
metrics.MRenderingQueue.Set(float64(rs.inProgressCount))
return rs.renderCSVAction(ctx, renderKey, opts)
}
func (rs *RenderingService) GetRenderUser(key string) (*RenderUser, bool) { func (rs *RenderingService) GetRenderUser(key string) (*RenderUser, bool) {
val, err := rs.RemoteCacheService.Get(fmt.Sprintf(renderKeyPrefix, key)) val, err := rs.RemoteCacheService.Get(fmt.Sprintf(renderKeyPrefix, key))
if err != nil { if err != nil {
@ -205,17 +240,20 @@ func (rs *RenderingService) GetRenderUser(key string) (*RenderUser, bool) {
return nil, false return nil, false
} }
func (rs *RenderingService) getFilePathForNewImage() (string, error) { func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) {
rand, err := util.GetRandomString(20) rand, err := util.GetRandomString(20)
if err != nil { if err != nil {
return "", err return "", err
} }
pngPath, err := filepath.Abs(filepath.Join(rs.Cfg.ImagesDir, rand))
if err != nil { ext := "png"
return "", err folder := rs.Cfg.ImagesDir
if rt == RenderCSV {
ext = "csv"
folder = rs.Cfg.CSVsDir
} }
return pngPath + ".png", nil return filepath.Abs(filepath.Join(folder, fmt.Sprintf("%s.%s", rand, ext)))
} }
func (rs *RenderingService) getURL(path string) string { func (rs *RenderingService) getURL(path string) string {
@ -282,3 +320,19 @@ func isoTimeOffsetToPosixTz(isoOffset string) string {
} }
return isoOffset return isoOffset
} }
func saveMetrics(elapsedTime int64, err error, renderType RenderType) {
if err == nil {
metrics.MRenderingRequestTotal.WithLabelValues("success", string(renderType)).Inc()
metrics.MRenderingSummary.WithLabelValues("success", string(renderType)).Observe(float64(elapsedTime))
return
}
if errors.Is(err, ErrTimeout) {
metrics.MRenderingRequestTotal.WithLabelValues("timeout", string(renderType)).Inc()
metrics.MRenderingSummary.WithLabelValues("timeout", string(renderType)).Observe(float64(elapsedTime))
} else {
metrics.MRenderingRequestTotal.WithLabelValues("failure", string(renderType)).Inc()
metrics.MRenderingSummary.WithLabelValues("failure", string(renderType)).Observe(float64(elapsedTime))
}
}

@ -228,6 +228,7 @@ type Cfg struct {
// Rendering // Rendering
ImagesDir string ImagesDir string
CSVsDir string
RendererUrl string RendererUrl string
RendererCallbackUrl string RendererCallbackUrl string
RendererConcurrentRequestLimit int RendererConcurrentRequestLimit int
@ -1297,6 +1298,7 @@ func readRenderingSettings(iniFile *ini.File, cfg *Cfg) error {
cfg.RendererConcurrentRequestLimit = renderSec.Key("concurrent_render_request_limit").MustInt(30) cfg.RendererConcurrentRequestLimit = renderSec.Key("concurrent_render_request_limit").MustInt(30)
cfg.ImagesDir = filepath.Join(cfg.DataPath, "png") cfg.ImagesDir = filepath.Join(cfg.DataPath, "png")
cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv")
return nil return nil
} }

Loading…
Cancel
Save