From ec71919e7b10c2488e893a3095f640338249414d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Wed, 12 May 2021 17:16:57 +0200 Subject: [PATCH] Rendering: add CSV support (#33729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 --- pkg/api/render.go | 14 +- pkg/infra/metrics/metrics.go | 10 +- .../pluginextensionv2/rendererv2.pb.go | 296 ++++++++++++++++-- .../pluginextensionv2/rendererv2.proto | 16 + pkg/services/alerting/notifier.go | 2 +- pkg/services/alerting/notifier_test.go | 4 + pkg/services/cleanup/cleanup.go | 21 +- pkg/services/rendering/http_mode.go | 140 ++++++--- pkg/services/rendering/interface.go | 33 +- pkg/services/rendering/plugin_mode.go | 57 +++- pkg/services/rendering/rendering.go | 96 ++++-- pkg/setting/setting.go | 2 + 12 files changed, 579 insertions(+), 112 deletions(-) diff --git a/pkg/api/render.go b/pkg/api/render.go index 6940ff8ac4b..a52b9b7f603 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -4,9 +4,7 @@ import ( "errors" "fmt" "net/http" - "runtime" "strconv" - "strings" "time" "github.com/grafana/grafana/pkg/models" @@ -57,8 +55,8 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { Width: width, Height: height, Timeout: time.Duration(timeout) * time.Second, - OrgId: c.OrgId, - UserId: c.UserId, + OrgID: c.OrgId, + UserID: c.UserId, OrgRole: c.OrgRole, Path: c.Params("*") + queryParams, Timezone: queryReader.Get("tz", ""), @@ -72,14 +70,6 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { c.Handle(hs.Cfg, 500, err.Error(), err) 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) return diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go index ebdbf5adb4d..5e23681f5fd 100644 --- a/pkg/infra/metrics/metrics.go +++ b/pkg/infra/metrics/metrics.go @@ -367,25 +367,25 @@ func init() { MRenderingRequestTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "rendering_request_total", - Help: "counter for image rendering requests", + Help: "counter for rendering requests", Namespace: ExporterName, }, - []string{"status"}, + []string{"status", "type"}, ) MRenderingSummary = prometheus.NewSummaryVec( prometheus.SummaryOpts{ Name: "rendering_request_duration_milliseconds", - Help: "summary of image rendering request duration", + Help: "summary of rendering request duration", Objectives: objectiveMap, Namespace: ExporterName, }, - []string{"status"}, + []string{"status", "type"}, ) MRenderingQueue = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "rendering_queue_size", - Help: "size of image rendering queue", + Help: "size of rendering queue", Namespace: ExporterName, }) diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go index 3f05f55d2f4..6cce4b154e5 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.15.6 +// protoc v3.15.8 // source: rendererv2.proto package pluginextensionv2 @@ -237,6 +237,156 @@ func (x *RenderResponse) GetError() string { 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_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, 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, - 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, - 0x59, 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, + 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, + 0xd3, 0x02, 0x0a, 0x10, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, + 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, 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, 0x42, 0x16, 0x5a, 0x14, 0x2e, 0x2f, - 0x3b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, - 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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, + 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 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, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x16, 0x5a, 0x14, 0x2e, 0x2f, 0x3b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -296,23 +477,30 @@ func file_rendererv2_proto_rawDescGZIP() []byte { 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{}{ - (*StringList)(nil), // 0: pluginextensionv2.StringList - (*RenderRequest)(nil), // 1: pluginextensionv2.RenderRequest - (*RenderResponse)(nil), // 2: pluginextensionv2.RenderResponse - nil, // 3: pluginextensionv2.RenderRequest.HeadersEntry + (*StringList)(nil), // 0: pluginextensionv2.StringList + (*RenderRequest)(nil), // 1: pluginextensionv2.RenderRequest + (*RenderResponse)(nil), // 2: pluginextensionv2.RenderResponse + (*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{ - 3, // 0: pluginextensionv2.RenderRequest.headers:type_name -> pluginextensionv2.RenderRequest.HeadersEntry - 0, // 1: pluginextensionv2.RenderRequest.HeadersEntry.value:type_name -> pluginextensionv2.StringList - 1, // 2: pluginextensionv2.Renderer.Render:input_type -> pluginextensionv2.RenderRequest - 2, // 3: pluginextensionv2.Renderer.Render:output_type -> pluginextensionv2.RenderResponse - 3, // [3:4] is the sub-list for method output_type - 2, // [2:3] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 5, // 0: pluginextensionv2.RenderRequest.headers:type_name -> pluginextensionv2.RenderRequest.HeadersEntry + 6, // 1: pluginextensionv2.RenderCSVRequest.headers:type_name -> pluginextensionv2.RenderCSVRequest.HeadersEntry + 0, // 2: pluginextensionv2.RenderRequest.HeadersEntry.value:type_name -> pluginextensionv2.StringList + 0, // 3: pluginextensionv2.RenderCSVRequest.HeadersEntry.value:type_name -> pluginextensionv2.StringList + 1, // 4: pluginextensionv2.Renderer.Render:input_type -> pluginextensionv2.RenderRequest + 3, // 5: pluginextensionv2.Renderer.RenderCSV:input_type -> pluginextensionv2.RenderCSVRequest + 2, // 6: pluginextensionv2.Renderer.Render:output_type -> pluginextensionv2.RenderResponse + 4, // 7: pluginextensionv2.Renderer.RenderCSV:output_type -> pluginextensionv2.RenderCSVResponse + 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() } @@ -357,6 +545,30 @@ func file_rendererv2_proto_init() { 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{} out := protoimpl.TypeBuilder{ @@ -364,7 +576,7 @@ func file_rendererv2_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_rendererv2_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 7, NumExtensions: 0, 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. type RendererClient interface { 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 { @@ -410,9 +623,19 @@ func (c *rendererClient) Render(ctx context.Context, in *RenderRequest, opts ... 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. type RendererServer interface { Render(context.Context, *RenderRequest) (*RenderResponse, error) + RenderCSV(context.Context, *RenderCSVRequest) (*RenderCSVResponse, error) } // UnimplementedRendererServer can be embedded to have forward compatible implementations. @@ -422,6 +645,9 @@ type UnimplementedRendererServer struct { func (*UnimplementedRendererServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { 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) { 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) } +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{ ServiceName: "pluginextensionv2.Renderer", HandlerType: (*RendererServer)(nil), @@ -453,6 +697,10 @@ var _Renderer_serviceDesc = grpc.ServiceDesc{ MethodName: "Render", Handler: _Renderer_Render_Handler, }, + { + MethodName: "RenderCSV", + Handler: _Renderer_RenderCSV_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "rendererv2.proto", diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto index 01964400ad6..5a2ae102631 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto @@ -24,6 +24,22 @@ message RenderResponse { string error = 1; } +message RenderCSVRequest { + string url = 1; + string filePath = 2; + string renderKey = 3; + string domain = 4; + int32 timeout = 5; + string timezone = 6; + map headers = 7; +} + +message RenderCSVResponse { + string error = 1; + string fileName = 2; +} + service Renderer { rpc Render(RenderRequest) returns (RenderResponse); + rpc RenderCSV(RenderCSVRequest) returns (RenderCSVResponse); } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 0cb172328be..42e68c5e60e 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -201,7 +201,7 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout Width: 1000, Height: 500, Timeout: timeout, - OrgId: evalCtx.Rule.OrgID, + OrgID: evalCtx.Rule.OrgID, OrgRole: models.ROLE_ADMIN, ConcurrentLimit: setting.AlertingRenderLimit, } diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index f2c671c8547..11fe8c61228 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -361,6 +361,10 @@ func (s *testRenderService) Render(ctx context.Context, opts rendering.Opts) (*r 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) { if s.renderErrorImageProvider != nil { return s.renderErrorImageProvider(err) diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index 8254a4facd7..6c802d598a4 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -74,13 +74,24 @@ func (srv *CleanUpService) cleanUpOldAnnotations(ctx context.Context) { } 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 } - files, err := ioutil.ReadDir(srv.Cfg.ImagesDir) + files, err := ioutil.ReadDir(folder) if err != nil { - srv.log.Error("Problem reading image dir", "error", err) + srv.log.Error("Problem reading dir", "folder", folder, "error", err) return } @@ -94,14 +105,14 @@ func (srv *CleanUpService) cleanUpTmpFiles() { } for _, file := range toDelete { - fullPath := path.Join(srv.Cfg.ImagesDir, file.Name()) + fullPath := path.Join(folder, file.Name()) err := os.Remove(fullPath) if err != nil { 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 { diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index 96d1ead5465..8102ace59a0 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -5,14 +5,14 @@ import ( "errors" "fmt" "io" + "io/fs" + "mime" "net" "net/http" "net/url" "os" "strconv" "time" - - "github.com/grafana/grafana/pkg/setting" ) var netTransport = &http.Transport{ @@ -27,18 +27,18 @@ var netClient = &http.Client{ Transport: netTransport, } -func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { - filePath, err := rs.getFilePathForNewImage() +func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { + filePath, err := rs.getNewFilePath(RenderPNG) if err != nil { return nil, err } - rendererUrl, err := url.Parse(rs.Cfg.RendererUrl) + rendererURL, err := url.Parse(rs.Cfg.RendererUrl) if err != nil { return nil, err } - queryParams := rendererUrl.Query() + queryParams := rendererURL.Query() queryParams.Add("url", rs.getURL(opts.Path)) queryParams.Add("renderKey", renderKey) queryParams.Add("width", strconv.Itoa(opts.Width)) @@ -48,32 +48,16 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, queryParams.Add("encoding", opts.Encoding) queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds()))) queryParams.Add("deviceScaleFactor", fmt.Sprintf("%f", opts.DeviceScaleFactor)) - rendererUrl.RawQuery = queryParams.Encode() - req, err := http.NewRequest("GET", rendererUrl.String(), nil) - if err != nil { - return nil, err - } - - req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) - - for k, v := range opts.Headers { - req.Header[k] = v - } + 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() - req = req.WithContext(reqContext) - - rs.log.Debug("calling remote rendering service", "url", rendererUrl) - - // make request to renderer server - resp, err := netClient.Do(req) + resp, err := rs.doRequest(reqContext, rendererURL, opts.Headers) 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 nil, err } // save response to file @@ -83,42 +67,128 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, } }() + err = rs.readFileResponse(reqContext, resp, filePath) + 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. + reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) + defer cancel() + + resp, err := rs.doRequest(reqContext, rendererURL, opts.Headers) + if err != nil { + return nil, err + } + + // save response to file + defer func() { + if err := resp.Body.Close(); err != nil { + rs.log.Warn("Failed to close response body", "err", err) + } + }() + + _, 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 - if errors.Is(reqContext.Err(), context.DeadlineExceeded) { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { rs.log.Info("Rendering timed out") - return nil, ErrTimeout + return ErrTimeout } // if we didn't get a 200 response, something went wrong. if resp.StatusCode != http.StatusOK { 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) } out, err := os.Create(filePath) if err != nil { - return nil, err + return err } + 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 rs.log.Warn("Failed to close file", "path", filePath, "err", err) } }() + _, err = io.Copy(out, resp.Body) if err != nil { // 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") - return nil, ErrTimeout + return ErrTimeout } + 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 { - 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 } diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index 9ef97139864..e9125f106b3 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -9,14 +9,22 @@ import ( ) 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 { Width int Height int Timeout time.Duration - OrgId int64 - UserId int64 + OrgID int64 + UserID int64 OrgRole models.RoleType Path string Encoding string @@ -26,15 +34,34 @@ type Opts struct { 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 { FilePath string } +type RenderCSVResult struct { + FilePath string + FileName string +} + 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 { IsAvailable() bool Render(ctx context.Context, opts Opts) (*RenderResult, error) + RenderCSV(ctx context.Context, opts CSVOpts) (*RenderCSVResult, error) RenderErrorImage(error error) (*RenderResult, error) GetRenderUser(key string) (*RenderUser, bool) } diff --git a/pkg/services/rendering/plugin_mode.go b/pkg/services/rendering/plugin_mode.go index 6d3d4d15b26..545c3a1b6c6 100644 --- a/pkg/services/rendering/plugin_mode.go +++ b/pkg/services/rendering/plugin_mode.go @@ -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) { - pngPath, err := rs.getFilePathForNewImage() + filePath, err := rs.getNewFilePath(RenderPNG) if err != nil { return nil, err } @@ -36,7 +36,7 @@ func (rs *RenderingService) renderViaPluginV1(ctx context.Context, renderKey str Url: rs.getURL(opts.Path), Width: int32(opts.Width), Height: int32(opts.Height), - FilePath: pngPath, + FilePath: filePath, Timeout: int32(opts.Timeout.Seconds()), RenderKey: renderKey, 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 &RenderResult{FilePath: pngPath}, nil + return &RenderResult{FilePath: filePath}, nil } 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 { return nil, err } @@ -79,7 +79,7 @@ func (rs *RenderingService) renderViaPluginV2(ctx context.Context, renderKey str Width: int32(opts.Width), Height: int32(opts.Height), DeviceScaleFactor: float32(opts.DeviceScaleFactor), - FilePath: pngPath, + FilePath: filePath, Timeout: int32(opts.Timeout.Seconds()), RenderKey: renderKey, 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 &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 } diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index d9a65297d4c..cc4b9d56ede 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -44,6 +44,7 @@ type RenderingService struct { log log.Logger pluginInfo *plugins.RendererPlugin renderAction renderFunc + renderCSVAction renderCSVFunc domain string 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) } + // 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 switch { case rs.Cfg.RendererUrl != "": @@ -80,7 +87,8 @@ func (rs *RenderingService) Run(ctx context.Context) error { if rs.remoteAvailable() { rs.log = rs.log.New("renderer", "http") rs.log.Info("Backend rendering via external http server") - rs.renderAction = rs.renderViaHttp + rs.renderAction = rs.renderViaHTTP + rs.renderCSVAction = rs.renderCSVViaHTTP <-ctx.Done() return nil } @@ -94,6 +102,7 @@ func (rs *RenderingService) Run(ctx context.Context) error { } rs.renderAction = rs.renderViaPlugin + rs.renderCSVAction = rs.renderCSVViaPlugin <-ctx.Done() return nil } @@ -136,23 +145,12 @@ func (rs *RenderingService) renderUnavailableImage() *RenderResult { func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) { startTime := time.Now() - elapsedTime := time.Since(startTime).Milliseconds() 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() - metrics.MRenderingSummary.WithLabelValues("success").Observe(float64(elapsedTime)) - return result, nil + return result, err } 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 { 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 { return nil, err } @@ -190,6 +188,43 @@ func (rs *RenderingService) render(ctx context.Context, opts Opts) (*RenderResul 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) { val, err := rs.RemoteCacheService.Get(fmt.Sprintf(renderKeyPrefix, key)) if err != nil { @@ -205,17 +240,20 @@ func (rs *RenderingService) GetRenderUser(key string) (*RenderUser, bool) { return nil, false } -func (rs *RenderingService) getFilePathForNewImage() (string, error) { +func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) { rand, err := util.GetRandomString(20) if err != nil { return "", err } - pngPath, err := filepath.Abs(filepath.Join(rs.Cfg.ImagesDir, rand)) - if err != nil { - return "", err + + ext := "png" + 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 { @@ -282,3 +320,19 @@ func isoTimeOffsetToPosixTz(isoOffset string) string { } 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)) + } +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 42f6a535a0a..22a8c9e79f0 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -228,6 +228,7 @@ type Cfg struct { // Rendering ImagesDir string + CSVsDir string RendererUrl string RendererCallbackUrl string 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.ImagesDir = filepath.Join(cfg.DataPath, "png") + cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv") return nil }