├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Dockerfile ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── config.ru ├── config.yml.example ├── grafana_matrix.gemspec ├── kubernetes-manifest.yaml ├── lib ├── grafana_matrix.rb └── grafana_matrix │ ├── config.rb │ ├── image_handler.rb │ ├── renderer.rb │ ├── server.rb │ ├── templates │ ├── html.erb │ └── plain.erb │ └── version.rb └── test ├── fixtures ├── message-details.html ├── message.html └── message.plain ├── grafana_matrix_test.rb ├── renderer_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /vendor/ 10 | Gemfile.local* 11 | Gemfile.lock 12 | config.yml 13 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AllCops: 3 | TargetRubyVersion: 2.5 4 | Exclude: 5 | - '*.spec' 6 | - 'Rakefile' 7 | - 'vendor/**/*' 8 | NewCops: enable 9 | SuggestExtensions: false 10 | 11 | # Don't enforce documentation 12 | Style/Documentation: 13 | Enabled: false 14 | 15 | Style/FrozenStringLiteralComment: 16 | Enabled: false 17 | 18 | Style/MultilineBlockChain: 19 | Enabled: false 20 | 21 | Metrics/PerceivedComplexity: 22 | Enabled: false 23 | 24 | Metrics/CyclomaticComplexity: 25 | Enabled: false 26 | 27 | Style/RescueModifier: 28 | Enabled: false 29 | 30 | Metrics/MethodLength: 31 | Max: 40 32 | 33 | Metrics/AbcSize: 34 | Enabled: false 35 | 36 | Metrics/BlockLength: 37 | Max: 40 38 | Exclude: 39 | - 'test/**/*' 40 | 41 | Metrics/ClassLength: 42 | Max: 200 43 | Exclude: 44 | - 'test/**/*' 45 | 46 | Layout/LineLength: 47 | Max: 190 48 | 49 | Style/ClassAndModuleChildren: 50 | Exclude: 51 | - 'test/**/*' 52 | - 'app/controllers/concerns/foreman/**/*' 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | rvm: 5 | - 2.6 6 | - 2.7 7 | - 3.0 8 | 9 | cache: 10 | bundler: true 11 | before_install: 12 | - gem install bundler rubocop 13 | - gem update --system 14 | script: 15 | - rubocop lib/ 16 | - bundle exec rake test 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0-alpine 2 | 3 | #RUN apt-get update -qq && apt-get install -y build-essential 4 | 5 | ENV APP_HOME /app 6 | ENV RACK_ENV production 7 | RUN mkdir $APP_HOME 8 | WORKDIR $APP_HOME 9 | 10 | ADD Gemfile* $APP_HOME/ 11 | ADD *gemspec $APP_HOME/ 12 | ADD config.yml.example config.ru $APP_HOME/ 13 | ADD lib $APP_HOME/lib/ 14 | 15 | RUN bundle install --without development 16 | 17 | EXPOSE 9292/tcp 18 | CMD ["bundle", "exec", "rackup"] 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in grafana_matrix.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Alexander Olofsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grafana Matrix 2 | 3 | A Grafana webhook ingress for sending Matrix notifications 4 | 5 | Working: 6 | - Simple notifications 7 | - Authenticated requests 8 | 9 | TODO: 10 | - Proper styling 11 | 12 | ## Installation / Usage 13 | 14 | Until a more proper release is done; 15 | Download the git repo, instantiate the bundle, create a proper configuration file, and launch the server 16 | 17 | ```sh 18 | git clone https://github.com/ananace/ruby-grafana-matrix 19 | cd ruby-grafana-matrix 20 | bundle install --path=vendor 21 | cp config.yml.example config.yml 22 | vi config.yml 23 | # Edit the configuration to suit your requirements 24 | 25 | bundle exec rackup 26 | ``` 27 | 28 | You would then add the ingester as a Grafana webhook channel like so - albeit with port 9292 unless changed; 29 | 30 | ![Grafana config](https://i.imgur.com/Cu4m8Ew.png) 31 | 32 | ## Docker 33 | 34 | Build the image: 35 | 36 | `docker build -t ruby-grafana-matrix:latest .` 37 | 38 | Create a proper configuration file: 39 | 40 | ```sh 41 | cp config.yml.example config.yml 42 | vi config.yml 43 | ``` 44 | Run the resulting container, and mount your `config.yml` inside of it: 45 | 46 | `docker run -v $PWD/config.yml:/app/config.yml --name ruby-grafana-matrix ruby-grafana-matrix:latest` 47 | 48 | If running the container on the same host as Grafana, you can attach it to the same Docker network and use the container name in the Grafana webhook URL. 49 | 50 | ## Contributing 51 | 52 | Bug reports and pull requests are welcome on GitHub at https://github.com/ananace/ruby-grafana-matrix 53 | 54 | ## License 55 | 56 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 57 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << 'test' 6 | t.libs << 'lib' 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'grafana_matrix' 2 | 3 | config = GrafanaMatrix::Config.global 4 | config.load! 'config.yml' 5 | warn 'Specifying port/bind in config' if config.port? || config.bind? 6 | 7 | Signal.trap('HUP') do 8 | warn "[#{Time.now}] SIGHUP received, reloading configuration" 9 | config.load! 10 | end 11 | 12 | map '/health' do 13 | run -> { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } 14 | end 15 | 16 | map '/' do 17 | run GrafanaMatrix::Server.new 18 | end 19 | -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | --- 2 | # Set up your HS connections 3 | matrix: 4 | - name: matrix-org 5 | url: 'https://matrix.org' 6 | # Create a user - log that user in using a post request 7 | # curl -XPOST -d '{"type": "m.login.password", 8 | # "user":"grafana", 9 | # "password":"2m4ny53cr3t5"}' 10 | # "https://my-matrix-server/_matrix/client/r0/login" 11 | # Fill that access token in here 12 | access_token: '' 13 | #device_id: # Optional 14 | #- name: matrix-priv 15 | # url: 'https://private.matrix.org' 16 | # access_token: '' 17 | 18 | # The default message type for messages, should be either m.text or m.notice, 19 | # defaults to m.text 20 | msgtype: m.text 21 | 22 | # Set up notification ingress rules 23 | rules: 24 | - name: hq # Name of the rule 25 | room: "#hq:matrix.org" # Room or ID 26 | matrix: matrix-org # The Matrix HS to use - defaults to first one 27 | msgtype: m.notice 28 | # The following values are optional: 29 | #image: true # Attach image to the notification? 30 | #embed_image: true # Upload and embed the image into the message? 31 | #details_tag: false # Use a
tag to include image (MSC2184) 32 | #templates: 33 | # Templates to use when rendering the notification, available placeholders: 34 | # %TEMPLATES% - lib/grafana_matrix/templates 35 | # $ - Environment variables 36 | #html: "%TEMPLATES%/html.erb" # Path to HTML template 37 | #plain: "%TEMPLATES%/plain.erb" # Path to plaintext template 38 | #auth: 39 | #user: example 40 | #pass: any HTTP encodable string 41 | #- name: other-hq 42 | # room: "#hq:private.matrix.org 43 | # matrix: matrix-priv 44 | 45 | # To use the webhook, you need to configure it into Grafana as: 46 | # 47 | # Url: http://:/hook?rule= 48 | # Http Method: POST 49 | -------------------------------------------------------------------------------- /grafana_matrix.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.join File.expand_path('lib', __dir__), 'grafana_matrix/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'grafana_matrix' 7 | spec.version = GrafanaMatrix::VERSION 8 | spec.authors = ['Alexander Olofsson'] 9 | spec.email = ['alexander.olofsson@liu.se'] 10 | 11 | spec.summary = 'A Matrix notification target for Grafana' 12 | spec.description = 'Converts Grafana alerts into Matrix notifications ' \ 13 | 'using Grafana alert webhooks' 14 | spec.homepage = 'https://github.com/ananace/ruby-grafana-matrix' 15 | spec.license = 'MIT' 16 | 17 | spec.extra_rdoc_files = %w[LICENSE.txt README.md] 18 | spec.files = Dir['lib/**/*'] + spec.extra_rdoc_files 19 | spec.require_paths = ['lib'] 20 | 21 | spec.required_ruby_version = '>= 2.6', '< 4' 22 | 23 | spec.add_dependency 'matrix_sdk', '~> 2.3' 24 | spec.add_dependency 'sinatra' 25 | spec.add_dependency 'webrick', '~> 1.7' 26 | 27 | spec.add_development_dependency 'bundler' 28 | spec.add_development_dependency 'minitest' 29 | spec.add_development_dependency 'mocha' 30 | spec.add_development_dependency 'rake' 31 | spec.add_development_dependency 'rubocop' 32 | spec.add_development_dependency 'simplecov' 33 | end 34 | -------------------------------------------------------------------------------- /kubernetes-manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | labels: 6 | app: grafana-matrix-ingress 7 | name: grafana-matrix-ingress 8 | spec: 9 | ports: 10 | - port: 9292 11 | protocol: TCP 12 | targetPort: 9292 13 | selector: 14 | app: grafana-matrix-ingress 15 | type: ClusterIP 16 | --- 17 | apiVersion: v1 18 | data: 19 | config.yml: | 20 | --- 21 | # Webhook server configuration 22 | # Or use the launch options `-o '::' -p 4567` 23 | #bind: '::' 24 | #port: 4567 25 | 26 | # Set up your HS connections 27 | matrix: 28 | - name: matrix-org 29 | url: https://matrix.org 30 | access_token: 31 | #device_id: # Optional 32 | #- name: matrix-priv 33 | # url: https://private.matrix.org 34 | # access_token: 35 | 36 | 37 | # Set up notification ingress rules 38 | rules: 39 | - name: hq # Name of the rule 40 | room: "#hq:matrix.org" # Room or ID 41 | matrix: matrix-org # The Matrix HS to use - defaults to first one 42 | # The following values are optional: 43 | #image: true # Attach image to the notification? 44 | #embed_image: true # Upload and embed the image into the message? 45 | #templates: 46 | # Templates to use when rendering the notification, available placeholders: 47 | # %TEMPLATES% - lib/grafana_matrix/templates 48 | # $ - Environment variables 49 | #html: "%TEMPLATES%/html.erb" # Path to HTML template 50 | #plain: "%TEMPLATES%/plain.erb" # Path to plaintext template 51 | #auth: 52 | #user: example 53 | #pass: any HTTP encodable string 54 | #- name: other-hq 55 | # room: "#hq:private.matrix.org 56 | # matrix: matrix-priv 57 | 58 | # To use the webhook, you need to configure it into Grafana as: 59 | # 60 | # Url: http://:/hook?rule= 61 | # Http Method: POST 62 | kind: ConfigMap 63 | metadata: 64 | labels: 65 | app: grafana-matrix-ingress 66 | name: grafana-matrix-ingress 67 | --- 68 | apiVersion: extensions/v1beta1 69 | kind: Deployment 70 | metadata: 71 | labels: 72 | app: grafana-matrix-ingress 73 | name: grafana-matrix-ingress 74 | spec: 75 | selector: 76 | matchLabels: 77 | app: grafana-matrix-ingress 78 | template: 79 | metadata: 80 | labels: 81 | app: grafana-matrix-ingress 82 | spec: 83 | containers: 84 | - image: ananace/grafana-matrix:latest 85 | name: grafana-matrix-ingress 86 | ports: 87 | - containerPort: 9292 88 | protocol: TCP 89 | resources: 90 | limits: 91 | cpu: 250m 92 | memory: 256Mi 93 | requests: 94 | cpu: 10m 95 | memory: 25Mi 96 | volumeMounts: 97 | - mountPath: /app/config.yml 98 | name: config 99 | subPath: config.yml 100 | restartPolicy: Always 101 | volumes: 102 | - configMap: 103 | name: grafana-matrix-ingress 104 | name: config 105 | -------------------------------------------------------------------------------- /lib/grafana_matrix.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'grafana_matrix/config' 4 | require 'grafana_matrix/image_handler' 5 | require 'grafana_matrix/renderer' 6 | require 'grafana_matrix/server' 7 | require 'grafana_matrix/version' 8 | 9 | module GrafanaMatrix; end 10 | -------------------------------------------------------------------------------- /lib/grafana_matrix/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'matrix_sdk' 4 | require 'psych' 5 | 6 | module GrafanaMatrix 7 | class Config 8 | Rule = Struct.new(:config, :data) do 9 | def name 10 | data.fetch(:name) 11 | end 12 | 13 | def room 14 | data.fetch(:room) 15 | end 16 | 17 | def matrix 18 | data.fetch(:matrix) 19 | end 20 | 21 | def templates 22 | data.fetch(:templates, nil) 23 | end 24 | 25 | def auth 26 | data.fetch(:auth, nil) 27 | end 28 | 29 | def auth_user 30 | auth?.fetch(:user) 31 | end 32 | 33 | def auth_pass 34 | auth?.fetch(:pass) 35 | end 36 | 37 | def html_template 38 | templates&.fetch('html', nil) || Renderer::HTML_TEMPLATE 39 | end 40 | 41 | def plain_template 42 | templates&.fetch('plain', nil) || Renderer::PLAIN_TEMPLATE 43 | end 44 | 45 | def image? 46 | data.fetch(:image, true) 47 | end 48 | 49 | def embed_image? 50 | data.fetch(:embed_image, true) 51 | end 52 | 53 | def details? 54 | data.fetch(:details_tag, false) 55 | end 56 | 57 | def msgtype 58 | data.fetch(:msgtype, config.default_msgtype) 59 | end 60 | 61 | def client 62 | # TODO: Proper handling of this 63 | return Object.new if matrix.nil? 64 | 65 | @client ||= config.client(matrix) 66 | end 67 | end 68 | 69 | def self.global 70 | @global ||= self.new 71 | end 72 | 73 | def initialize(config = {}) 74 | if config.is_a? String 75 | file = config 76 | config = {} 77 | end 78 | @config = config 79 | @clients = {} 80 | 81 | load!(file) if file 82 | end 83 | 84 | def load!(filename = 'config.yml') 85 | raise 'No such file' unless File.exist? filename 86 | 87 | @config = Psych.load(File.read(filename)) 88 | true 89 | end 90 | 91 | def default_msgtype 92 | @config.fetch('msgtype', 'm.text') 93 | end 94 | 95 | def bind? 96 | @config.key? 'bind' 97 | end 98 | 99 | def bind 100 | @config.fetch('bind', '::') 101 | end 102 | 103 | def port? 104 | @config.key? 'port' 105 | end 106 | 107 | def port 108 | @config.fetch('port', 4567) 109 | end 110 | 111 | def client(client_name = nil) 112 | client_name ||= @config['matrix'].first['name'] 113 | raise 'No client name provided' unless client_name 114 | 115 | client_data = @config['matrix'].find { |m| m['name'] == client_name } 116 | raise 'No client configuration found for name given' unless client_data 117 | 118 | @clients[client_name] ||= begin 119 | client_data = client_data.dup.transform_keys { |key| key.to_sym rescue key } 120 | 121 | MatrixSdk::Api.new(client_data[:url], 122 | **client_data.reject { |k, _v| %i[url].include? k }) 123 | end 124 | end 125 | 126 | def rules(rule_name) 127 | @config['rules'] 128 | .select { |m| m['name'] == rule_name } 129 | .map do |rule_data| 130 | rule_data = rule_data.dup.transform_keys { |key| key.to_sym rescue key } 131 | 132 | Rule.new self, rule_data 133 | end 134 | end 135 | 136 | def rule(rule_name) 137 | rules(rule_name).first 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/grafana_matrix/image_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'net/http' 4 | 5 | module GrafanaMatrix 6 | class ImageHandler 7 | def upload(client, image) 8 | # TODO 9 | file = Net::HTTP.get(URI(image)) 10 | uploaded = client.media_upload(file, 'image/png') 11 | uploaded[:content_uri] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/grafana_matrix/renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GrafanaMatrix 4 | class Renderer 5 | HTML_TEMPLATE = '%TEMPLATES%/html.erb'.freeze 6 | PLAIN_TEMPLATE = '%TEMPLATES%/plain.erb'.freeze 7 | 8 | SEVERITY_COLOURS = { 9 | ok: '#10a345', 10 | paused: '#8e8e8e', 11 | alerting: '#ed2e18', 12 | pending: '#8e8e8e', 13 | no_data: '#f79520' 14 | }.freeze 15 | 16 | def render(data, rule, template) 17 | erb = ERB.new template, trim_mode: '-' 18 | erb.result(binding) 19 | end 20 | 21 | def render_html(data, rule, template = HTML_TEMPLATE) 22 | render data, rule, File.read(expand_path(template)) 23 | end 24 | 25 | def render_plain(data, rule, template = PLAIN_TEMPLATE) 26 | render data, rule, File.read(expand_path(template)) 27 | end 28 | 29 | private 30 | 31 | def expand_path(path) 32 | path.gsub('%TEMPLATES%', File.expand_path('templates', __dir__)) 33 | .gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}/) do 34 | match = Regexp.last_match 35 | ENV[match[1]] 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/grafana_matrix/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sinatra/base' 4 | 5 | module GrafanaMatrix 6 | class Server < ::Sinatra::Base 7 | configure :development, :production do 8 | enable :logging 9 | end 10 | 11 | helpers do 12 | def config; @config ||= GrafanaMatrix::Config.global; end 13 | def renderer; @renderer ||= GrafanaMatrix::Renderer.new; end 14 | def image_handler; @image_handler ||= GrafanaMatrix::ImageHandler.new; end 15 | 16 | def authorized(user, pass) 17 | @auth ||= Rack::Auth::Basic::Request.new(request.env) 18 | @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == [user, pass] 19 | end 20 | end 21 | 22 | get '/' do 23 | # Just to override the default page 24 | end 25 | 26 | post '/hook' do 27 | rule_name = params[:rule] 28 | halt 400, 'Missing rule name' unless rule_name 29 | 30 | rules = (config.rules(rule_name) rescue []) 31 | .select do |rule| 32 | next true unless rule.auth 33 | 34 | # Parse HTTP basic auth 35 | authorized(rule.auth['user'], rule.auth['pass']) 36 | end 37 | halt 404, 'No such rule configured' if rules.empty? 38 | 39 | data = request.body.read 40 | halt 400, 'No notification body provided' if data.empty? 41 | 42 | data = JSON.parse(data) rescue nil 43 | halt 400, 'Unable to parse notification body' unless data 44 | 45 | logger.debug 'Data:' 46 | logger.debug data 47 | 48 | rules.each do |rule| 49 | client = rule.client 50 | halt 500, 'Unable to acquire Matrix client from rule' unless client 51 | 52 | room = rule.room if rule.room.start_with? '!' 53 | room ||= client.join_room(rule.room).room_id 54 | halt 500, 'Unable to acquire Matrix room from rule and client' unless room 55 | 56 | # Upload the image to the Matrix HS if requested to be embedded 57 | begin 58 | data['imageUrl'] = image_handler.upload(client, data['imageUrl']) if rule.embed_image? && rule.image? && data['imageUrl'] 59 | rescue StandardError => e 60 | logger.fatal "Failed to upload image\n#{e.class} (#{e.message})\n#{e.backtrace.join "\n"}" 61 | # Disable embedding for this call 62 | rule.data[:embed_image] = false 63 | end 64 | 65 | plain = renderer.render_plain(data, rule, rule.plain_template) 66 | html = renderer.render_html(data, rule, rule.html_template) 67 | 68 | logger.debug 'Plain:' 69 | logger.debug plain 70 | 71 | logger.debug 'HTML:' 72 | logger.debug html 73 | 74 | # Support rules with nil client explicitly specified, for testing 75 | next unless client.is_a? MatrixSdk::Api 76 | 77 | client.send_message_event(room, 'm.room.message', 78 | { msgtype: rule.msgtype, 79 | body: plain, 80 | formatted_body: html, 81 | format: 'org.matrix.custom.html' }) 82 | end 83 | 84 | { result: :success }.to_json 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/grafana_matrix/templates/html.erb: -------------------------------------------------------------------------------- 1 |

<%= data['title'] %>

2 | <%- if data['imageUrl'] && rule.details? && data['state'] != 'ok' -%> 3 |
4 | <%- unless data.fetch('message', '').empty? -%> 5 | <%= data['message'] %>
6 | <%- end -%> 7 | <%- else -%> 8 | <%- unless data.fetch('message', '').empty? -%> 9 |

<%= data['message'] %>

10 | <%- end -%> 11 | <%- end -%> 12 | <%- if data.key? 'error' -%> 13 |
Error:
14 |

<%= data['error'] %>

15 | <%- end -%> 16 | <%- if data['state'] != 'ok' -%> 17 | <%- unless data['evalMatches'].empty? -%> 18 | <%- data['evalMatches'].each do |match| -%> 19 | <%= match['metric'] %>: <%= match['value'] %>
20 | <%- end -%> 21 | <%- end -%> 22 | <%- if data['imageUrl'] && rule.details? -%> 23 |
24 | <%- end -%> 25 | <%- if data['imageUrl'] && rule.image? -%> 26 | <%- if !rule.details? -%> 27 |
28 | <%- end -%> 29 | <%- if rule.embed_image? -%> 30 | <%= data['ruleName'] %> 31 | <%- else -%> 32 | Alert image 33 | <%- end -%> 34 | <%- end -%> 35 | <%- end -%> 36 | <%- if data['imageUrl'] && rule.details? -%> 37 |
38 | <%- end -%> 39 | -------------------------------------------------------------------------------- /lib/grafana_matrix/templates/plain.erb: -------------------------------------------------------------------------------- 1 | <%= data['title'] %> 2 | <%- unless data.fetch('message', '').empty? -%> 3 | <%= data['message'] %> 4 | <%- end -%> 5 | <%- if data.key? 'error' -%> 6 | Error: <%= data['error'] %> 7 | <%- end -%> 8 | 9 | <%- if data['state'] != 'ok' -%> 10 | <%- unless data['evalMatches'].empty? -%> 11 | <%- data['evalMatches'].each do |match| -%> 12 | <%= match['metric'] %>: <%= match['value'] %> 13 | <%- end -%> 14 | 15 | <%- end -%> 16 | <%- if data['imageUrl'] && rule.image? -%> 17 | Image: <%= data['imageUrl'] %> 18 | <%- end -%> 19 | <%- end -%> 20 | URL: <%= data['ruleUrl'] %> 21 | -------------------------------------------------------------------------------- /lib/grafana_matrix/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GrafanaMatrix 4 | VERSION = '0.0.1' 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/message-details.html: -------------------------------------------------------------------------------- 1 |

[Alerting] IO wait alert

2 |
3 | IO wait percentage averaged above 10% the last five minutes
4 | node.example.com: 14.45
5 | node2.example.com: 11.22
6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /test/fixtures/message.html: -------------------------------------------------------------------------------- 1 |

[Alerting] IO wait alert

2 |

IO wait percentage averaged above 10% the last five minutes

3 | node.example.com: 14.45
4 | node2.example.com: 11.22
5 |
6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/message.plain: -------------------------------------------------------------------------------- 1 | [Alerting] IO wait alert 2 | IO wait percentage averaged above 10% the last five minutes 3 | 4 | node.example.com: 14.45 5 | node2.example.com: 11.22 6 | 7 | Image: https://grafana.example.com/image.png 8 | URL: https://grafana.example.com/panel-url 9 | -------------------------------------------------------------------------------- /test/grafana_matrix_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class GrafanaMatrixTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::GrafanaMatrix::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/renderer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RendererTest < Minitest::Test 4 | ALERTDATA = { 5 | 'title' => '[Alerting] IO wait alert', 6 | 'message' => 'IO wait percentage averaged above 10% the last five minutes', 7 | 'state' => 'alerting', 8 | 'evalMatches' => [ 9 | { 'metric' => 'node.example.com', 'value' => 14.45 }, 10 | { 'metric' => 'node2.example.com', 'value' => 11.22 } 11 | ], 12 | 'imageUrl' => 'https://grafana.example.com/image.png', 13 | 'ruleUrl' => 'https://grafana.example.com/panel-url' 14 | }.freeze 15 | ALERTRULE = GrafanaMatrix::Config::Rule.new(nil, {}).freeze 16 | ALERTRULE_DETAILS = GrafanaMatrix::Config::Rule.new(nil, { details_tag: true }).freeze 17 | 18 | def setup 19 | @renderer = GrafanaMatrix::Renderer.new 20 | end 21 | 22 | def test_that_it_renders_plain 23 | assert_equal load_fixture('message.plain').read, @renderer.render_plain(ALERTDATA, ALERTRULE) 24 | end 25 | 26 | def test_that_it_renders_html 27 | assert_equal load_fixture('message.html').read, @renderer.render_html(ALERTDATA, ALERTRULE) 28 | end 29 | 30 | def test_that_it_renders_html_with_details 31 | assert_equal load_fixture('message-details.html').read, @renderer.render_html(ALERTDATA, ALERTRULE_DETAILS) 32 | end 33 | 34 | def test_path_expansion 35 | ENV['GRAFANA_MATRIX_TESTENV'] = 'TestValue' 36 | assert_equal 'TestValue', @renderer.send(:expand_path, '$GRAFANA_MATRIX_TESTENV') 37 | assert_equal 'TestValue/blah', @renderer.send(:expand_path, '${GRAFANA_MATRIX_TESTENV}/blah') 38 | assert_equal File.join(File.expand_path('../lib/grafana_matrix/templates/', __dir__), 'TestValue/blah'), @renderer.send(:expand_path, '%TEMPLATES%/${GRAFANA_MATRIX_TESTENV}/blah') 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | SimpleCov.start do 5 | add_filter '/test/' 6 | add_filter '/vendor/' 7 | end 8 | 9 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 10 | require 'grafana_matrix' 11 | 12 | require 'minitest/autorun' 13 | require 'mocha' 14 | 15 | def load_fixture(filename) 16 | File.open(File.join('test', 'fixtures', filename)) 17 | end 18 | --------------------------------------------------------------------------------