├── Gemfile
├── lib
├── grafana_matrix
│ ├── version.rb
│ ├── image_handler.rb
│ ├── templates
│ │ ├── plain.erb
│ │ └── html.erb
│ ├── renderer.rb
│ ├── server.rb
│ └── config.rb
└── grafana_matrix.rb
├── .gitignore
├── test
├── grafana_matrix_test.rb
├── fixtures
│ ├── message.plain
│ ├── message.html
│ └── message-details.html
├── test_helper.rb
└── renderer_test.rb
├── Rakefile
├── .travis.yml
├── Dockerfile
├── config.ru
├── .rubocop.yml
├── LICENSE.txt
├── grafana_matrix.gemspec
├── README.md
├── config.yml.example
└── kubernetes-manifest.yaml
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in grafana_matrix.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/lib/grafana_matrix/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module GrafanaMatrix
4 | VERSION = '0.0.1'
5 | end
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/fixtures/message.html:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/fixtures/message-details.html:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/grafana_matrix/templates/html.erb:
--------------------------------------------------------------------------------
1 |
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 |
31 | <%- else -%>
32 | Alert image
33 | <%- end -%>
34 | <%- end -%>
35 | <%- end -%>
36 | <%- if data['imageUrl'] && rule.details? -%>
37 |
38 | <%- end -%>
39 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------