diff --git a/.travis.yml b/.travis.yml index 063bd58..2d89c8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: ruby rvm: -- 2.3.3 - 2.5.5 +- 3.0.1 before_install: - export TZ=Europe/Berlin diff --git a/README.md b/README.md index 9a1681e..ec0afcf 100644 --- a/README.md +++ b/README.md @@ -24,29 +24,38 @@ home's energy usage. That's how it started. ## Features -* Build PDF reports based on [grafana](https://github.com/grafana/grafana) dashboards -(other formats supported) -* Include dynamic content from grafana (see [function documentation](FUNCTION_CALLS.md) -as a detailed reference): +* Build reports based on [grafana](https://github.com/grafana/grafana) dashboards, PDF +(default) and many other formats supported +* Easy-to-use configuration wizard, including fully automated functionality to create a +demo report +* Include dynamic content from grafana (find here a reference for all +[asciidcotor reporter calls](FUNCTION_CALLS.md)): * panels as images * tables based on grafana panel queries or custom database queries (no images!) * single values to be integrated in text, based on grafana panel queries or custom database queries * Multi purpose use of the reporter - * webservice to be called directly from grafana - it also runs without further -dependencies in the standard asciidoctor docker container! - * standalone command line tool, e.g. to be automated with cron or bash scrips -* Comes with a complete configuration wizard, including functionality to build a -demo report on top of the configured grafana host -* Supports all SQL based datasources, as well as graphite and prometheus + * webservice to be called directly from grafana + * standalone command line tool, e.g. to be automated with `cron` or `bash` scrips + * seemlessly runs from asciidocotor docker container without further dependencies +* Webhook callbacks before, on cancel and on finishing callbacks (see configuration file) * Solid as a rock, also in case of template errors and whatever else may happen * Full [API documentation](https://rubydoc.info/gems/ruby-grafana-reporter) available +Functionalities are provided as shown here: + +Database | Image rendering | Panel-based rendering | Query-based rendering +------------------------- | :-------: | :-----------: | :------------: +all SQL based datasources | supported | supported | supported +Graphite | supported | supported | supported +Prometheus | supported | supported | supported +other datasources | supported | not-supported | not-supported + ## Quick Start You don't have a grafana setup runnning already? No worries, just configure `https://play.grafana.org` in the configuration wizard and see the magic -happen for that! +happen! If your grafana setup requires a login, you'll have to setup an api key for the reporter. Please follow the steps @@ -58,10 +67,6 @@ first. * [Download latest Windows executable](https://github.com/divinity666/ruby-grafana-reporter/releases/latest) * `ruby-grafana-reporter -w` -Known issues: -* images are currently not included in PDF conversions due to missing support in Prawn gem for windows; -other target formats do work properly with images - **Raspberry Pi:** * `sudo apt-get install ruby` @@ -175,6 +180,6 @@ Inspired by [Izak Marai's grafana reporter](https://github.com/IzakMarais/report ## Donations If this project saves you as much time as I hope it does, and if you'd like to -support my work, feel free donate, even a cup of coffee is appreciated :) +support my work, feel free donate. :) [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?hosted_button_id=35LH6JNLPHPHQ) diff --git a/lib/VERSION.rb b/lib/VERSION.rb index d906eeb..bb7a178 100644 --- a/lib/VERSION.rb +++ b/lib/VERSION.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true # Version information -GRAFANA_REPORTER_VERSION = [0, 4, 0].freeze +GRAFANA_REPORTER_VERSION = [0, 4, 1].freeze # Release date -GRAFANA_REPORTER_RELEASE_DATE = '2021-04-14' +GRAFANA_REPORTER_RELEASE_DATE = '2021-05-17' diff --git a/lib/grafana/abstract_datasource.rb b/lib/grafana/abstract_datasource.rb index 27b9a1e..30dafaa 100644 --- a/lib/grafana/abstract_datasource.rb +++ b/lib/grafana/abstract_datasource.rb @@ -6,33 +6,53 @@ module Grafana class AbstractDatasource attr_reader :model + @@subclasses = [] + + # Registers the subclass as datasource, which is asked by {#accepts?}, if it can handle a datasource + # model. + # @param subclass [Class] class inheriting from this abstract class + def self.inherited(subclass) + @@subclasses << subclass + end + + # Overwrite this method, to specify if the current datasource implementation handles the given model. + # This method is called by {#build_instance} to determine, if the current datasource implementation + # can handle the given grafana model. By default this method returns false. + # @param model [Hash] grafana specification of the datasource to check + # @return [Boolean] True if fits, false otherwise + def self.handles?(_model) + false + end + # Factory method to build a datasource from a given datasource Hash description. # @param ds_model [Hash] grafana specification of a single datasource # @return [AbstractDatasource] instance of a fitting datasource implementation def self.build_instance(ds_model) raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model.is_a?(Hash) - raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model['meta'] - raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model['meta']['id'] - raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model['meta']['category'] - - return SqlDatasource.new(ds_model) if ds_model['meta']['category'] == 'sql' - case ds_model['meta']['id'] - when 'graphite' - return GraphiteDatasource.new(ds_model) - - when 'prometheus' - return PrometheusDatasource.new(ds_model) + raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model['meta'].is_a?(Hash) + @@subclasses.each do |datasource_class| + return datasource_class.new(ds_model) if datasource_class.handles?(ds_model) end - raise DatasourceTypeNotSupportedError.new(ds_model['name'], ds_model['meta']['id']) + UnsupportedDatasource.new(ds_model) end def initialize(model) @model = model end + # @return [String] category of the datasource, e.g. `tsdb` or `sql` + def category + @model['meta']['category'] + end + + # @return [String] type of the datasource, e.g. `mysql` + def type + @model['type'] || @model['meta']['id'] + end + # @return [String] name of the datasource def name @model['name'] diff --git a/lib/grafana/errors.rb b/lib/grafana/errors.rb index 9b50680..72f2adc 100644 --- a/lib/grafana/errors.rb +++ b/lib/grafana/errors.rb @@ -57,21 +57,13 @@ def initialize(panel) end end - # Raised if no SQL query is specified in a {AbstractSqlQuery} object. + # Raised if no SQL query is specified. class MissingSqlQueryError < GrafanaError def initialize super('No SQL statement has been specified.') end end - # Raised if a datasource shall be queried, which is not (yet) supported by the reporter - class DatasourceTypeNotSupportedError < GrafanaError - def initialize(name, type) - super("The configured datasource with name '#{name}' is of type '#{type}', which is currently "\ - 'not supported by ruby-grafana-reporter. It will only be usable in panel image queries.') - end - end - # Raised if a datasource shall be queried, which is not (yet) supported by the reporter class InvalidDatasourceQueryProvidedError < GrafanaError def initialize(query) diff --git a/lib/grafana/grafana.rb b/lib/grafana/grafana.rb index d74b190..b5037ea 100644 --- a/lib/grafana/grafana.rb +++ b/lib/grafana/grafana.rb @@ -9,6 +9,8 @@ module Grafana # Main class for handling the interaction with one specific Grafana instance. class Grafana + attr_reader :logger + # @param base_uri [String] full URI pointing to the specific grafana instance without # trailing slash, e.g. +https://localhost:3000+. # @param key [String] API key for the grafana instance, if required @@ -116,13 +118,7 @@ def initialize_datasources json = JSON.parse(settings.body) json['datasources'].select { |_k, v| v['id'].to_i.positive? }.each do |ds_name, ds_value| - begin - @datasources[ds_name] = AbstractDatasource.build_instance(ds_value) - rescue DatasourceTypeNotSupportedError => e - # an unsupported datasource type has been configured in the dashboard - # - no worries here - @logger.warn(e.message) - end + @datasources[ds_name] = AbstractDatasource.build_instance(ds_value) end @datasources['default'] = @datasources[json['defaultDatasource']] end diff --git a/lib/grafana/graphite_datasource.rb b/lib/grafana/graphite_datasource.rb index bd193ae..70e60ea 100644 --- a/lib/grafana/graphite_datasource.rb +++ b/lib/grafana/graphite_datasource.rb @@ -3,6 +3,12 @@ module Grafana # Implements the interface to graphite datasources. class GraphiteDatasource < AbstractDatasource + # @see AbstractDatasource#handles? + def self.handles?(model) + tmp = new(model) + tmp.type == 'graphite' + end + # +:raw_query+ needs to contain a Graphite query as String # @see AbstractDatasource#request def request(query_description) diff --git a/lib/grafana/influxdb_datasource.1disabled_rb b/lib/grafana/influxdb_datasource.1disabled_rb deleted file mode 100644 index d9e3da2..0000000 --- a/lib/grafana/influxdb_datasource.1disabled_rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Grafana - class InfluxDbDatasource < AbstractDatasource - - def initialize(ds_model) - @model = ds_model - end - - def model - @model - end - - def url(query) - raise NotImplementedError - "/api/datasources/proxy/#{model.id}/query" - # db - # q includes times somehow... - end - - def request(query) - { - request: Net::HTTP::Get - } - end - - def raw_query(target) - target['query'] - end - end -end diff --git a/lib/grafana/prometheus_datasource.rb b/lib/grafana/prometheus_datasource.rb index f2020a2..bdddd56 100644 --- a/lib/grafana/prometheus_datasource.rb +++ b/lib/grafana/prometheus_datasource.rb @@ -3,6 +3,12 @@ module Grafana # Implements the interface to Prometheus datasources. class PrometheusDatasource < AbstractDatasource + # @see AbstractDatasource#handles? + def self.handles?(model) + tmp = new(model) + tmp.type == 'prometheus' + end + # +:raw_query+ needs to contain a Prometheus query as String # @see AbstractDatasource#request def request(query_description) diff --git a/lib/grafana/sql_datasource.rb b/lib/grafana/sql_datasource.rb index 097cec7..582ffc8 100644 --- a/lib/grafana/sql_datasource.rb +++ b/lib/grafana/sql_datasource.rb @@ -3,6 +3,12 @@ module Grafana # Implements the interface to all SQL based datasources (tested with PostgreSQL and MariaDB/MySQL). class SqlDatasource < AbstractDatasource + # @see AbstractDatasource#handles? + def self.handles?(model) + tmp = new(model) + tmp.category == 'sql' + end + # +:raw_query+ needs to contain a SQL query as String in the respective database dialect # @see AbstractDatasource#request def request(query_description) diff --git a/lib/grafana/unsupported_datasource.rb b/lib/grafana/unsupported_datasource.rb new file mode 100644 index 0000000..44888b9 --- /dev/null +++ b/lib/grafana/unsupported_datasource.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Grafana + # Dummy class, which is used, if a datasource is currently unsupported. + class UnsupportedDatasource < AbstractDatasource + end +end diff --git a/lib/grafana/webrequest.rb b/lib/grafana/webrequest.rb index 9787df5..1322344 100644 --- a/lib/grafana/webrequest.rb +++ b/lib/grafana/webrequest.rb @@ -59,7 +59,7 @@ def execute(timeout = nil) def configure_ssl @http.use_ssl = true @http.verify_mode = OpenSSL::SSL::VERIFY_PEER - if self.class.ssl_cert && !File.exist?(self.class.ssl_cert) + if self.class.ssl_cert && !File.file?(self.class.ssl_cert) @logger.warn('SSL certificate file does not exist.') elsif self.class.ssl_cert @http.cert_store = OpenSSL::X509::Store.new diff --git a/lib/grafana_reporter/abstract_query.rb b/lib/grafana_reporter/abstract_query.rb index ece0eec..9f9c13a 100644 --- a/lib/grafana_reporter/abstract_query.rb +++ b/lib/grafana_reporter/abstract_query.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module GrafanaReporter - # @abstract Override {#pre_process}, {#post_process} and {#self.build_demo_entry} in subclass. + # @abstract Override {#pre_process} and {#post_process} in subclass. # # Superclass containing everything for all queries towards grafana. class AbstractQuery @@ -9,7 +9,7 @@ class AbstractQuery attr_writer :raw_query attr_reader :variables, :result, :panel - # @param grafana_or_panel [Object] {Grafana} or {Panel} object for which the query is executed + # @param grafana_or_panel [Object] {Grafana::Grafana} or {Grafana::Panel} object for which the query is executed def initialize(grafana_or_panel) if grafana_or_panel.is_a?(Grafana::Panel) @panel = grafana_or_panel @@ -24,7 +24,7 @@ def initialize(grafana_or_panel) # # Runs the whole process to receive values properly from this query: # - calls {#pre_process} - # - executes this query against the {Grafana} instance + # - executes this query against the {Grafana::AbstractDatasource} implementation instance # - calls {#post_process} # # @return [Hash] result of the query in standardized format @@ -32,25 +32,14 @@ def execute return @result unless @result.nil? pre_process + raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource) + @result = @datasource.request(from: @from, to: @to, raw_query: raw_query, variables: grafana_variables, prepared_request: @grafana.prepare_request, timeout: timeout) post_process @result end - # Sets default configurations from the given {Dashboard} and store them as settings in the query. - # - # Following data is extracted: - # - +from+, by {Dashboard#from_time} - # - +to+, by {Dashboard#to_time} - # - and all variables as {Variable}, prefixed with +var-+, as grafana also does it - # @param dashboard [Dashboard] dashboard from which the defaults are captured - def set_defaults_from_dashboard(dashboard) - @from = dashboard.from_time - @to = dashboard.to_time - dashboard.variables.each { |item| merge_variables({ "var-#{item.name}": item }) } - end - # Overwrite this function to extract a proper raw query value from this object. # # If the property +@raw_query+ is not set manually by the calling object, this @@ -77,35 +66,22 @@ def post_process raise NotImplementedError end - # Merges the given hashes to the current object by using the {#merge_variables} method. - # It respects the priorities of the hashes and the object and allows only valid variables to be passed. - # @param document_hash [Hash] variables from report template level - # @param item_hash [Hash] variables from item configuration level, i.e. specific call, which may override document - # @return [void] - # TODO: rename method - def merge_hash_variables(document_hash, item_hash) - sel_doc_items = document_hash.select do |k, _v| - k =~ /^var-/ || k == 'grafana-report-timestamp' || k =~ /grafana_default_(?:from|to)_timezone/ - end - merge_variables(sel_doc_items.each_with_object({}) { |(k, v), h| h[k] = ::Grafana::Variable.new(v) }) - - sel_items = item_hash.select do |k, _v| - # TODO: specify accepted options in each class or check if simply all can be allowed with prefix +var-+ - k =~ /^var-/ || k =~ /^render-/ || k =~ /filter_columns|format|replace_values_.*|transpose|column_divider| - row_divider|from_timezone|to_timezone|result_type|query/x - end - merge_variables(sel_items.each_with_object({}) { |(k, v), h| h[k] = ::Grafana::Variable.new(v) }) + # Used to specify variables to be used for this query. This method ensures, that only the values of the + # {Grafana::Variable} stored in the +variables+ Array are overwritten. + # @param name [String] name of the variable to set + # @param variable [Grafana::Variable] variable from which the {Grafana::Variable#raw_value} will be assigned to the query variables + def assign_variable(name, variable) + raise GrafanaReporterError, "Provided variable is not of type Grafana::Variable (name: '#{name}', value: '#{value}')" unless variable.is_a?(Grafana::Variable) - @timeout = item_hash['timeout'] || document_hash['grafana-default-timeout'] || @timeout - @from = item_hash['from'] || document_hash['from'] || @from - @to = item_hash['to'] || document_hash['to'] || @to + @variables[name] ||= variable + @variables[name].raw_value = variable.raw_value end # Transposes the given result. # # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored. # - # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response}) + # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request}) # @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed # @return [Hash] transposed query result def transpose(result, transpose_variable) @@ -121,7 +97,7 @@ def transpose(result, transpose_variable) # # Multiple columns may be filtered. Therefore the column titles have to be named in the # {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). - # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response}) + # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request}) # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result # @return [Hash] filtered query result def filter_columns(result, filter_columns_variable) @@ -145,10 +121,9 @@ def filter_columns(result, filter_columns_variable) # The formatting will be applied separately for every column. Therefore the column formats have to be named # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for # a column, no change will happen. - # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response}) + # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request}) # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result # @return [Hash] formatted query result - # TODO: make sure that caught errors are also visible in logger def format_columns(result, formats) return result unless formats @@ -162,6 +137,7 @@ def format_columns(result, formats) begin row[i] = format % row[i] if row[i] rescue StandardError => e + @grafana.logger.error(e.message) row[i] = e.message end end @@ -195,7 +171,7 @@ def format_columns(result, formats) # '42 is the answer'. Important to know: the regular expressions always have to start # with +^+ and end with +$+, i.e. the expression itself always has to match # the whole content in one field. - # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#preformat_response}. + # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}. # @param configs [Array] one variable for replacing values in one column # @return [Hash] query result with replaced values # TODO: make sure that caught errors are also visible in logger @@ -280,7 +256,8 @@ def replace_values(result, configs) # @param timezone [Grafana::Variable] timezone to use, if not system timezone # @return [String] translated date as timestamp string def translate_date(orig_date, report_time, is_to_time, timezone = nil) - report_time ||= Variable.new(Time.now.to_s) + # TODO: add test case for creation of variable, if not given, maybe also print a warning + report_time ||= ::Grafana::Variable.new(Time.now.to_s) return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ return orig_date if orig_date =~ /^\d+$/ @@ -378,24 +355,5 @@ def fit_date(date, fit_letter, is_to_time) date end - - # Merges the given Hash with the stored variables. - # - # Can be used to easily set many values at once in the local variables hash. - # - # Please note, that the values of the Hash need to be of type {Variable}. - # - # @param hash [Hash] Hash containing variable name as key and {Variable} as value - # @return [AbstractQuery] this object - # TODO: test if this method can be removed, or make it private at least - def merge_variables(hash) - hash.each do |k, v| - if @variables[k.to_s].nil? - @variables[k.to_s] = v - else - @variables[k.to_s].raw_value = v.raw_value - end - end - end end end diff --git a/lib/grafana_reporter/abstract_report.rb b/lib/grafana_reporter/abstract_report.rb index 118f9df..7a069a7 100644 --- a/lib/grafana_reporter/abstract_report.rb +++ b/lib/grafana_reporter/abstract_report.rb @@ -35,21 +35,13 @@ class AbstractReport attr_reader :done # @param config [Configuration] configuration object - # @param template [String] path to the template to be used - # @param destination_file_or_path [String or File] path to the destination report or file object to use - # @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration - def initialize(config, template, destination_file_or_path = nil, custom_attributes = {}) + def initialize(config) @config = config @logger = Logger::TwoWayDelegateLogger.new @logger.additional_logger = @config.logger - @done = false - @template = template - @destination_file_or_path = destination_file_or_path - @custom_attributes = custom_attributes - @start_time = nil - @end_time = nil - @cancel = false - raise MissingTemplateError, @template.to_s unless File.exist?(@template.to_s) + @grafana_instances = {} + + init_before_create end # Registers a new event listener object. @@ -66,6 +58,17 @@ def self.clear_event_listeners @@event_listeners.default = [] end + # @param instance [String] requested grafana instance + # @return [Grafana::Grafana] the requested grafana instance. + def grafana(instance) + unless @grafana_instances[instance] + @grafana_instances[instance] = ::Grafana::Grafana.new(@config.grafana_host(instance), + @config.grafana_api_key(instance), + logger: @logger) + end + @grafana_instances[instance] + end + # Call to request cancelling the report generation. # @return [void] def cancel! @@ -122,25 +125,73 @@ def full_log end # Is being called to start the report generation. + # @param template [String] path to the template to be used, trailing +.adoc+ extension may be omitted + # @param destination_file_or_path [String or File] path to the destination report or file object to use + # @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration # @return [void] - def create_report + def create_report(template, destination_file_or_path = nil, custom_attributes = {}) + init_before_create + @template = template + @destination_file_or_path = destination_file_or_path + @custom_attributes = custom_attributes + + # automatically add extension, if a file with adoc extension exists + @template = "#{@template}.adoc" if File.file?("#{@template}.adoc") && !File.file?(@template.to_s) + raise MissingTemplateError, @template.to_s unless File.file?(@template.to_s) + notify(:on_before_create) @start_time = Time.new logger.info("Report started at #{@start_time}") end - # @abstract + # Used to calculate the progress of a report. By default expects +@total_steps+ to contain the total + # number of steps, which will be processed with each call of {#next_step}. # @return [Integer] number between 0 and 100, representing the current progress of the report creation. def progress + return @current_pos.to_i if @total_steps.to_i.zero? + + @current_pos.to_f / @total_steps + end + + # Increments the progress. + # @return [Integer] number of the current progress position. + def next_step + @current_pos += 1 + @current_pos + end + + # @abstract + # Provided class objects need to implement a method +build_demo_entry(panel)+. + # @return [Array] array of class objects, which shall be included in a demo report + def self.demo_report_classes raise NotImplementedError end private + # Called, if the report generation has died with an error. + # @param error [StandardError] occured error + # @return [void] + def died_with_error(error) + @error = [error.message] << [error.backtrace] + done! + end + + def init_before_create + @done = false + @start_time = nil + @end_time = nil + @cancel = false + @current_pos = 0 + end + def done! + return if @done + @done = true @end_time = Time.new - logger.info("Report creation ended after #{@end_time - @start_time} seconds with status '#{status}'") + @start_time ||= @end_time + logger.info("Report creation ended after #{@end_time.to_i - @start_time.to_i} seconds with status '#{status}'") notify(:on_after_finish) end diff --git a/lib/grafana_reporter/alerts_table_query.rb b/lib/grafana_reporter/alerts_table_query.rb index fb53471..122b139 100644 --- a/lib/grafana_reporter/alerts_table_query.rb +++ b/lib/grafana_reporter/alerts_table_query.rb @@ -19,18 +19,18 @@ class AlertsTableQuery < AbstractQuery def pre_process raise MissingMandatoryAttributeError, 'columns' unless @raw_query['columns'] - @from = translate_date(@from, @variables['grafana-report-timestamp'], false, @variables['from_timezone'] || + @from = translate_date(@from, @variables['grafana_report_timestamp'], false, @variables['from_timezone'] || @variables['grafana_default_from_timezone']) - @to = translate_date(@to, @variables['grafana-report-timestamp'], true, @variables['to_timezone'] || + @to = translate_date(@to, @variables['grafana_report_timestamp'], true, @variables['to_timezone'] || @variables['grafana_default_to_timezone']) @datasource = Grafana::GrafanaAlertsDatasource.new(nil) end # Filter the query result for the given columns and sets the result in the preformatted SQL # result stlye. - - # Additionally it applies {QueryMixin#format_columns}, {QueryMixin#replace_values} and - # {QueryMixin#filter_columns}. + # + # Additionally it applies {AbstractQuery#format_columns}, {AbstractQuery#replace_values} and + # {AbstractQuery#filter_columns}. # @return [void] def post_process @result = format_columns(@result, @variables['format']) diff --git a/lib/grafana_reporter/annotations_table_query.rb b/lib/grafana_reporter/annotations_table_query.rb index 4485ffd..04993b6 100644 --- a/lib/grafana_reporter/annotations_table_query.rb +++ b/lib/grafana_reporter/annotations_table_query.rb @@ -18,9 +18,9 @@ class AnnotationsTableQuery < AbstractQuery def pre_process raise MissingMandatoryAttributeError, 'columns' unless @raw_query['columns'] - @from = translate_date(@from, @variables['grafana-report-timestamp'], false, @variables['from_timezone'] || + @from = translate_date(@from, @variables['grafana_report_timestamp'], false, @variables['from_timezone'] || @variables['grafana_default_from_timezone']) - @to = translate_date(@to, @variables['grafana-report-timestamp'], true, @variables['to_timezone'] || + @to = translate_date(@to, @variables['grafana_report_timestamp'], true, @variables['to_timezone'] || @variables['grafana_default_to_timezone']) @datasource = Grafana::GrafanaAnnotationsDatasource.new(nil) end @@ -28,8 +28,8 @@ def pre_process # Filters the query result for the given columns and sets the result # in the preformatted SQL result style. # - # Additionally it applies {QueryMixin#format_columns}, {QueryMixin#replace_values} and - # {QueryMixin#filter_columns}. + # Additionally it applies {AbstractQuery#format_columns}, {AbstractQuery#replace_values} and + # {AbstractQuery#filter_columns}. # @return [void] def post_process @result = format_columns(@result, @variables['format']) diff --git a/lib/grafana_reporter/application/application.rb b/lib/grafana_reporter/application/application.rb index 11725dd..199de0b 100644 --- a/lib/grafana_reporter/application/application.rb +++ b/lib/grafana_reporter/application/application.rb @@ -13,8 +13,6 @@ module Application # It can be run to test the grafana connection, render a single template # or run as a service. class Application - # Default file name for grafana reporter configuration file - CONFIG_FILE = 'grafana_reporter.config' # Contains the {Configuration} object of the application. attr_accessor :config @@ -32,7 +30,7 @@ def initialize # @param params [Array] command line parameters, mainly ARGV can be used. # @return [Integer] 0 if everything is fine, -1 if execution aborted. def configure_and_run(params = []) - config_file = CONFIG_FILE + config_file = Configuration::DEFAULT_CONFIG_FILE_NAME tmp_config = Configuration.new action_wizard = false @@ -44,10 +42,14 @@ def configure_and_run(params = []) end opts.on('-c', '--config CONFIG_FILE_NAME', 'Specify custom configuration file,'\ - " instead of #{CONFIG_FILE}.") do |file_name| + " instead of #{Configuration::DEFAULT_CONFIG_FILE_NAME}.") do |file_name| config_file = file_name end + opts.on('-r', '--register FILE', 'Register a custom plugin, e.g. your own Datasource implementation') do |plugin| + require plugin + end + opts.on('-d', '--debug LEVEL', 'Specify detail level: FATAL, ERROR, WARN, INFO, DEBUG.') do |level| tmp_config.set_param('grafana-reporter:debug-level', level) end @@ -65,7 +67,7 @@ def configure_and_run(params = []) opts.on('--ssl-cert FILE', 'Manually specify a SSL cert file for HTTPS connection to grafana. Only '\ 'needed if not working properly otherwise.') do |file| - if File.exist?(file) + if File.file?(file) tmp_config.set_param('grafana-reporter:ssl-cert', file) else config.logger.warn("SSL certificate file #{file} does not exist. Setting will be ignored.") @@ -106,22 +108,14 @@ def configure_and_run(params = []) end # abort if config file does not exist - unless File.exist?(config_file) + unless File.file?(config_file) puts "Config file '#{config_file}' does not exist. Consider calling the configuration wizard"\ ' with option \'-w\' or use \'-h\' to see help message. Aborting.' return -1 end - # read config file - config_hash = nil - begin - config_hash = YAML.load_file(config_file) - rescue StandardError => e - raise ConfigurationError, "Could not read config file '#{config_file}' (Error: #{e.message})" - end - # merge command line configuration with read config file - @config.config = config_hash + @config.load_config_from_file(config_file) @config.merge!(tmp_config) run @@ -146,7 +140,7 @@ def run when Configuration::MODE_SINGLE_RENDER begin - config.report_class.new(config, config.template, config.to_file).create_report + config.report_class.new(config).create_report(config.template, config.to_file) rescue StandardError => e puts "#{e.message}\n#{e.backtrace.join("\n")}" end diff --git a/lib/grafana_reporter/application/webservice.rb b/lib/grafana_reporter/application/webservice.rb index 186c492..65655a9 100644 --- a/lib/grafana_reporter/application/webservice.rb +++ b/lib/grafana_reporter/application/webservice.rb @@ -6,9 +6,12 @@ module Application # make use of `webrick` or similar, so that it can be used without futher dependencies # in conjunction with the standard asciidoctor docker container. class Webservice + # Array of possible webservice running states + STATUS = %I[stopped running stopping].freeze + def initialize @reports = [] - @running = false + @status = :stopped end # Runs the webservice with the given {Configuration} object. @@ -19,17 +22,32 @@ def run(config) # start webserver @server = TCPServer.new(@config.webserver_port) @logger.info("Server listening on port #{@config.webserver_port}...") - @running = true @progress_reporter = Thread.new {} + @status = :running accept_requests_loop - @running = false + @status = :stopped + end + + # @return True, if webservice is stopped, false otherwise + def stopped? + @status == :stopped end # @return True, if webservice is up and running, false otherwise def running? - @running + @status == :running + end + + # Forces stopping the webservice. + def stop! + @status = :stopping + + # invoke a new request, so that the webservice stops. + socket = TCPSocket.new('localhost', @config.webserver_port) + socket.send '', 0 + socket.close end private @@ -39,6 +57,14 @@ def accept_requests_loop # step 1) accept incoming connection socket = @server.accept + # TODO: shutdown properly on SIGINT/SIGHUB + + # stop webservice properly, if shall be shutdown + if @status == :stopping + socket.close + break + end + # step 2) print the request headers (separated by a blank line e.g. \r\n) request = '' line = '' @@ -57,9 +83,6 @@ def accept_requests_loop rescue WebserviceUnknownPathError => e @logger.debug(e.message) socket.write http_response(404, '', e.message) - rescue MissingTemplateError => e - @logger.error(e.message) - socket.write http_response(400, 'Bad Request', e.message) rescue WebserviceGeneralRenderingError => e @logger.fatal(e.message) socket.write http_response(400, 'Bad Request', e.message) @@ -179,7 +202,7 @@ def view_report(attrs) def render_report(attrs) # build report - template_file = "#{@config.templates_folder}#{attrs['var-template']}.adoc" + template_file = "#{@config.templates_folder}#{attrs['var-template']}" file = Tempfile.new('gf_pdf_', @config.reports_folder) begin @@ -188,9 +211,10 @@ def render_report(attrs) @logger.debug("File permissions could not be set for #{file.path}: #{e.message}") end - report = @config.report_class.new(@config, template_file, file, attrs) + report = @config.report_class.new(@config) + Thread.report_on_exception = false Thread.new do - report.create_report + report.create_report(template_file, file, attrs) end @reports << report diff --git a/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb b/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb index 4811394..afac36c 100644 --- a/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +++ b/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb @@ -31,11 +31,11 @@ module Asciidoctor # # +to+ - 'to' time for the sql query # - # +format+ - see {QueryMixin#format_columns} + # +format+ - see {AbstractQuery#format_columns} # - # +replace_values+ - see {QueryMixin#replace_values} + # +replace_values+ - see {AbstractQuery#replace_values} # - # +filter_columns+ - see {QueryMixin#filter_columns} + # +filter_columns+ - see {AbstractQuery#filter_columns} class AlertsTableIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor include ProcessorMixin @@ -56,12 +56,12 @@ def process(doc, reader, _target, attrs) " dashboard: #{dashboard_id}, panel: #{panel_id})") query = AlertsTableQuery.new(@report.grafana(instance)) - query.set_defaults_from_dashboard(@report.grafana(instance).dashboard(dashboard_id)) if dashboard_id + assign_dashboard_defaults(query, @report.grafana(instance).dashboard(dashboard_id)) if dashboard_id defaults = {} defaults['dashboardId'] = dashboard_id if dashboard_id defaults['panelId'] = panel_id if panel_id - query.merge_hash_variables(doc.attributes, attrs) + assign_doc_and_item_variables(query, doc.attributes, attrs) selected_attrs = attrs.select do |k, _v| k =~ /(?:columns|limit|folderId|dashboardId|panelId|dahboardTag|dashboardQuery|state|query)/x end diff --git a/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb b/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb index b97675b..2d1babc 100644 --- a/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +++ b/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb @@ -31,11 +31,11 @@ module Asciidoctor # # +to+ - 'to' time for the sql query # - # +format+ - see {QueryMixin#format_columns} + # +format+ - see {AbstractQuery#format_columns} # - # +replace_values+ - see {QueryMixin#replace_values} + # +replace_values+ - see {AbstractQuery#replace_values} # - # +filter_columns+ - see {QueryMixin#filter_columns} + # +filter_columns+ - see {AbstractQuery#filter_columns} class AnnotationsTableIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor include ProcessorMixin @@ -55,12 +55,12 @@ def process(doc, reader, _target, attrs) @report.logger.debug("Processing AnnotationsTableIncludeProcessor (instance: #{instance})") query = AnnotationsTableQuery.new(@report.grafana(instance)) - query.set_defaults_from_dashboard(@report.grafana(instance).dashboard(dashboard_id)) if dashboard_id + assign_dashboard_defaults(query, @report.grafana(instance).dashboard(dashboard_id)) if dashboard_id defaults = {} defaults['dashboardId'] = dashboard_id if dashboard_id defaults['panelId'] = panel_id if panel_id - query.merge_hash_variables(doc.attributes, attrs) + assign_doc_and_item_variables(query, doc.attributes, attrs) selected_attrs = attrs.select do |k, _v| k =~ /(?:columns|limit|alertId|dashboardId|panelId|userId|type|tags)/ end diff --git a/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb b/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb index 77a908c..af45a82 100644 --- a/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +++ b/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb @@ -47,7 +47,8 @@ def process(parent, target, attrs) begin query = PanelImageQuery.new(@report.grafana(instance).dashboard(dashboard).panel(target)) - query.merge_hash_variables(parent.document.attributes, attrs) + assign_dashboard_defaults(query, @report.grafana(instance).dashboard(dashboard)) + assign_doc_and_item_variables(query, parent.document.attributes, attrs) @report.logger.debug("from: #{query.from}, to: #{query.to}") image = query.execute diff --git a/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb b/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb index a45e292..cc1a59b 100644 --- a/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +++ b/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb @@ -44,13 +44,15 @@ def process(parent, target, attrs) dashboard = attrs['dashboard'] || parent.document.attr('grafana_default_dashboard') @report.logger.debug("Processing PanelImageInlineMacro (instance: #{instance}, dashboard: #{dashboard},"\ " panel: #{target})") - query = PanelImageQuery.new(@report.grafana(instance).dashboard(dashboard).panel(target)) - # set alt text to a default, because otherwise asciidoctor fails - attrs['alt'] = '' unless attrs['alt'] - query.merge_hash_variables(parent.document.attributes, attrs) - @report.logger.debug("from: #{query.from}, to: #{query.to}") begin + query = PanelImageQuery.new(@report.grafana(instance).dashboard(dashboard).panel(target)) + # set alt text to a default, because otherwise asciidoctor fails + attrs['alt'] = '' unless attrs['alt'] + assign_dashboard_defaults(query, @report.grafana(instance).dashboard(dashboard)) + assign_doc_and_item_variables(query, parent.document.attributes, attrs) + @report.logger.debug("from: #{query.from}, to: #{query.to}") + image = query.execute image_path = @report.save_image_file(image) rescue GrafanaReporterError => e diff --git a/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb b/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb index 4977797..f86c2c5 100644 --- a/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +++ b/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb @@ -40,7 +40,8 @@ def process(parent, target, attrs) begin query = PanelPropertyQuery.new(@report.grafana(instance).dashboard(dashboard).panel(target)) query.raw_query = { property_name: attrs[:field] } - query.merge_hash_variables(parent.document.attributes, attrs) + assign_dashboard_defaults(query, @report.grafana(instance).dashboard(dashboard)) + assign_doc_and_item_variables(query, parent.document.attributes, attrs) @report.logger.debug("from: #{query.from}, to: #{query.to}") description = query.execute diff --git a/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb b/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb index 39e5f17..cc4c394 100644 --- a/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +++ b/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb @@ -32,11 +32,11 @@ module Asciidoctor # # +to+ - 'to' time for the sql query # - # +format+ - see {QueryMixin#format_columns} + # +format+ - see {AbstractQuery#format_columns} # - # +replace_values+ - see {QueryMixin#replace_values} + # +replace_values+ - see {AbstractQuery#replace_values} # - # +filter_columns+ - see {QueryMixin#filter_columns} + # +filter_columns+ - see {AbstractQuery#filter_columns} class PanelQueryTableIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor include ProcessorMixin @@ -60,8 +60,8 @@ def process(doc, reader, target, attrs) begin panel = @report.grafana(instance).dashboard(dashboard).panel(panel_id) query = QueryValueQuery.new(panel) - query.set_defaults_from_dashboard(panel.dashboard) - query.merge_hash_variables(doc.attributes, attrs) + assign_dashboard_defaults(query, panel.dashboard) + assign_doc_and_item_variables(query, doc.attributes, attrs) @report.logger.debug("from: #{query.from}, to: #{query.to}") reader.unshift_lines query.execute diff --git a/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb b/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb index 3c46b18..17f1afa 100644 --- a/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +++ b/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb @@ -32,11 +32,11 @@ module Asciidoctor # # +to+ - 'to' time for the sql query # - # +format+ - see {QueryMixin#format_columns} + # +format+ - see {AbstractQuery#format_columns} # - # +replace_values+ - see {QueryMixin#replace_values} + # +replace_values+ - see {AbstractQuery#replace_values} # - # +filter_columns+ - see {QueryMixin#filter_columns} + # +filter_columns+ - see {AbstractQuery#filter_columns} class PanelQueryValueInlineMacro < ::Asciidoctor::Extensions::InlineMacroProcessor include ProcessorMixin use_dsl @@ -57,8 +57,8 @@ def process(parent, target, attrs) begin panel = @report.grafana(instance).dashboard(dashboard).panel(target) query = QueryValueQuery.new(panel) - query.set_defaults_from_dashboard(panel.dashboard) - query.merge_hash_variables(parent.document.attributes, attrs) + assign_dashboard_defaults(query, panel.dashboard) + assign_doc_and_item_variables(query, parent.document.attributes, attrs) @report.logger.debug("from: #{query.from}, to: #{query.to}") create_inline(parent, :quoted, query.execute) diff --git a/lib/grafana_reporter/asciidoctor/processor_mixin.rb b/lib/grafana_reporter/asciidoctor/processor_mixin.rb index dc94733..b7cbb5c 100644 --- a/lib/grafana_reporter/asciidoctor/processor_mixin.rb +++ b/lib/grafana_reporter/asciidoctor/processor_mixin.rb @@ -12,12 +12,52 @@ def current_report(report) self end - # This method is called if a demo report shall be built for the given {Panel}. - # @param panel [Panel] panel object, for which a demo entry shall be created. + # This method is called if a demo report shall be built for the given {Grafana::Panel}. + # @param panel [Grafana::Panel] panel object, for which a demo entry shall be created. # @return [String] String containing the entry, or nil if not possible for given panel def build_demo_entry(panel) raise NotImplementedError end + + # Sets default configurations from the given {Grafana::Dashboard} and store them as settings in the + # {AbstractQuery}. + # + # Following data is extracted: + # - +from+, by {Grafana::Dashboard#from_time} + # - +to+, by {Grafana::Dashboard#to_time} + # - and all variables as {Grafana::Variable}, prefixed with +var-+, as grafana also does it + # @param query [AbstractQuery] query object, for which the defaults are set + # @param dashboard [Grafana::Dashboard] dashboard from which the defaults are captured + def assign_dashboard_defaults(query, dashboard) + query.from = dashboard.from_time + query.to = dashboard.to_time + dashboard.variables.each { |item| query.assign_variable("var-#{item.name}", item) } + end + + # Merges the given hashes to the given query object. It respects the priorities of the hashes and the + # object and allows only valid variables to be passed. + # @param query [AbstractQuery] query object, for which the defaults are set + # @param document_hash [Hash] variables from report template level + # @param item_hash [Hash] variables from item configuration level, i.e. specific call, which may override document + # @return [void] + def assign_doc_and_item_variables(query, document_hash, item_hash) + sel_doc_items = document_hash.select do |k, _v| + k =~ /^var-/ || k =~ /grafana_default_(?:from|to)_timezone/ + end + sel_doc_items.each { |k, v| query.assign_variable(k, ::Grafana::Variable.new(v)) } + query.assign_variable('grafana_report_timestamp', ::Grafana::Variable.new(document_hash['localdatetime'])) + + sel_items = item_hash.select do |k, _v| + # TODO: specify accepted options in each class or check if simply all can be allowed with prefix +var-+ + k =~ /^var-/ || k =~ /^render-/ || k =~ /filter_columns|format|replace_values_.*|transpose|column_divider| + row_divider|from_timezone|to_timezone|result_type|query/x + end + sel_items.each { |k, v| query.assign_variable(k, ::Grafana::Variable.new(v)) } + + query.timeout = item_hash['timeout'] || document_hash['grafana-default-timeout'] || query.timeout + query.from = item_hash['from'] || document_hash['from'] || query.from + query.to = item_hash['to'] || document_hash['to'] || query.to + end end end end diff --git a/lib/grafana_reporter/asciidoctor/report.rb b/lib/grafana_reporter/asciidoctor/report.rb index 386dde1..d58be28 100644 --- a/lib/grafana_reporter/asciidoctor/report.rb +++ b/lib/grafana_reporter/asciidoctor/report.rb @@ -6,22 +6,18 @@ module Asciidoctor # Implementation of a specific {AbstractReport}. It is used to # build reports specifically for asciidoctor results. class Report < ::GrafanaReporter::AbstractReport - # (see AbstractReport#initialize) - def initialize(config, template, destination_file_or_path = nil, custom_attributes = {}) + # @see AbstractReport#initialize + def initialize(config) super - @current_pos = 0 @image_files = [] - @grafana_instances = {} end - # Starts to create an asciidoctor report. It utilizes all {Extensions} to - # realize the conversion. + # Starts to create an asciidoctor report. It utilizes all extensions in the {GrafanaReporter::Asciidoctor} + # namespace to realize the conversion. # @see AbstractReport#create_report - # @return [void] - def create_report + def create_report(template, destination_file_or_path = nil, custom_attributes = {}) super attrs = { 'convert-backend' => 'pdf' }.merge(@config.default_document_attributes.merge(@custom_attributes)) - attrs['grafana-report-timestamp'] = @start_time.to_s logger.debug("Document attributes: #{attrs}") initialize_step_counter @@ -87,37 +83,17 @@ def create_report end clean_image_files + rescue MissingTemplateError => e + @logger.error(e.message) + @error = [e.message] done! + raise e rescue StandardError => e # catch all errors during execution died_with_error(e) raise e - end - - # @see AbstractReport#progress - # @return [Float] number between 0 and 1 reflecting the current progress. - def progress - return 0 if @total_steps.to_i.zero? - - @current_pos.to_f / @total_steps - end - - # @param instance [String] requested grafana instance - # @return [Grafana::Grafana] the requested grafana instance. - def grafana(instance) - unless @grafana_instances[instance] - @grafana_instances[instance] = ::Grafana::Grafana.new(@config.grafana_host(instance), - @config.grafana_api_key(instance), - logger: @logger) - end - @grafana_instances[instance] - end - - # Increments the progress. - # @return [Integer] number of the current progress position. - def next_step - @current_pos += 1 - @current_pos + ensure + done! end # Called to save a temporary image file. After the final generation of the @@ -126,6 +102,7 @@ def next_step # @return [String] path to the temporary file. def save_image_file(img_data) file = Tempfile.new(['gf_image_', '.png'], @config.images_folder.to_s) + file.binmode file.write(img_data) path = file.path.gsub(/#{@config.images_folder}/, '') @@ -135,12 +112,11 @@ def save_image_file(img_data) path end - # Called, if the report generation has died with an error. - # @param error [StandardError] occured error - # @return [void] - def died_with_error(error) - @error = [error.message] << [error.backtrace] - done! + # @see AbstractReport#demo_report_classes + def self.demo_report_classes + [AlertsTableIncludeProcessor, AnnotationsTableIncludeProcessor, PanelImageBlockMacro, PanelImageInlineMacro, + PanelPropertyInlineMacro, PanelQueryTableIncludeProcessor, PanelQueryValueInlineMacro, + SqlTableIncludeProcessor, SqlValueInlineMacro, ShowHelpIncludeProcessor, ShowEnvironmentIncludeProcessor] end private diff --git a/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb b/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb index 642332b..571bba5 100644 --- a/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +++ b/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb @@ -26,11 +26,11 @@ module Asciidoctor # # +to+ - 'to' time for the sql query # - # +format+ - see {QueryMixin#format_columns} + # +format+ - see {AbstractQuery#format_columns} # - # +replace_values+ - see {QueryMixin#replace_values} + # +replace_values+ - see {AbstractQuery#replace_values} # - # +filter_columns+ - see {QueryMixin#filter_columns} + # +filter_columns+ - see {AbstractQuery#filter_columns} class SqlTableIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor include ProcessorMixin @@ -54,7 +54,7 @@ def process(doc, reader, target, attrs) query = QueryValueQuery.new(@report.grafana(instance)) query.datasource = @report.grafana(instance).datasource_by_id(target.split(':')[1].to_i) query.raw_query = attrs['sql'] - query.merge_hash_variables(doc.attributes, attrs) + assign_doc_and_item_variables(query, doc.attributes, attrs) @report.logger.debug("from: #{query.from}, to: #{query.to}") reader.unshift_lines query.execute diff --git a/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb b/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb index 7e672d4..660051a 100644 --- a/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +++ b/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb @@ -26,11 +26,11 @@ module Asciidoctor # # +to+ - 'to' time for the sql query # - # +format+ - see {QueryMixin#format_columns} + # +format+ - see {AbstractQuery#format_columns} # - # +replace_values+ - see {QueryMixin#replace_values} + # +replace_values+ - see {AbstractQuery#replace_values} # - # +filter_columns+ - see {QueryMixin#filter_columns} + # +filter_columns+ - see {AbstractQuery#filter_columns} class SqlValueInlineMacro < ::Asciidoctor::Extensions::InlineMacroProcessor include ProcessorMixin use_dsl @@ -52,7 +52,7 @@ def process(parent, target, attrs) query = QueryValueQuery.new(@report.grafana(instance)) query.datasource = @report.grafana(instance).datasource_by_id(target) query.raw_query = attrs['sql'] - query.merge_hash_variables(parent.document.attributes, attrs) + assign_doc_and_item_variables(query, parent.document.attributes, attrs) @report.logger.debug("from: #{query.from}, to: #{query.to}") create_inline(parent, :quoted, query.execute) diff --git a/lib/grafana_reporter/configuration.rb b/lib/grafana_reporter/configuration.rb index c4c71ef..2925914 100644 --- a/lib/grafana_reporter/configuration.rb +++ b/lib/grafana_reporter/configuration.rb @@ -13,6 +13,10 @@ module GrafanaReporter class Configuration # @return [AbstractReport] specific report class, which should be used. attr_accessor :report_class + attr_accessor :logger + + # Default file name for grafana reporter configuration file + DEFAULT_CONFIG_FILE_NAME = 'grafana_reporter.config' # Returned by {#mode} if only a connection test shall be executed. MODE_CONNECTION_TEST = 'test' @@ -30,7 +34,15 @@ def initialize @logger = ::Logger.new($stderr, level: :info) end - attr_accessor :logger + # Reads a given configuration file. + # @param config_file [String] path to configuration file, defaults to DEFAULT_CONFIG_FILE_NAME + # @return [Hash] configuration hash to be set as {Configuration#config} + def load_config_from_file(config_file = nil) + config_file ||= DEFAULT_CONFIG_FILE_NAME + self.config = YAML.load_file(config_file) + rescue StandardError => e + raise ConfigurationError, "Could not read config file '#{config_file}' (Error: #{e.message})" + end # Used to overwrite the current configuration. def config=(new_config) @@ -53,7 +65,7 @@ def mode def template return nil if get_config('default-document-attributes:var-template').nil? - "#{templates_folder}#{get_config('default-document-attributes:var-template')}.adoc" + "#{templates_folder}#{get_config('default-document-attributes:var-template')}" end # @return [String] destination filename for the report in {MODE_SINGLE_RENDER}. diff --git a/lib/grafana_reporter/console_configuration_wizard.rb b/lib/grafana_reporter/console_configuration_wizard.rb index cb4dee9..be93a90 100644 --- a/lib/grafana_reporter/console_configuration_wizard.rb +++ b/lib/grafana_reporter/console_configuration_wizard.rb @@ -31,7 +31,7 @@ def start_wizard(config_file, console_config) demo_report = create_demo_report(config) demo_report ||= '<>' - config_param = config_file == Application::Application::CONFIG_FILE ? '' : " -c #{config_file}" + config_param = config_file == Configuration::DEFAULT_CONFIG_FILE_NAME ? '' : " -c #{config_file}" program_call = "#{Gem.ruby} #{$PROGRAM_NAME}" program_call = ENV['OCRA_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '') if ENV['OCRA_EXECUTABLE'] @@ -137,16 +137,8 @@ def create_demo_report(config) end end - # TODO: move this to Asciidoctor::Report class - classes = [Asciidoctor::AlertsTableIncludeProcessor, Asciidoctor::AnnotationsTableIncludeProcessor, - Asciidoctor::PanelImageBlockMacro, Asciidoctor::PanelImageInlineMacro, - Asciidoctor::PanelPropertyInlineMacro, Asciidoctor::PanelQueryTableIncludeProcessor, - Asciidoctor::PanelQueryValueInlineMacro, Asciidoctor::SqlTableIncludeProcessor, - Asciidoctor::SqlValueInlineMacro, Asciidoctor::ShowHelpIncludeProcessor, - Asciidoctor::ShowEnvironmentIncludeProcessor] - grafana = ::Grafana::Grafana.new(config.grafana_host, config.grafana_api_key) - demo_report_content = DemoReportWizard.new(classes).build(grafana) + demo_report_content = DemoReportWizard.new(config.report_class.demo_report_classes).build(grafana) begin File.write(demo_report_file, demo_report_content, mode: 'w') diff --git a/lib/grafana_reporter/demo_report_wizard.rb b/lib/grafana_reporter/demo_report_wizard.rb index f520120..b5ccd14 100644 --- a/lib/grafana_reporter/demo_report_wizard.rb +++ b/lib/grafana_reporter/demo_report_wizard.rb @@ -48,6 +48,8 @@ def evaluate_dashboard(dashboard, query_classes) results = {} dashboard.panels.shuffle.each do |panel| + next if panel.datasource.is_a?(Grafana::UnsupportedDatasource) + query_classes.each do |query_class| unless query_class.public_instance_methods.include?(:build_demo_entry) results[query_class] = "Method 'build_demo_entry' not implemented for #{query_class.name}" @@ -57,6 +59,11 @@ def evaluate_dashboard(dashboard, query_classes) begin result = query_class.new.build_demo_entry(panel) results[query_class] = result if result + rescue Grafana::DatasourceDoesNotExistError + # properly catch DatasourceDoesNotExist errors here, as they don't lead to a real issue + # during demo report creation + # This may e.g. happen if a panel asks e.g. for datasource '-- Dashboard --' which is + # currently not allowed rescue StandardError => e puts "#{e.message}\n#{e.backtrace.join("\n")}" end diff --git a/lib/grafana_reporter/erb/report.rb b/lib/grafana_reporter/erb/report.rb new file mode 100644 index 0000000..faf1d11 --- /dev/null +++ b/lib/grafana_reporter/erb/report.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'erb' + +module GrafanaReporter + module ERB + # Implementation of a specific {AbstractReport}. It is used to + # build reports specifically for erb templates. + class Report < ::GrafanaReporter::AbstractReport + # Starts to create an asciidoctor report. It utilizes all extensions in the {GrafanaReporter::Asciidoctor} + # namespace to realize the conversion. + # @see AbstractReport#create_report + def create_report(template, destination_file_or_path = nil, custom_attributes = {}) + super + attrs = @config.default_document_attributes.merge(@custom_attributes) + logger.debug("Document attributes: #{attrs}") + + # TODO: if path is true, a default filename has to be generated. check if this should be a general function instead + @report = self + File.write(path, ::ERB.new(File.read(template)).result(binding)) + + # TODO: check if closing output file is correct here, or maybe can be moved to AbstractReport.done! + @destination_file_or_path.close if @destination_file_or_path.is_a?(File) + rescue MissingTemplateError => e + @logger.error(e.message) + @error = [e.message] + done! + raise e + rescue StandardError => e + # catch all errors during execution + died_with_error(e) + raise e + ensure + done! + end + + # @see AbstractReport#demo_report_classes + def self.demo_report_classes + [] + end + end + end +end diff --git a/lib/grafana_reporter/errors.rb b/lib/grafana_reporter/errors.rb index 6b5ff32..ae0262f 100644 --- a/lib/grafana_reporter/errors.rb +++ b/lib/grafana_reporter/errors.rb @@ -8,6 +8,14 @@ def initialize(message) end end + # Raised if a datasource shall be queried, which is not (yet) supported by the reporter + class DatasourceNotSupportedError < GrafanaReporterError + def initialize(ds, query) + super("The datasource '#{ds.name}' is of type '#{ds.type}' which is currently not supported for "\ + "the query type '#{query}'.") + end + end + # Thrown, if the requested grafana instance does not have the mandatory 'host' # setting configured. class GrafanaInstanceWithoutHostError < GrafanaReporterError @@ -46,7 +54,7 @@ def initialize(item, verb, expected, currently) end end - # Thrown, if the value configuration in {QueryMixin#replace_values} is + # Thrown, if the value configuration in {AbstractQuery#replace_values} is # invalid. class MalformedReplaceValuesStatementError < GrafanaReporterError def initialize(statement) diff --git a/lib/grafana_reporter/help.rb b/lib/grafana_reporter/help.rb index 9f9ed33..9e6aaa0 100644 --- a/lib/grafana_reporter/help.rb +++ b/lib/grafana_reporter/help.rb @@ -18,11 +18,6 @@ def github(headline_level = 2) "#{toc}\n\n#{help_text(github_options.merge(level: headline_level))}" end - # @see AbstractQuery#self.build_demo_entry - def self.build_demo_entry(_panel) - 'include::grafana_help[]' - end - private def github_options diff --git a/lib/grafana_reporter/logger/two_way_logger.rb b/lib/grafana_reporter/logger/two_way_delegate_logger.rb similarity index 100% rename from lib/grafana_reporter/logger/two_way_logger.rb rename to lib/grafana_reporter/logger/two_way_delegate_logger.rb diff --git a/lib/grafana_reporter/panel_image_query.rb b/lib/grafana_reporter/panel_image_query.rb index d2e8c87..5cc29a8 100644 --- a/lib/grafana_reporter/panel_image_query.rb +++ b/lib/grafana_reporter/panel_image_query.rb @@ -5,9 +5,9 @@ module GrafanaReporter class PanelImageQuery < AbstractQuery # Sets the proper render variables. def pre_process - @from = translate_date(@from, @variables['grafana-report-timestamp'], false, @variables['from_timezone'] || + @from = translate_date(@from, @variables['grafana_report_timestamp'], false, @variables['from_timezone'] || @variables['grafana_default_from_timezone']) - @to = translate_date(@to, @variables['grafana-report-timestamp'], true, @variables['to_timezone'] || + @to = translate_date(@to, @variables['grafana_report_timestamp'], true, @variables['to_timezone'] || @variables['grafana_default_to_timezone']) # TODO: ensure that in case of timezones are specified, that they are also forwarded to the image renderer # rename "render-" variables diff --git a/lib/grafana_reporter/query_value_query.rb b/lib/grafana_reporter/query_value_query.rb index 7e5050e..84d4ce9 100644 --- a/lib/grafana_reporter/query_value_query.rb +++ b/lib/grafana_reporter/query_value_query.rb @@ -8,15 +8,15 @@ class QueryValueQuery < AbstractQuery def pre_process @datasource = @panel.datasource if @panel - @from = translate_date(@from, @variables['grafana-report-timestamp'], false, @variables['from_timezone'] || + @from = translate_date(@from, @variables['grafana_report_timestamp'], false, @variables['from_timezone'] || @variables['grafana_default_from_timezone']) - @to = translate_date(@to, @variables['grafana-report-timestamp'], true, @variables['to_timezone'] || + @to = translate_date(@to, @variables['grafana_report_timestamp'], true, @variables['to_timezone'] || @variables['grafana_default_to_timezone']) @variables['result_type'] ||= Variable.new('') end - # Executes {QueryMixin#format_columns}, {QueryMixin#replace_values} and - # {QueryMixin#filter_columns} on the query results. + # Executes {AbstractQuery#format_columns}, {AbstractQuery#replace_values} and + # {AbstractQuery#filter_columns} on the query results. # # Finally the results are formatted as a asciidoctor table. # @see Grafana::AbstractQuery#post_process diff --git a/lib/ruby_grafana_reporter.rb b/lib/ruby_grafana_reporter.rb index 65c6468..2ec2ecc 100644 --- a/lib/ruby_grafana_reporter.rb +++ b/lib/ruby_grafana_reporter.rb @@ -24,6 +24,7 @@ %w[grafana_reporter], %w[grafana_reporter asciidoctor extensions], %w[grafana_reporter asciidoctor], + %w[grafana_reporter erb], %w[grafana_reporter application] ] folders.each { |folder| Dir[File.join(__dir__, *folder, '*.rb')].sort.each { |file| require_relative file } } diff --git a/ruby-grafana-reporter.gemspec b/ruby-grafana-reporter.gemspec index 6b98915..5cd732f 100644 --- a/ruby-grafana-reporter.gemspec +++ b/ruby-grafana-reporter.gemspec @@ -7,6 +7,7 @@ folders = [ %w[grafana_reporter], %w[grafana_reporter asciidoctor extensions], %w[grafana_reporter asciidoctor], + %w[grafana_reporter erb], %w[grafana_reporter application] ] @@ -34,16 +35,16 @@ Gem::Specification.new do |s| } # the required ruby version is determined from the base docker image, currently debian stretch - s.required_ruby_version = '>=2.3.3' + s.required_ruby_version = '>=2.5' s.extra_rdoc_files = ['README.md', 'LICENSE'] s.bindir = 'bin' s.add_runtime_dependency 'asciidoctor', '~>2.0' - s.add_runtime_dependency 'asciidoctor-pdf', '~>1.5' + s.add_runtime_dependency 'asciidoctor-pdf', '~>1.6' # the following package includes an interface to zip, which is also needed here # make sure that supported zip versions match - look in sub-dependency 'gepub' - # s.add_runtime_dependency 'asciidoctor-epub3', '~>1.5.0' + # s.add_runtime_dependency 'asciidoctor-epub3', '~>1.5.1' s.add_runtime_dependency 'rubyzip', '>1.1.1', '<2.4' s.add_development_dependency 'rspec', '~>3.9' diff --git a/spec/integration/alerts_table_include_processor_spec.rb b/spec/integration/alerts_table_include_processor_spec.rb index 22a0697..d250a73 100644 --- a/spec/integration/alerts_table_include_processor_spec.rb +++ b/spec/integration/alerts_table_include_processor_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do include_processor AlertsTableIncludeProcessor.new.current_report(report) diff --git a/spec/integration/annotations_table_include_processor_spec.rb b/spec/integration/annotations_table_include_processor_spec.rb index edc2467..9587af4 100644 --- a/spec/integration/annotations_table_include_processor_spec.rb +++ b/spec/integration/annotations_table_include_processor_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do include_processor AnnotationsTableIncludeProcessor.new.current_report(report) diff --git a/spec/integration/application_spec.rb b/spec/integration/application_spec.rb index a4742d9..cdd0038 100644 --- a/spec/integration/application_spec.rb +++ b/spec/integration/application_spec.rb @@ -81,6 +81,12 @@ def done? File.delete('./result.pdf') if File.exist?('./result.pdf') end + it 'can single render a template with extension' do + expect(subject.config.logger).not_to receive(:error) + expect { subject.configure_and_run(['-c', './spec/tests/demo_config.txt', '-t', 'spec/tests/demo_report.adoc', '-o', './result.pdf', '-d', 'ERROR']) }.not_to output(/ERROR/).to_stderr + expect(File.exist?('./result.pdf')).to be true + end + it 'can single render a template and output to custom folder' do expect(subject.config.logger).not_to receive(:error) expect { subject.configure_and_run(['-c', './spec/tests/demo_config.txt', '-t', 'spec/tests/demo_report', '-o', './result.pdf', '-d', 'ERROR']) }.not_to output(/ERROR/).to_stderr @@ -98,10 +104,57 @@ def done? end it 'does not raise error on non existing template' do + expect(subject.config.logger).to receive(:error).with(/is not a valid template/) expect { subject.configure_and_run(['-c', './spec/tests/demo_config.txt', '-t', 'does_not_exist']) }.to output(/report template .* is not a valid template/).to_stdout end end + context 'custom plugins' do + subject { GrafanaReporter::Application::Application.new } + + before do + File.delete('./result.pdf') if File.exist?('./result.pdf') + end + + after do + # remove temporary added plugin from respective places, so that other test cases run + # as if that would have never happened + expect(Object.constants.include?(:MyUnknownDatasource)).to be true + AbstractDatasource.class_eval('@@subclasses -= [MyUnknownDatasource]') + Object.send(:remove_const, :MyUnknownDatasource) + Object.send(:const_set, :MyUnknownDatasource, Class.new) + end + + it 'can register and apply custom plugins' do + expect { subject.configure_and_run(['-c', './spec/tests/demo_config.txt', '-t', 'spec/tests/custom_demo_report', '-o', './result.pdf']) }.to output(/ERROR/).to_stderr + expect(subject.config.logger).not_to receive(:error) + expect { subject.configure_and_run(['-c', './spec/tests/demo_config.txt', '-t', 'spec/tests/custom_demo_report', '-o', './result.pdf', '-r', './spec/tests/custom_plugin']) }.not_to output(/ERROR/).to_stderr + expect(Object.constants.include?(:MyUnknownDatasource)).to be true + end + end + + context 'ERB templating' do + subject { GrafanaReporter::Application::Application.new } + + before do + File.delete('./result.txt') if File.exist?('./result.txt') + allow(subject.config.logger).to receive(:debug) + allow(subject.config.logger).to receive(:info) + allow(subject.config.logger).to receive(:warn) + end + + after do + File.delete('./result.txt') if File.exist?('./result.txt') + end + + it 'can single render a template with extension' do + expect(subject.config.logger).not_to receive(:error) + expect { subject.configure_and_run(['-c', './spec/tests/erb.config', '-t', 'spec/tests/erb.template', '-o', './result.txt', '-d', 'ERROR']) }.not_to output(/ERROR/).to_stderr + expect(File.exist?('./result.txt')).to be true + expect(File.read('./result.txt')).to include('This is a test 1594308060000.') + end + end + context 'webserver' do before(:context) do WebMock.disable_net_connect!(allow: ['http://localhost:8033']) @@ -136,8 +189,9 @@ def done? after(:context) do WebMock.enable! AbstractReport.clear_event_listeners - @webserver.kill - # TODO: kill webservice properly and release port again + # kill webservice properly and release port again + @app.webservice.stop! + sleep 0.1 until @app.webservice.stopped? end it 'responds to overview' do @@ -242,14 +296,24 @@ def done? expect(res['content-disposition']).to include('.zip') end - it 'returns error on render without proper template' do - expect(@app.config.logger).to receive(:error).with(/is not a valid template\./) + it 'returns error on render without template' do + evt = ReportEventHandler.new + AbstractReport.add_event_listener(:on_after_finish, evt) + + expect_any_instance_of(GrafanaReporter::Logger::TwoWayDelegateLogger).to receive(:error).with(/is not a valid template\./) res = Net::HTTP.get(URI('http://localhost:8033/render')) - expect(res).to include("is not a valid template.") + cur_time = Time.new + sleep 0.1 while !evt.done? && Time.new - cur_time < 10 + end - expect(@app.config.logger).to receive(:error).with(/is not a valid template\./) + it 'returns error on render with non existing template' do + evt = ReportEventHandler.new + AbstractReport.add_event_listener(:on_after_finish, evt) + + expect_any_instance_of(GrafanaReporter::Logger::TwoWayDelegateLogger).to receive(:error).with(/is not a valid template\./) res = Net::HTTP.get(URI('http://localhost:8033/render?var-template=does_not_exist')) - expect(res).to include("is not a valid template.") + cur_time = Time.new + sleep 0.1 while !evt.done? && Time.new - cur_time < 10 end end end diff --git a/spec/integration/panel_image_macro_spec.rb b/spec/integration/panel_image_macro_spec.rb index 61eedd8..b8f2466 100644 --- a/spec/integration/panel_image_macro_spec.rb +++ b/spec/integration/panel_image_macro_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do block_macro PanelImageBlockMacro.new.current_report(report) @@ -18,7 +18,13 @@ expect(Asciidoctor.convert("grafana_panel_image::#{STUBS[:panel_sql][:id]}[dashboard=\"#{STUBS[:dashboard]}\"]", to_file: false)).to include(' { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } config.logger.level = ::Logger::Severity::WARN - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do inline_macro PanelImageInlineMacro.new.current_report(report) @@ -44,8 +50,7 @@ it 'cleans up created temporary files' do expect(@report.logger).not_to receive(:error) - ts = Time.now.to_s - result = Asciidoctor.convert("grafana_panel_image:#{STUBS[:panel_sql][:id]}[dashboard=\"#{STUBS[:dashboard]}\"]", to_file: false, attributes: { 'grafana-report-timestamp' => ts }) + result = Asciidoctor.convert("grafana_panel_image:#{STUBS[:panel_sql][:id]}[dashboard=\"#{STUBS[:dashboard]}\"]", to_file: false) tmp_file = result.to_s.gsub(/.*img src="([^"]+)".*/m, '\1') # TODO: ensure that the file existed before expect(File.exist?("./spec/templates/images/#{tmp_file}")).to be false diff --git a/spec/integration/panel_property_inline_macro_spec.rb b/spec/integration/panel_property_inline_macro_spec.rb index 7e887b5..73ba48f 100644 --- a/spec/integration/panel_property_inline_macro_spec.rb +++ b/spec/integration/panel_property_inline_macro_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin]} } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do inline_macro PanelPropertyInlineMacro.new.current_report(report) diff --git a/spec/integration/panel_query_table_include_processor_spec.rb b/spec/integration/panel_query_table_include_processor_spec.rb index abc29ea..34527b2 100644 --- a/spec/integration/panel_query_table_include_processor_spec.rb +++ b/spec/integration/panel_query_table_include_processor_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin]} } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do include_processor PanelQueryTableIncludeProcessor.new.current_report(report) @@ -73,6 +73,13 @@ end + context 'unknown datasource' do + it 'returns error on unknown datasource requests' do + expect(@report.logger).to receive(:error) + expect(Asciidoctor.convert("include::grafana_panel_query_table:#{STUBS[:panel_ds_unknown][:id]}[query=\"A\",dashboard=\"#{STUBS[:dashboard]}\",from=\"0\",to=\"0\"]", to_file: false)).to include('Error') + end + end + context 'graphite' do it 'can handle graphite requests' do expect(@report.logger).not_to receive(:error) diff --git a/spec/integration/panel_query_value_inline_macro_spec.rb b/spec/integration/panel_query_value_inline_macro_spec.rb index 3aa495f..fe252e0 100644 --- a/spec/integration/panel_query_value_inline_macro_spec.rb +++ b/spec/integration/panel_query_value_inline_macro_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do inline_macro PanelQueryValueInlineMacro.new.current_report(report) @@ -60,6 +60,11 @@ expect(Asciidoctor.convert("grafana_panel_query_value:#{STUBS[:panel_sql][:id]}[query=\"#{STUBS[:panel_sql][:letter]}\",dashboard=\"#{STUBS[:dashboard]}\",format=\",%.2f\",filter_columns=\"time_sec\"]", to_file: false)).to include('

43.90') end + it 'can filter columns and handle wront format definitions' do + expect(@report.logger).to receive(:error).with('invalid format character - %').at_least(:once) + expect(Asciidoctor.convert("grafana_panel_query_value:#{STUBS[:panel_sql][:id]}[query=\"#{STUBS[:panel_sql][:letter]}\",dashboard=\"#{STUBS[:dashboard]}\",format=\",%2%2f\",filter_columns=\"time_sec\"]", to_file: false)).to include('

invalid format character') + end + it 'shows fatal error if query is missing' do expect(@report.logger).to receive(:fatal).with(/GrafanaError: The specified query '' does not exist in the panel '11' in dashboard.*/) expect(Asciidoctor.convert("grafana_panel_query_value:#{STUBS[:panel_sql][:id]}[dashboard=\"#{STUBS[:dashboard]}\",format=\",%.2f\",filter_columns=\"time_sec\"]", to_file: false)).to include('GrafanaError: The specified query \'\' does not exist in the panel \'11\' in dashboard') diff --git a/spec/integration/report_webhook_spec.rb b/spec/integration/report_webhook_spec.rb index fe419f1..21c9d35 100644 --- a/spec/integration/report_webhook_spec.rb +++ b/spec/integration/report_webhook_spec.rb @@ -32,8 +32,9 @@ after(:context) do WebMock.enable! AbstractReport.clear_event_listeners - @webserver.kill - # TODO: kill webservice properly and release port again + # kill webservice properly and release port again + @app.webservice.stop! + sleep 0.1 until @app.webservice.stopped? end it 'calls event listener properly' do diff --git a/spec/integration/show_environment_include_processor_spec.rb b/spec/integration/show_environment_include_processor_spec.rb index ecb6c74..1fb5e38 100644 --- a/spec/integration/show_environment_include_processor_spec.rb +++ b/spec/integration/show_environment_include_processor_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do include_processor ShowEnvironmentIncludeProcessor.new.current_report(report) diff --git a/spec/integration/show_help_include_processor_spec.rb b/spec/integration/show_help_include_processor_spec.rb index 7f49019..3757098 100644 --- a/spec/integration/show_help_include_processor_spec.rb +++ b/spec/integration/show_help_include_processor_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do include_processor ShowHelpIncludeProcessor.new.current_report(report) diff --git a/spec/integration/sql_table_include_processor_spec.rb b/spec/integration/sql_table_include_processor_spec.rb index 97ad612..a818949 100644 --- a/spec/integration/sql_table_include_processor_spec.rb +++ b/spec/integration/sql_table_include_processor_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do include_processor SqlTableIncludeProcessor.new.current_report(report) diff --git a/spec/integration/sql_value_inline_macro_spec.rb b/spec/integration/sql_value_inline_macro_spec.rb index 79478fe..052de6b 100644 --- a/spec/integration/sql_value_inline_macro_spec.rb +++ b/spec/integration/sql_value_inline_macro_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do inline_macro SqlValueInlineMacro.new.current_report(report) diff --git a/spec/integration/value_as_variable_include_processor_spec.rb b/spec/integration/value_as_variable_include_processor_spec.rb index 6f0e3ec..77d6638 100644 --- a/spec/integration/value_as_variable_include_processor_spec.rb +++ b/spec/integration/value_as_variable_include_processor_spec.rb @@ -5,7 +5,7 @@ config = Configuration.new config.logger.level = ::Logger::Severity::WARN config.config = { 'grafana' => { 'default' => { 'host' => STUBS[:url], 'api_key' => STUBS[:key_admin] } } } - report = Report.new(config, './spec/tests/demo_report.adoc') + report = Report.new(config) Asciidoctor::Extensions.unregister_all Asciidoctor::Extensions.register do include_processor ValueAsVariableIncludeProcessor.new.current_report(report) diff --git a/spec/models/abstract_datasource_spec.rb b/spec/models/abstract_datasource_spec.rb index 03ee688..5f02c0d 100644 --- a/spec/models/abstract_datasource_spec.rb +++ b/spec/models/abstract_datasource_spec.rb @@ -12,7 +12,7 @@ expect { AbstractDatasource.build_instance(nil) }.to raise_error(InvalidDatasourceQueryProvidedError) end - it 'raises error if unknown datasource definition is provided' do - expect { AbstractDatasource.build_instance({'meta' => {'category' => 'unknown', 'id' => 'unknown_ds'}}) }.to raise_error(DatasourceTypeNotSupportedError) + it 'returns unsupported datasource if not supported' do + expect(AbstractDatasource.build_instance({'meta' => {'category' => 'unknown', 'id' => 'unknown_ds'}})).to be_a(Grafana::UnsupportedDatasource) end end diff --git a/spec/models/abstract_report_spec.rb b/spec/models/abstract_report_spec.rb index 6da4f73..dd73ddc 100644 --- a/spec/models/abstract_report_spec.rb +++ b/spec/models/abstract_report_spec.rb @@ -1,9 +1,9 @@ include GrafanaReporter describe AbstractReport do - subject { AbstractReport.new(Configuration.new, './spec/tests/demo_report.adoc') } + subject { AbstractReport.new(Configuration.new) } it 'has abstract methods' do - expect { subject.progress }.to raise_error(NotImplementedError) + expect { subject.class.demo_report_classes }.to raise_error(NotImplementedError) end end diff --git a/spec/models/dashboard_spec.rb b/spec/models/dashboard_spec.rb index 9493abd..b203b1b 100644 --- a/spec/models/dashboard_spec.rb +++ b/spec/models/dashboard_spec.rb @@ -4,7 +4,7 @@ let(:dashboard) { Dashboard.new(JSON.parse(File.read('./spec/tests/demo_dashboard.json'))['dashboard'], Grafana::Grafana.new('')) } it 'contains panels' do - expect(dashboard.panels.length).to eq(9) + expect(dashboard.panels.length).to eq(10) expect(dashboard.panel(11)).to be_a(Panel) expect(dashboard.panel(11).field('id')).to eq(11) expect(dashboard.panel(11).field('no_field_exists')).to eq('') diff --git a/spec/stubs/webmock.rb b/spec/stubs/webmock.rb index 0e8b49b..5bfa8c6 100644 --- a/spec/stubs/webmock.rb +++ b/spec/stubs/webmock.rb @@ -8,6 +8,7 @@ key_admin: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', key_viewer: 'viewerxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', dashboard: 'IDBRfjSmz', + panel_ds_unknown: { id: '10' }, panel_sql: { id: '11', letter: 'A', title: 'Temperaturen' }, panel_graphite: { id: '12', letter: 'A' }, panel_prometheus: { id: '13', letter: 'A' }, @@ -110,13 +111,13 @@ ) .to_return(status: 200, body: '{"results":{"A":{"refId":"A","meta":{"rowCount":0,"sql":"SELECT 1 as value WHERE value = 0"},"series":null,"tables":null,"dataframes":null}}}', headers: {}) - stub_request(:get, %r{http://localhost/render/d-solo/IDBRfjSmz\?from=\d+&fullscreen=true&panelId=11&theme=light&timeout=60(?:&var-[^&]+)*}).with( + stub_request(:get, %r{http://localhost/render/d-solo/IDBRfjSmz\?from=\d+&fullscreen=true&panelId=(?:10|11)&theme=light&timeout=60(?:&var-[^&]+)*}).with( headers: default_header.merge({ 'Accept' => 'image/png', 'Authorization' => "Bearer #{STUBS[:key_admin]}" }) ) - .to_return(status: 200, body: File.read('./spec/tests/sample_image.png'), headers: {}) + .to_return(status: 200, body: File.read('./spec/tests/sample_image.png', File.size('./spec/tests/sample_image.png')), headers: {}) stub_request(:post, 'http://localhost/api/tsdb/query').with( body: %r{.*SELECT time as time_sec, value / 10 as Ist FROM istwert_hk1 WHERE \$__unixEpochFilter\(time\) ORDER BY time DESC.*}, diff --git a/spec/tests/custom_demo_report.adoc b/spec/tests/custom_demo_report.adoc new file mode 100644 index 0000000..f77f568 --- /dev/null +++ b/spec/tests/custom_demo_report.adoc @@ -0,0 +1,3 @@ += Ruby-Grafana-Reporter Demo Report + +Show a valid value: grafana_sql_value:5[sql="SELECT 1"] diff --git a/spec/tests/custom_plugin.rb b/spec/tests/custom_plugin.rb new file mode 100644 index 0000000..fd60e4e --- /dev/null +++ b/spec/tests/custom_plugin.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class MyUnknownDatasource < ::Grafana::AbstractDatasource + def self.handles?(model) + model['type'] == 'UnknownDatasource' + end + + def request(query_description) + { header: ['I am handled'], content: [[1000]] } + end +end diff --git a/spec/tests/demo_dashboard.json b/spec/tests/demo_dashboard.json index aaf5bd0..65f6fe1 100644 --- a/spec/tests/demo_dashboard.json +++ b/spec/tests/demo_dashboard.json @@ -46,6 +46,18 @@ "id": 2, "links": [], "panels": [ + { + "datasource": "UnknownDatasource", + "id": 10, + "targets": [ + { + "hide": false, + "refId": "A", + "target": "alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count, 10), 20), 'cpu')" + } + ], + "type": "singlestat" + }, { "datasource": "Graphite", "id": 12, diff --git a/spec/tests/demo_dashboard_with_unknown_datasource.json b/spec/tests/demo_dashboard_with_unknown_datasource.json deleted file mode 100644 index c762d27..0000000 --- a/spec/tests/demo_dashboard_with_unknown_datasource.json +++ /dev/null @@ -1,1175 +0,0 @@ -{ - "meta": { - "isHome": true, - "canSave": false, - "canEdit": true, - "canAdmin": false, - "canStar": false, - "slug": "", - "url": "", - "expires": "0001-01-01T00:00:00Z", - "created": "0001-01-01T00:00:00Z", - "updated": "0001-01-01T00:00:00Z", - "updatedBy": "", - "createdBy": "", - "version": 0, - "hasAcl": false, - "isFolder": false, - "folderId": 0, - "folderTitle": "General", - "folderUrl": "", - "provisioned": false, - "provisionedExternalId": "" - }, - "dashboard": { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": false, - "iconColor": "rgba(0, 211, 255, 1)", - "limit": 100, - "name": "Annotations & Alerts", - "showIn": 0, - "tags": [ - "all" - ], - "type": "tags" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": 2, - "links": [], - "panels": [ - { - "datasource": "DatasourceDoesNotExist", - "id": 12, - "targets": [ - { - "hide": false, - "refId": "A", - "target": "alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count, 10), 20), 'cpu')" - } - ], - "type": "singlestat" - }, - { - "datasource": "Prometheus", - "id": 13, - "targets": [ - { - "hide": false, - "expr": "sum by(mode)(irate(node_cpu_seconds_total{job=\"node\", instance=~\"$node:.*\", mode!=\"idle\"}[5m])) > 0", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{mode}}", - "metric": "", - "refId": "A", - "step": 10 - } - ], - "title": "CPU usage", - "type": "graph" - }, - { - "datasource": "demo", - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "fill": 1, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 8 - }, - "id": 21, - "legend": { - "alignAsTable": false, - "avg": false, - "current": false, - "max": false, - "min": false, - "rightSide": false, - "show": false, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [ - { - "alias": "count", - "yaxis": 1 - } - ], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "alias": "", - "format": "time_series", - "group": [], - "hide": false, - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n MAX(value / 10) as max_temperature,\n AVG(value / 10) as avg_temperature,\n MIN(value / 10) as min_temperatureq\n FROM rm_temperatur\n WHERE $__unixEpochFilter(time)\n GROUP BY YEAR(FROM_UNIXTIME(time)), MONTH(FROM_UNIXTIME(time)), DAY(FROM_UNIXTIME(time))\n", - "refId": "A", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Tagestemperaturen Aktuelles Jahr", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": 3, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "decimals": null, - "format": "short", - "label": null, - "logBase": 1, - "max": "50", - "min": null, - "show": true - }, - { - "decimals": null, - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - }, - "options": { - "dataLinks": [] - }, - "fillGradient": 0, - "hiddenSeries": false - }, - { - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 16, - "title": "A", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "", - "decimals": 1, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 12, - "w": 12, - "x": 0, - "y": 1 - }, - "hiddenSeries": false, - "id": 11, - "legend": { - "alignAsTable": true, - "avg": true, - "current": false, - "max": true, - "min": true, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "format": "time_series", - "group": [], - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value / 10 as Außentemperatur\nFROM rm_temperatur\nWHERE $__unixEpochFilter(time)\nORDER BY time DESC", - "refId": "D", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - }, - { - "alias": "", - "format": "time_series", - "group": [], - "hide": false, - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value / 10 as Ist\nFROM istwert_hk1\nWHERE $__unixEpochFilter(time)\nORDER BY time DESC", - "refId": "A", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - }, - { - "alias": "", - "format": "time_series", - "group": [], - "hide": false, - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value / 10 as Soll\nFROM sollwert_hk1\nWHERE $__unixEpochFilter(time)\nORDER BY time DESC\n", - "refId": "B", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - }, - { - "alias": "", - "format": "time_series", - "group": [], - "hide": false, - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value / 10 as Warmwasser\nFROM warmwasser_temperatur\nWHERE $__unixEpochFilter(time)\nORDER BY time DESC", - "refId": "C", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Temperaturen", - "description": "ich baue hier eine $my-var Variable ein", - "tooltip": { - "shared": false, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "decimals": null, - "format": "celsius", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "decimals": null, - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": true, - "alignLevel": null - } - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 13 - }, - "id": 14, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "demo", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 14 - }, - "hiddenSeries": false, - "id": 8, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": false, - "min": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [ - { - "alias": "Sonnenhöhe", - "yaxis": 2 - } - ], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "alias": "", - "format": "time_series", - "group": [], - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value as Sonnenrichtung\nFROM rm_sonnenrichtung\nWHERE $__unixEpochFilter(time)\nORDER BY time", - "refId": "A", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - }, - { - "alias": "", - "format": "time_series", - "group": [], - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value as Sonnenhöhe\nFROM rm_sonnenhoehe\nWHERE $__unixEpochFilter(time)\nORDER BY time", - "refId": "B", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Sonnenverlauf", - "tooltip": { - "shared": false, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "degree", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "degree", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "demo", - "decimals": 2, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 14 - }, - "hiddenSeries": false, - "id": 3, - "legend": { - "alignAsTable": true, - "avg": true, - "current": false, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": true, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [ - { - "alias": "value", - "yaxis": 2 - }, - { - "alias": "Verdichter an", - "yaxis": 2 - }, - { - "alias": "WW an", - "yaxis": 2 - } - ], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "alias": "", - "format": "time_series", - "group": [], - "hide": false, - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT time_sec, kWh FROM (SELECT\n time as time_sec,\n IF(@lastvalue <> -1, IF(value < 0, 0, (value - @lastvalue) * 0.1), 0) as kWh,\n IF(value < 0, @lastvalue := 0, @lastvalue := value)\nFROM wirkarbeit, (SELECT @lastvalue := -1) SQLVars\nWHERE $__unixEpochFilter(time)\nORDER BY time ASC) aq", - "refId": "A", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - }, - { - "alias": "", - "format": "time_series", - "group": [], - "hide": false, - "metricColumn": "none", - "rawQuery": true, - "rawSql": "(SELECT\n $__unixEpochFrom() as time,\n value as \"Verdichter an\"\nFROM verdichter_an\nWHERE time <= $__unixEpochFrom() LIMIT 1)\nUNION\n(SELECT\n time as time,\n value as \"Verdichter an\"\nFROM verdichter_an\nWHERE $__unixEpochFilter(time))\nUNION\n(SELECT\n $__unixEpochTo() as time,\n value as \"Verdichter an\"\nFROM verdichter_an\nWHERE time <= $__unixEpochTo() LIMIT 1)", - "refId": "B", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Wirkarbeit", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "watth", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": "4", - "min": null, - "show": false - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "columns": [], - "datasource": "demo", - "fontSize": "100%", - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 21 - }, - "hideTimeOverride": false, - "id": 10, - "links": [], - "options": {}, - "pageSize": null, - "scroll": true, - "showHeader": true, - "sort": { - "col": 0, - "desc": true - }, - "styles": [ - { - "alias": "Time", - "dateFormat": "YYYY-MM-DD HH:mm:ss", - "link": false, - "pattern": "Time", - "type": "date" - }, - { - "alias": "", - "colorMode": null, - "colors": [ - "rgba(245, 54, 54, 0.9)", - "rgba(237, 129, 40, 0.89)", - "rgba(50, 172, 45, 0.97)" - ], - "decimals": 2, - "pattern": "/.*/", - "thresholds": [], - "type": "number", - "unit": "short" - } - ], - "targets": [ - { - "alias": "", - "format": "table", - "rawSql": "SELECT 1", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Annotations", - "transform": "annotations", - "type": "table" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "demo", - "decimals": 1, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 22 - }, - "hiddenSeries": false, - "id": 12, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [ - { - "alias": "Helligkeit", - "yaxis": 2 - }, - { - "alias": "Regen", - "steppedLine": true - } - ], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "alias": "", - "format": "time_series", - "group": [], - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value as Helligkeit\nFROM rm_lichtwert\nWHERE $__unixEpochFilter(time)\nORDER BY time DESC", - "refId": "A", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - }, - { - "alias": "", - "format": "time_series", - "group": [], - "metricColumn": "none", - "rawQuery": true, - "rawSql": "(SELECT\n $__unixEpochFrom() as time,\n value as Regen\nFROM rm_regen\nWHERE time <= $__unixEpochFrom() LIMIT 1)\nUNION ALL\n(SELECT\n time as time,\n value as Regen\nFROM rm_regen\nWHERE $__unixEpochFilter(time))\nUNION ALL\n(SELECT\n $__unixEpochTo() as time,\n value as Regen\nFROM rm_regen\nWHERE time <= $__unixEpochTo() LIMIT 1)", - "refId": "C", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - }, - { - "format": "time_series", - "group": [], - "metricColumn": "none", - "rawQuery": true, - "rawSql": "SELECT\n time as time_sec,\n value as 'Windstärke'\nFROM rm_wind\nWHERE $__unixEpochFilter(time)\nORDER BY time DESC", - "refId": "B", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Wetter", - "tooltip": { - "shared": false, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "decimals": null, - "format": "none", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "decimals": null, - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "B", - "type": "row" - } - ], - "refresh": false, - "schemaVersion": 21, - "style": "dark", - "tags": [], - "templating": { - "list": [ - { - "allValue": null, - "current": { - "selected": false, - "text": "ten", - "value": "10" - }, - "hide": 0, - "includeAll": false, - "label": null, - "multi": false, - "name": "test", - "options": [ - { - "selected": false, - "text": "one", - "value": "1" - }, - { - "selected": true, - "text": "ten", - "value": "10" - }, - { - "selected": false, - "text": "twenty-five", - "value": "25" - } - ], - "query": "1,10,25", - "skipUrlSync": false, - "type": "custom" - }, - { - "allValue": null, - "current": { - "selected": true, - "text": "1 + 2 + , + $ + / + \" + ' + . + a + | + \\", - "value": [ - "1", - "2", - ",", - "$", - "/", - "\"", - "'", - ".", - "a", - "|", - "\\" - ] - }, - "hide": 0, - "includeAll": false, - "label": null, - "multi": true, - "name": "testmulti", - "options": [ - { - "selected": true, - "text": "1", - "value": "1" - }, - { - "selected": true, - "text": "2", - "value": "2" - }, - { - "selected": true, - "text": ",", - "value": "," - }, - { - "selected": true, - "text": "$", - "value": "$" - }, - { - "selected": true, - "text": "/", - "value": "/" - }, - { - "selected": true, - "text": "\"", - "value": "\"" - }, - { - "selected": true, - "text": "'", - "value": "'" - }, - { - "selected": true, - "text": ".", - "value": "." - }, - { - "selected": true, - "text": "a", - "value": "a" - }, - { - "selected": true, - "text": "|", - "value": "|" - }, - { - "selected": true, - "text": "\\", - "value": "\\" - } - ], - "query": "1,2,\\,,$,/,\",',.,a,|,\\", - "queryValue": "", - "skipUrlSync": false, - "type": "custom" - }, - { - "allValue": null, - "current": { - "selected": true, - "text": "test bla12$/\"'.a,|=\\", - "value": "test bla12$/\"'.a,|=\\" - }, - "hide": 0, - "includeAll": false, - "label": null, - "multi": false, - "name": "testsingle", - "options": [ - { - "selected": true, - "text": "test bla12$/\"'.a,|=\\", - "value": "test bla12$/\"'.a,|=\\" - }, - { - "selected": false, - "text": "resolved", - "value": "1" - } - ], - "query": "test bla12$/\"'.a\\,|=\\", - "queryValue": "", - "skipUrlSync": false, - "type": "custom" - }, - { - "allValue": null, - "current": { - "selected": true, - "text": "1596660163000", - "value": "1596660163000" - }, - "hide": 0, - "includeAll": false, - "label": null, - "multi": false, - "name": "timestamp", - "options": [ - { - "selected": true, - "text": "1596660163000", - "value": "1596660163000" - } - ], - "query": "1596660163000", - "queryValue": "", - "skipUrlSync": false, - "type": "custom" - } - ] - }, - "time": { - "from": "now-24h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "timezone": "", - "title": "Todayd", - "uid": "IDBRfjSmz", - "version": 30 - } -} - diff --git a/spec/tests/erb.config b/spec/tests/erb.config new file mode 100644 index 0000000..70d6930 --- /dev/null +++ b/spec/tests/erb.config @@ -0,0 +1,19 @@ +# This configuration has been built with the configuration wizard. + +grafana: + default: + host: http://localhost + api_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + +grafana-reporter: + report-class: GrafanaReporter::ERB::Report + templates-folder: . + reports-folder: . + report-retention: 24 + webservice-port: 8015 + +default-document-attributes: + imagesdir: . +# feel free to add here additional asciidoctor document attributes which are applied to all your templates diff --git a/spec/tests/erb.template b/spec/tests/erb.template new file mode 100644 index 0000000..fc2276e --- /dev/null +++ b/spec/tests/erb.template @@ -0,0 +1,9 @@ +<% +dashboard = 'IDBRfjSmz' +instance = 'default' +panel = @report.grafana(instance).dashboard(dashboard).panel(11) +query = QueryValueQuery.new(panel) +query.assign_variable('result_type', ::Grafana::Variable.new('panel_value')) +query.assign_variable('query', ::Grafana::Variable.new('A')) +%> +This is a test <%= query.execute %>. diff --git a/spec/tests/frontend_settings.json b/spec/tests/frontend_settings.json index d00a6cc..356b465 100644 --- a/spec/tests/frontend_settings.json +++ b/spec/tests/frontend_settings.json @@ -231,6 +231,18 @@ "name": "Prometheus", "type": "prometheus", "url":"/api/datasources/proxy/4" + }, + "UnknownDatasource": { + "id": 5, + "meta": { + "type": "datasource", + "name": "UnknownDatasource", + "id": "UnknownDatasource", + "category":"tsdb" + }, + "name": "UnknownDatasource", + "type": "UnknownDatasource", + "url":"/api/datasources/proxy/4" } }, "defaultDatasource": "demo",