├── .docker └── run.sh ├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── config.sample.yaml ├── src └── github.com │ └── turt2live │ └── matrix-monitor-bot │ ├── cmd │ └── monitor_bot │ │ └── main.go │ ├── config │ ├── config.go │ └── consts.go │ ├── events │ ├── general.go │ └── info.go │ ├── logging │ └── logger.go │ ├── matrix │ ├── client.go │ ├── dispatch_ping.go │ ├── events.go │ ├── invite_handler.go │ ├── message_handler.go │ ├── ping_handler.go │ ├── responses.go │ └── util.go │ ├── metrics │ ├── metrics.go │ ├── ping_handler.go │ └── server.go │ ├── pinger │ └── producer.go │ ├── tracker │ ├── domain.go │ ├── helpers.go │ ├── remote_domain.go │ ├── room.go │ └── tree.go │ ├── util │ ├── arrays.go │ ├── retry.go │ └── time.go │ └── webserver │ └── server.go ├── vendor └── manifest └── web ├── compare.html ├── fonts ├── stylesheet.css ├── weblysleekuisl.eot ├── weblysleekuisl.svg ├── weblysleekuisl.ttf └── weblysleekuisl.woff ├── img └── grey.png ├── layout.html └── style.css /.docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd /data 3 | if [ ! -f monitor-bot.yaml ]; then 4 | cp /etc/monitor-bot.sample monitor-bot.yaml 5 | fi 6 | chown -R ${UID}:${GID} /data 7 | exec su-exec ${UID}:${GID} monitor_bot -web /etc/monbot-web 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | .idea 4 | .travis.yml 5 | monitor-bot*.yaml 6 | logs/ 7 | pkg/ 8 | bin/ 9 | vendor/pkg 10 | vendor/src 11 | vendor/bin 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /bin 3 | /pkg 4 | /logs 5 | 6 | monitor-bot*.yaml 7 | .env 8 | 9 | vendor/pkg 10 | vendor/src 11 | 12 | # Binaries for programs and plugins 13 | *.exe 14 | *.dll 15 | *.so 16 | *.dylib 17 | 18 | # Test binary, build with `go test -c` 19 | *.test 20 | 21 | # Output of the go coverage tool, specifically when used with LiteIDE 22 | *.out 23 | 24 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 25 | .glide/ 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9 4 | before_install: 5 | - go get github.com/constabulary/gb/... 6 | install: 7 | - gb vendor restore 8 | script: 9 | - gb build all 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/alpine 2 | COPY . /tmp/src 3 | RUN apk add --no-cache \ 4 | su-exec \ 5 | && apk add --no-cache \ 6 | -t build-deps \ 7 | go \ 8 | git \ 9 | musl-dev \ 10 | dos2unix \ 11 | && apk add --no-cache ca-certificates \ 12 | && cd /tmp/src \ 13 | && GOPATH=`pwd` go get github.com/constabulary/gb/... \ 14 | && PATH=$PATH:`pwd`/bin gb vendor restore \ 15 | && GOPATH=`pwd`:`pwd`/vendor go build -o bin/monitor_bot ./src/github.com/turt2live/matrix-monitor-bot/cmd/monitor_bot/ \ 16 | && cp bin/monitor_bot .docker/run.sh /usr/local/bin \ 17 | && mkdir -p /etc/monbot-web \ 18 | && cp -r web/* /etc/monbot-web/ \ 19 | && cp config.sample.yaml /etc/monitor-bot.yaml.sample \ 20 | && dos2unix /etc/monitor-bot.yaml.sample \ 21 | && dos2unix /usr/local/bin/run.sh \ 22 | && chmod a+x /usr/local/bin/run.sh \ 23 | && cd / \ 24 | && rm -rf /tmp/* \ 25 | && apk del build-deps 26 | 27 | CMD exec /usr/local/bin/run.sh 28 | VOLUME ["/data"] 29 | EXPOSE 8080 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matrix-monitor-bot 2 | 3 | [![#monitorbot:t2bot.io](https://img.shields.io/badge/matrix-%23monitorbot:t2bot.io-brightgreen.svg)](https://matrix.to/#/#monitorbot:t2bot.io) 4 | [![TravisCI badge](https://travis-ci.org/turt2live/matrix-monitor-bot.svg?branch=master)](https://travis-ci.org/turt2live/matrix-monitor-bot) 5 | 6 | A bot to measure latency between homeservers, as perceived by users. 7 | 8 | # Installing 9 | 10 | Assuming Go 1.9 is already installed on your PATH: 11 | ```bash 12 | # Get it 13 | git clone https://github.com/turt2live/matrix-monitor-bot 14 | cd matrix-monitor-bot 15 | 16 | # Set up the build tools 17 | currentDir=$(pwd) 18 | export GOPATH="$currentDir/vendor/src:$currentDir/vendor:$currentDir:"$GOPATH 19 | go get github.com/constabulary/gb/... 20 | export PATH=$PATH":$currentDir/vendor/bin:$currentDir/vendor/src/bin" 21 | 22 | # Build it 23 | gb vendor restore 24 | gb build 25 | 26 | # Configure it (edit monitor-bot.yaml to meet your needs) 27 | cp config.sample.yaml monitor-bot.yaml 28 | 29 | # Run it 30 | bin/monitor_bot 31 | ``` 32 | 33 | ### Installing in Alpine Linux 34 | 35 | The steps are almost the same as above. The only difference is that `gb build` will not work, so instead use the following lines: 36 | ```bash 37 | go build -o bin/monitor_bot ./src/github.com/turt2live/matrix-monitor-bot/cmd/monitor_bot/ 38 | ``` 39 | 40 | # Docker 41 | 42 | `/path/to/matrix-monitor-bot` should always be pointed to a folder that has your `monitor-bot.yaml` file in it. If the config 43 | file does not exist, one will be created for you (and promptly not work because it doesn't have a valid config). A folder 44 | named `logs` will also be created here (assuming you use the default configuration). 45 | 46 | 47 | **From Docker Hub:** 48 | ``` 49 | docker run -p 8080:8080 -v /path/to/matrix-monitor-bot:/data turt2live/matrix-monitor-bot 50 | ``` 51 | 52 | 53 | **Build the image yourself:** 54 | ``` 55 | git clone https://github.com/turt2live/matrix-monitor-bot 56 | cd matrix-monitor-bot 57 | docker build -t matrix-monitor-bot . 58 | docker run -p 8080:8080 -v /path/to/matrix-monitor-bot:/data matrix-monitor-bot 59 | ``` 60 | 61 | # Prometheus Metrics 62 | 63 | If metrics are enabled in your config, matrix-monitor-bot will serve up metrics for scraping by Prometheus. Every metric 64 | that is exported is a [Histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) metric. The following 65 | metrics are exported: 66 | 67 | * `monbot_ping_send_delay_seconds` - Number of seconds for the origin to send a ping to their homeserver 68 | * `monbot_ping_receive_delay_seconds` - Number of seconds for a bot to receive a ping 69 | * `monbot_ping_time_seconds` - Total number of seconds for a ping to go through (from the remote bot to the receiver) 70 | 71 | 72 | # Architecture 73 | 74 | TODO: This section 75 | * How the bot measures things 76 | * What the Prometheus metrics are 77 | * Why the bot uses m.room.message and not a custom event 78 | * Why the bot uses messages for pongs instead of read receipts 79 | * Why the display name gets overwritten and how it is used 80 | * Why someone should run this on their server 81 | -------------------------------------------------------------------------------- /config.sample.yaml: -------------------------------------------------------------------------------- 1 | # The homeserver configuration for the bot. This is where the bot will connect to. 2 | homeserver: 3 | # The URL to the client/server API. This is usually the same as specified in Riot/your client 4 | url: "https://t2bot.io" 5 | 6 | # An access token to an existing account for the bot. The display name will get overwritten. 7 | accessToken: "Your_token_here" 8 | 9 | # The settings to control what and how the bot monitors 10 | monitor: 11 | # The rooms to automatically try and join on startup. Each item in the list is a list of room 12 | # aliases to try to ensure the bot gets into the room. This can allow, for example, the bot 13 | # to join the room through the matrix.org alias if t2bot.io is down for some reason. 14 | rooms: 15 | - ["#monitor-public:t2bot.io", "#monitor-public:matrix.org"] 16 | 17 | # If enabled, the bot will auto-accept invites to allow other people to measure latency with 18 | # your server. If this is disabled (default), only the rooms listed in `rooms` above will 19 | # be allowed to use the bot. 20 | allowOtherRooms: false 21 | 22 | # The Prometheus metrics configuration 23 | metrics: 24 | # If enabled, Prometheus metrics will be served from /_monitorbot/metrics on the webserver. 25 | enabled: false 26 | 27 | # The host to bind on. It is generally recommended to keep this protected from the world. 28 | bind: '127.0.0.1' 29 | 30 | # The port to bind metrics on. If this is the same as the webserver port then the bind 31 | # configuration from the webserver will be used. 32 | port: 9000 33 | 34 | # The webserver configuration. This handles both the frontend client and the metrics endpoint. 35 | webserver: 36 | # If enabled, the webserver will serve the fronend app from the root of the domain. If set 37 | # to false 38 | serveClient: true 39 | 40 | # The host to bind to. 41 | bind: '0.0.0.0' 42 | 43 | # The port to listen on. If you're using Docker, make sure to change your port mapping 44 | port: 8080 45 | 46 | # The path to serve /static from. If not specified, the default of "/" will be used to 47 | # indicate that all /static content is served from the root. 48 | relativePath: "/" 49 | 50 | # The default domain to use when presenting a compare page. For example, this is the domain 51 | # that will be advertised on the home page. 52 | #defaultCompareDomain: "t2bot.io" 53 | 54 | # The domains to compare against the requested domain by default. If this list is empty, all 55 | # domains for which stats are available will be used as the default. 56 | compareDefaultDomains: ["matrix.org", "t2bot.io"] 57 | 58 | # The domains to feature more prominently on the compare page. This is optional and defaults 59 | # to featuring no domains. 60 | featuredCompareDomains: ["matrix.org"] 61 | 62 | # Configuration for the bot's logging 63 | logging: 64 | # The directory to store log files to. They will be rotated automatically every day. 65 | directory: "logs" -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/cmd/monitor_bot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/turt2live/matrix-monitor-bot/config" 8 | "github.com/turt2live/matrix-monitor-bot/logging" 9 | "github.com/turt2live/matrix-monitor-bot/matrix" 10 | "math/rand" 11 | "time" 12 | "github.com/turt2live/matrix-monitor-bot/pinger" 13 | "net/http" 14 | "github.com/turt2live/matrix-monitor-bot/metrics" 15 | "github.com/turt2live/matrix-monitor-bot/webserver" 16 | "fmt" 17 | ) 18 | 19 | func init() { 20 | rand.Seed(time.Now().UnixNano()) 21 | } 22 | 23 | func main() { 24 | configPath := flag.String("config", "monitor-bot.yaml", "The path to the configuration") 25 | webContentPath := flag.String("web", "./web", "The path to the webserver content") 26 | flag.Parse() 27 | 28 | config.Path = *configPath 29 | config.Runtime.WebContentDir = *webContentPath 30 | 31 | err := logging.Setup(config.Get().Logging.Directory) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | logrus.Info("Starting monitor bot...") 37 | client, err := matrix.NewClient(config.Get().Homeserver.Url, config.Get().Homeserver.AccessToken) 38 | if err != nil { 39 | logrus.Fatal(err) 40 | } 41 | logrus.Info("Authenticated as ", client.UserId) 42 | 43 | client.AutoAcceptInvites = config.Get().Monitor.AllowOtherRooms 44 | 45 | for _, aliases := range config.Get().Monitor.Rooms { 46 | err = client.JoinRoomByAliases(aliases) 47 | if err != nil { 48 | logrus.Fatal("Failed to join configured rooms: ", err) 49 | } 50 | } 51 | 52 | // Prepare the webservers 53 | if config.Get().Metrics.Port == config.Get().Webserver.Port && config.Get().Metrics.Enabled { 54 | go func() { 55 | mux := http.NewServeMux() 56 | metrics.InitServer(mux) 57 | 58 | if config.Get().Webserver.WithClient { 59 | webserver.InitServer(mux, client) 60 | } 61 | 62 | address := fmt.Sprintf("%s:%d", config.Get().Webserver.Bind, config.Get().Webserver.Port) 63 | logrus.Info("Webserver and metrics listening on ", address) 64 | logrus.Fatal(http.ListenAndServe(address, mux)) 65 | }() 66 | } else { 67 | if config.Get().Metrics.Enabled { 68 | go func() { 69 | mux := http.NewServeMux() 70 | metrics.InitServer(mux) 71 | address := fmt.Sprintf("%s:%d", config.Get().Metrics.Bind, config.Get().Metrics.Port) 72 | logrus.Info("Metrics listening on ", address) 73 | logrus.Fatal(http.ListenAndServe(address, mux)) 74 | }() 75 | } 76 | 77 | if config.Get().Webserver.WithClient { 78 | go func() { 79 | mux := http.NewServeMux() 80 | webserver.InitServer(mux, client) 81 | address := fmt.Sprintf("%s:%d", config.Get().Webserver.Bind, config.Get().Webserver.Port) 82 | logrus.Info("Webserver listening on ", address) 83 | logrus.Fatal(http.ListenAndServe(address, mux)) 84 | }() 85 | } 86 | } 87 | 88 | logrus.Info("Starting ping producer") 89 | producer := pinger.NewProducer(config.PingInterval, client) 90 | producer.Start() 91 | 92 | logrus.Info("Starting sync") 93 | client.StartSync() 94 | } 95 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "sync" 8 | 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | type runtimeConfig struct { 13 | WebContentDir string 14 | } 15 | 16 | var Runtime = &runtimeConfig{} 17 | 18 | type HomeserverConfig struct { 19 | Url string `yaml:"url"` 20 | AccessToken string `yaml:"accessToken"` 21 | } 22 | 23 | type MonitorConfig struct { 24 | Rooms [][]string `yaml:"rooms,flow"` 25 | AllowOtherRooms bool `yaml:"allowOtherRooms"` 26 | } 27 | 28 | type MetricsConfig struct { 29 | Enabled bool `yaml:"enabled"` 30 | Bind string `yaml:"bind"` 31 | Port int `yaml:"port"` 32 | } 33 | 34 | type WebserverConfig struct { 35 | WithClient bool `yaml:"serveClient"` 36 | Bind string `yaml:"bind"` 37 | Port int `yaml:"port"` 38 | RelativePath string `yaml:"relativePath"` 39 | DefaultCompareDomain string `yaml:"defaultCompareDomain"` 40 | DefaultCompareToDomains []string `yaml:"compareDefaultDomains,flow"` 41 | FeaturedCompareDomains []string `yaml:"featuredCompareDomains,flow"` 42 | } 43 | 44 | type LoggingConfig struct { 45 | Directory string `yaml:"directory"` 46 | } 47 | 48 | type BotConfig struct { 49 | Homeserver *HomeserverConfig `yaml:"homeserver"` 50 | Monitor *MonitorConfig `yaml:"monitor"` 51 | Metrics *MetricsConfig `yaml:"metrics"` 52 | Webserver *WebserverConfig `yaml:"webserver"` 53 | Logging *LoggingConfig `yaml:"logging"` 54 | } 55 | 56 | var instance *BotConfig 57 | var singletonLock = &sync.Once{} 58 | var Path = "monitor-bot.yaml" 59 | 60 | func ReloadConfig() (error) { 61 | c := NewDefaultConfig() 62 | 63 | // Write a default config if the one given doesn't exist 64 | _, err := os.Stat(Path) 65 | exists := err == nil || !os.IsNotExist(err) 66 | if !exists { 67 | fmt.Println("Generating new configuration...") 68 | configBytes, err := yaml.Marshal(c) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | newFile, err := os.Create(Path) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | _, err = newFile.Write(configBytes) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | err = newFile.Close() 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | 89 | f, err := os.Open(Path) 90 | if err != nil { 91 | return err 92 | } 93 | defer f.Close() 94 | 95 | buffer, err := ioutil.ReadAll(f) 96 | err = yaml.Unmarshal(buffer, &c) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | instance = c 102 | return nil 103 | } 104 | 105 | func Get() (*BotConfig) { 106 | if instance == nil { 107 | singletonLock.Do(func() { 108 | err := ReloadConfig() 109 | if err != nil { 110 | panic(err) 111 | } 112 | }) 113 | } 114 | return instance 115 | } 116 | 117 | func NewDefaultConfig() *BotConfig { 118 | return &BotConfig{ 119 | Homeserver: &HomeserverConfig{ 120 | Url: "https://t2bot.io", 121 | AccessToken: "YOUR_TOKEN_HERE", 122 | }, 123 | Monitor: &MonitorConfig{ 124 | Rooms: [][]string{{"#monitor-public:t2bot.io", "#monitor-public:matrix.org"}}, 125 | }, 126 | Metrics: &MetricsConfig{ 127 | Enabled: false, 128 | Bind: "127.0.0.1", 129 | Port: 9000, 130 | }, 131 | Webserver: &WebserverConfig{ 132 | WithClient: true, 133 | Bind: "0.0.0.0", 134 | Port: 8080, 135 | RelativePath: "/", 136 | DefaultCompareDomain: "", 137 | DefaultCompareToDomains: make([]string, 0), 138 | FeaturedCompareDomains: make([]string, 0), 139 | }, 140 | Logging: &LoggingConfig{ 141 | Directory: "logs", 142 | }, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/config/consts.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const RemoteSendDelayWarnThreshold = 1500 * time.Millisecond 8 | const ReceiveDelayWarnThreshold = 5 * time.Second 9 | const PingTimeWarnThreshold = 1500 * time.Millisecond 10 | const PingInterval = 5 * time.Minute 11 | const WebWarnStatusThreshold = 1500 * time.Millisecond 12 | const WebAverageInterval = 15 * time.Minute 13 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/events/general.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | type DisplayHints struct { 4 | Hints [][]string `json:"display_hints"` 5 | } 6 | 7 | type TextBody struct { 8 | Body string `json:"body"` 9 | } 10 | 11 | type RelatesTo struct { 12 | InReplyTo ReplyTo `json:"m.in_reply_to"` 13 | } 14 | 15 | type ReplyTo struct { 16 | EventId string `json:"event_id"` 17 | } 18 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/events/info.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/turt2live/matrix-monitor-bot/tracker" 5 | ) 6 | 7 | type PingInfo struct { 8 | Version int `json:"version"` 9 | GeneratedMs int64 `json:"generated_ms"` 10 | SenderDomain string `json:"domain"` // Legacy 11 | Tree tracker.RemoteTree `json:"tree"` 12 | } 13 | 14 | type PingContent struct { 15 | Msgtype string `json:"msgtype"` 16 | Body string `json:"body"` 17 | DisplayHints DisplayHints `json:"m.display_hints"` 18 | TextBody TextBody `json:"m.text"` 19 | 20 | // This is the actual object we end up parsing ourselves. The rest of the stuff is so the event 21 | // doesn't look too atrocious in Riot/clients. 22 | PingInfo PingInfo `json:"io.t2bot.monitor.ping"` 23 | } 24 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "time" 7 | 8 | "github.com/lestrrat/go-file-rotatelogs" 9 | "github.com/rifflock/lfshook" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type utcFormatter struct { 14 | logrus.Formatter 15 | } 16 | 17 | func (f utcFormatter) Format(entry *logrus.Entry) ([]byte, error) { 18 | entry.Time = entry.Time.UTC() 19 | return f.Formatter.Format(entry) 20 | } 21 | 22 | func Setup(dir string) error { 23 | formatter := &utcFormatter{ 24 | &logrus.TextFormatter{ 25 | TimestampFormat: "2006-01-02 15:04:05.000 Z07:00", 26 | FullTimestamp: true, 27 | ForceColors: true, 28 | DisableColors: false, 29 | DisableTimestamp: false, 30 | QuoteEmptyFields: true, 31 | }, 32 | } 33 | 34 | logrus.SetFormatter(formatter) 35 | logrus.SetOutput(os.Stdout) 36 | 37 | if dir == "" { 38 | return nil 39 | } 40 | _ = os.MkdirAll(dir, os.ModePerm) 41 | 42 | logFile := path.Join(dir, "monitor_bot.log") 43 | writer, err := rotatelogs.New( 44 | logFile+".%Y%m%d%H%M", 45 | rotatelogs.WithLinkName(logFile), 46 | rotatelogs.WithMaxAge((24*time.Hour)*14), // keep for 14 days 47 | rotatelogs.WithRotationTime(24*time.Hour), // rotate every 24 hours 48 | ) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | logrus.AddHook(lfshook.NewHook(lfshook.WriterMap{ 54 | logrus.DebugLevel: writer, 55 | logrus.InfoLevel: writer, 56 | logrus.WarnLevel: writer, 57 | logrus.ErrorLevel: writer, 58 | logrus.FatalLevel: writer, 59 | logrus.PanicLevel: writer, 60 | }, formatter)) 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/client.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/matrix-org/gomatrix" 5 | 6 | "encoding/json" 7 | "github.com/hashicorp/go-multierror" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type Client struct { 12 | mxClient *gomatrix.Client 13 | info *BotInfo 14 | infoStr string 15 | joinedRoomIds []string 16 | 17 | UserId string // readonly 18 | Domain string // readonly 19 | AutoAcceptInvites bool 20 | } 21 | 22 | type BotInfo struct { 23 | FormatVersion int `json:"formatVersion"` 24 | IsBot bool `json:"isBot"` 25 | Domain string `json:"domain"` 26 | 27 | // Other properties we care to read from bots would go here 28 | } 29 | 30 | func NewClient(csUrl string, accessToken string) (*Client, error) { 31 | client := &Client{} 32 | mxClient, err := gomatrix.NewClient(csUrl, "", accessToken) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | client.mxClient = mxClient 38 | 39 | logrus.Info("Querying for user ID") 40 | resp := &WhoAmIResponse{} 41 | url := mxClient.BuildURL("/account/whoami") 42 | _, err = mxClient.MakeRequest("GET", url, nil, resp) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | client.UserId = resp.UserId 48 | mxClient.UserID = resp.UserId 49 | 50 | server, err := ExtractUserHomeserver(client.UserId) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | client.Domain = server 56 | client.info = &BotInfo{ 57 | FormatVersion: 1, 58 | IsBot: true, // obviously 59 | Domain: server, 60 | } 61 | 62 | // TODO: use extensible profiles instead of the display name when that is a thing 63 | b, _ := json.Marshal(client.info) 64 | botInfoStr := string(b) 65 | client.infoStr = botInfoStr 66 | logrus.Info("Getting current display name") 67 | name, err := mxClient.GetOwnDisplayName() 68 | if err != nil { 69 | return nil, err 70 | } 71 | if name.DisplayName != string(botInfoStr) { 72 | logrus.Info("Updating display name") 73 | err = mxClient.SetDisplayName(botInfoStr) 74 | if err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | return client, nil 80 | } 81 | 82 | func (c *Client) JoinRoomByAliases(aliases []string) (error) { 83 | var joinError error 84 | 85 | for _, alias := range aliases { 86 | logrus.Info("Trying to join room ", alias) 87 | resp, err := c.mxClient.JoinRoom(alias, "", nil) 88 | if err != nil { 89 | logrus.Warn(err) 90 | joinError = multierror.Append(joinError, err) 91 | continue 92 | } 93 | 94 | logrus.Info("Joined ", resp.RoomID, " through ", alias) 95 | c.joinedRoomIds = append(c.joinedRoomIds, resp.RoomID) 96 | return nil 97 | } 98 | 99 | return joinError 100 | } 101 | 102 | func (c *Client) StartSync() (error) { 103 | syncer := c.mxClient.Syncer.(*gomatrix.DefaultSyncer) 104 | syncer.OnEventType("m.room.member", c.handleMembership) 105 | syncer.OnEventType("m.room.message", c.handleMessage) 106 | return c.mxClient.Sync() 107 | } 108 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/dispatch_ping.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/turt2live/matrix-monitor-bot/util" 5 | "github.com/hashicorp/go-multierror" 6 | "github.com/sirupsen/logrus" 7 | "encoding/json" 8 | "github.com/turt2live/matrix-monitor-bot/events" 9 | "github.com/turt2live/matrix-monitor-bot/tracker" 10 | "github.com/turt2live/matrix-monitor-bot/metrics" 11 | "time" 12 | ) 13 | 14 | func (c *Client) DispatchPing() (error) { 15 | rooms, err := c.mxClient.JoinedRooms() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | var aggregateErr error 21 | for _, roomId := range rooms.JoinedRooms { 22 | expectingReplyFrom, err := c.GetMonitoredDomainsInRoom(roomId) 23 | if err != nil { 24 | aggregateErr = multierror.Append(aggregateErr, err) 25 | continue 26 | } 27 | 28 | if len(expectingReplyFrom) <= 0 { 29 | logrus.Warn("Empty room: ", roomId) 30 | continue 31 | } 32 | 33 | ping := events.PingContent{ 34 | Msgtype: "m.text", 35 | Body: "Ping from " + c.Domain, 36 | DisplayHints: events.DisplayHints{Hints: [][]string{{"io.t2bot.monitor.ping"}, {"m.text"}}}, 37 | TextBody: events.TextBody{Body: "Ping from " + c.Domain}, 38 | PingInfo: events.PingInfo{ 39 | Version: 2, 40 | GeneratedMs: util.NowMillis(), 41 | SenderDomain: c.Domain, 42 | Tree: tracker.CalculateRemoteTree(c.Domain, roomId), 43 | }, 44 | } 45 | 46 | logrus.Info("Expecting a reply from ", len(expectingReplyFrom), " servers in ", roomId, ": ", expectingReplyFrom) 47 | evt, err := c.mxClient.SendMessageEvent(roomId, "m.room.message", ping) 48 | if err != nil { 49 | aggregateErr = multierror.Append(aggregateErr, err) 50 | continue 51 | } 52 | metrics.RecordPingSendDelay(c.Domain, time.Duration(util.NowMillis()-ping.PingInfo.GeneratedMs)*time.Millisecond) 53 | logrus.Info("Ping in ", roomId, " is event ", evt.EventID) 54 | } 55 | 56 | return aggregateErr 57 | } 58 | 59 | func (c *Client) GetMonitoredDomainsInRoom(roomId string) ([]string, error) { 60 | members, err := c.mxClient.JoinedMembers(roomId) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | expectingReplyFrom := make([]string, 0) 66 | for userId, profile := range members.Joined { 67 | if userId == c.UserId { 68 | continue // Skip ourselves 69 | } 70 | 71 | if profile.DisplayName == nil { 72 | logrus.Warn("User ", userId, " in ", roomId, " is not a bot (no display name)") 73 | continue 74 | } 75 | 76 | info := &BotInfo{} 77 | err := json.Unmarshal([]byte(*profile.DisplayName), info) 78 | if err != nil { 79 | logrus.Warn("User ", userId, " in ", roomId, " does not look like a bot. Display name is '", *profile.DisplayName, "'. Error parsing display name: ", err) 80 | continue 81 | } 82 | 83 | if info.FormatVersion > 1 { 84 | logrus.Warn("User ", userId, " in ", roomId, " has a newer format for bot info. We're expecting a reply, however it may be incompatible.") 85 | } 86 | if info.FormatVersion < 1 { 87 | logrus.Warn("Not considering ", userId, " in ", roomId, " to be a bot because the format version is too old") 88 | continue 89 | } 90 | 91 | // Just in case someone does something weird 92 | if !info.IsBot { 93 | logrus.Warn("User ", userId, " in ", roomId, " has a display name that is JSON and matches the format, but claims it is not a bot. Ignoring user.") 94 | continue 95 | } 96 | 97 | domain, err := ExtractUserHomeserver(userId) 98 | if err != nil { 99 | logrus.Warn("Error determining domain for user ", userId, " in ", roomId, ": ", err) 100 | continue 101 | } 102 | 103 | if domain != info.Domain { 104 | logrus.Warn("User ", userId, " has a mismatch between the advertised domain (", info.Domain, ") and their user ID domain. Ignoring user") 105 | continue 106 | } 107 | 108 | expectingReplyFrom = append(expectingReplyFrom, domain) 109 | } 110 | 111 | return expectingReplyFrom, nil 112 | } 113 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/events.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | type RoomMemberEventContent struct { 4 | Membership string `json:"membership"` 5 | DisplayName string `json:"displayname"` 6 | AvatarUrl string `json:"avatar_url,omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/invite_handler.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/matrix-org/gomatrix" 5 | "github.com/sirupsen/logrus" 6 | "github.com/turt2live/matrix-monitor-bot/util" 7 | "time" 8 | ) 9 | 10 | func (c *Client) handleMembership(ev *gomatrix.Event) { 11 | if ev.StateKey == nil || *ev.StateKey != c.UserId || ev.Content == nil || ev.Content["membership"] != "invite" { 12 | return // Not an invite for us 13 | } 14 | 15 | go func() { 16 | util.Retry(10, time.Second, func() error { 17 | reply := &RoomMemberEventContent{ 18 | DisplayName: c.infoStr, 19 | } 20 | 21 | if c.AutoAcceptInvites { 22 | logrus.Info("Accepting invite to ", ev.RoomID) 23 | reply.Membership = "join" 24 | } else { 25 | logrus.Info("Declining invite to ", ev.RoomID) 26 | reply.Membership = "leave" 27 | } 28 | 29 | _, err := c.mxClient.SendStateEvent(ev.RoomID, "m.room.member", c.UserId, reply) 30 | if err != nil { 31 | logrus.Error("Error replying to invite in ", ev.RoomID, ": ", err) 32 | return err 33 | } 34 | 35 | return nil 36 | }) 37 | }() 38 | } 39 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/message_handler.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/matrix-org/gomatrix" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func (c *Client) handleMessage(ev *gomatrix.Event) { 9 | go func() { 10 | log := logrus.WithFields(logrus.Fields{ 11 | "sender": ev.Sender, 12 | "eventId": ev.ID, 13 | "roomId": ev.RoomID, 14 | }) 15 | 16 | if ev.Content == nil { 17 | log.Warn("Event has no content (redacted?)") 18 | return 19 | } 20 | 21 | if ev.Content["io.t2bot.monitor.ping"] != nil { 22 | if ev.Sender == c.UserId { 23 | return // Don't pong ourselves 24 | } 25 | c.handlePing(log, ev) 26 | return 27 | } 28 | 29 | log.Warn("Unexpected event - is someone talking in the monitor room?") 30 | }() 31 | } 32 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/ping_handler.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/matrix-org/gomatrix" 5 | "github.com/sirupsen/logrus" 6 | "encoding/json" 7 | "github.com/turt2live/matrix-monitor-bot/util" 8 | "github.com/turt2live/matrix-monitor-bot/events" 9 | "github.com/turt2live/matrix-monitor-bot/tracker" 10 | ) 11 | 12 | func (c *Client) handlePing(log *logrus.Entry, ev *gomatrix.Event) { 13 | ping := events.PingInfo{} 14 | pingAsStr, _ := json.Marshal(ev.Content["io.t2bot.monitor.ping"]) 15 | _ = json.Unmarshal(pingAsStr, &ping) 16 | 17 | if ping.Version > 2 { 18 | log.Warn("Ping is of a higher version (", ping.Version, "). Will attempt to parse") 19 | } 20 | if ping.Version < 1 { 21 | log.Warn("Ping version is too old for processing (", ping.Version, "). Ignoring ping") 22 | return 23 | } 24 | if ping.Version < 2 { 25 | log.Warn("Ping version is old, but compatible (", ping.Version, ")") 26 | } 27 | 28 | domain, err := ExtractUserHomeserver(ev.Sender) 29 | if err != nil { 30 | log.Error("Error parsing domain from which we received a ping: ", err) 31 | return 32 | } 33 | log.Info("Ping received from ", domain) 34 | 35 | if domain != ping.SenderDomain { 36 | log.Warn("Ping domain (", ping.SenderDomain, ") does not match sender domain (", domain, "). Ignoring ping.") 37 | return 38 | } 39 | 40 | receivedMs := util.NowMillis() 41 | tracker.RecordPing(domain, c.Domain, ev.RoomID, ev.ID, ping.GeneratedMs, ev.Timestamp, receivedMs, log) 42 | 43 | // Parse the remote tree 44 | if ping.Tree != nil { 45 | for k, v := range ping.Tree { 46 | for eventId, record := range v { 47 | tracker.RecordPing(k, domain, ev.RoomID, eventId, record.GeneratedTs, record.OriginTs, record.ReceivedTs, log) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/responses.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | type WhoAmIResponse struct { 4 | UserId string `json:"user_id"` 5 | } -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/matrix/util.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "strings" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func ExtractUserHomeserver(userId string) (string, error) { 9 | if string(userId[0]) != "@" { 10 | return "", errors.New("User ID does not start with @") 11 | } 12 | 13 | idx := strings.Index(userId, ":") 14 | if idx <= 1 { 15 | return "", errors.New("No localpart for user") 16 | } 17 | 18 | return userId[idx+1:], nil 19 | } 20 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/sirupsen/logrus" 6 | "github.com/turt2live/matrix-monitor-bot/config" 7 | "time" 8 | ) 9 | 10 | const namespace = "monbot" 11 | 12 | // Metrics: 13 | // [ bot ] --A-> [ matrix.org ] --B-> [ t2bot.io ] --C-> [ bot ] 14 | // A: Ping remote send delay 15 | // B: Ping federation delay 16 | // C: Ping sync delay 17 | 18 | var pingSendDelay *prometheus.HistogramVec // Metric A 19 | var pingReceiveDelay *prometheus.HistogramVec // Metric BC 20 | var pingTime *prometheus.HistogramVec // Metric ABC 21 | 22 | // TODO: Calculate and export time between pings 23 | // TODO: Detect and export missed pings (by threshold) 24 | // TODO: Detect and export missed pongs (by threshold) 25 | 26 | func initMetrics() { 27 | logrus.Info("Creating metrics...") 28 | 29 | pingSendDelay = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 30 | Name: "ping_send_delay_seconds", 31 | Help: "Number of seconds for the origin to send a ping to their homeserver", 32 | Namespace: namespace, 33 | }, []string{"sourceDomain"}) 34 | prometheus.MustRegister(pingSendDelay) 35 | 36 | pingReceiveDelay = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 37 | Name: "ping_receive_delay_seconds", 38 | Help: "Number of seconds for a bot to receive a ping", 39 | Namespace: namespace, 40 | }, []string{"sourceDomain", "receivingDomain"}) 41 | prometheus.MustRegister(pingReceiveDelay) 42 | 43 | pingTime = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 44 | Name: "ping_time_seconds", 45 | Help: "Total number of seconds a ping lasts", 46 | Namespace: namespace, 47 | }, []string{"sourceDomain", "receivingDomain"}) 48 | prometheus.MustRegister(pingTime) 49 | } 50 | 51 | func RecordPingSendDelay(domain string, duration time.Duration) { 52 | if !config.Get().Metrics.Enabled { 53 | return 54 | } 55 | 56 | pingSendDelay.With(prometheus.Labels{ 57 | "sourceDomain": domain, 58 | }).Observe(duration.Seconds()) 59 | } 60 | 61 | func RecordPingReceiveDelay(sourceDomain string, receivingDomain string, duration time.Duration) { 62 | if !config.Get().Metrics.Enabled { 63 | return 64 | } 65 | 66 | pingReceiveDelay.With(prometheus.Labels{ 67 | "sourceDomain": sourceDomain, 68 | "receivingDomain": receivingDomain, 69 | }).Observe(duration.Seconds()) 70 | } 71 | 72 | func RecordPingTime(sourceDomain string, receivingDomain string, duration time.Duration) { 73 | if !config.Get().Metrics.Enabled { 74 | return 75 | } 76 | 77 | pingTime.With(prometheus.Labels{ 78 | "sourceDomain": sourceDomain, 79 | "receivingDomain": receivingDomain, 80 | }).Observe(duration.Seconds()) 81 | } 82 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/metrics/ping_handler.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "net/http" 5 | "io" 6 | ) 7 | 8 | type PingHandler struct{} 9 | 10 | func (PingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 11 | w.Header().Set("Content-Type", "text/plain") 12 | io.WriteString(w, "matrix-monitor-bot is alive") 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/metrics/server.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "net/http" 5 | "github.com/prometheus/client_golang/prometheus/promhttp" 6 | ) 7 | 8 | func InitServer(mux *http.ServeMux) { 9 | initMetrics() 10 | mux.Handle("/_monitorbot/metrics", promhttp.Handler()) 11 | mux.Handle("/_monitorbot/ping", PingHandler{}) 12 | } 13 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/pinger/producer.go: -------------------------------------------------------------------------------- 1 | package pinger 2 | 3 | import ( 4 | "time" 5 | "github.com/turt2live/matrix-monitor-bot/matrix" 6 | "math/rand" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type Producer struct { 11 | interval time.Duration 12 | client *matrix.Client 13 | } 14 | 15 | func NewProducer(interval time.Duration, client *matrix.Client) (*Producer) { 16 | producer := &Producer{ 17 | interval: interval, 18 | client: client, 19 | } 20 | 21 | return producer 22 | } 23 | 24 | func (p *Producer) Start() { 25 | go func() { 26 | // Because the ticker doesn't send anything until $interval, we'll trigger a ping manually 27 | doPing(time.Now(), p) 28 | 29 | ticker := time.NewTicker(p.interval) 30 | for now := range ticker.C { 31 | logrus.Info("Scheduling a ping at ", now) 32 | 33 | // We add a little bit of jitter so we don't obviously look like a bot 34 | // It also gives us the opportunity to ensure that other bots aren't just echoing back at regular intervals 35 | jitter := time.Duration(rand.Int63n(int64(p.interval))) 36 | time.Sleep(jitter / 4) 37 | 38 | doPing(now, p) 39 | } 40 | }() 41 | } 42 | 43 | func doPing(now time.Time, p *Producer) { 44 | logrus.Info("Dispatching the ping for ", now) 45 | err := p.client.DispatchPing() 46 | if err != nil { 47 | logrus.Error("Error producing ping: ", err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/tracker/domain.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type Domain struct { 9 | name string 10 | remotes *sync.Map 11 | } 12 | 13 | type DomainTimings struct { 14 | Send time.Duration 15 | Receive time.Duration 16 | HasSend bool 17 | HasReceive bool 18 | } 19 | 20 | var domainCache = &sync.Map{} 21 | 22 | func GetDomain(name string) (*Domain) { 23 | i, ok := domainCache.Load(name) 24 | if !ok { 25 | i = ResetDomain(name) 26 | } 27 | 28 | return i.(*Domain) 29 | } 30 | 31 | func ResetDomain(name string) (*Domain) { 32 | i := &Domain{remotes: &sync.Map{}, name: name} 33 | domainCache.Store(name, i) 34 | return i 35 | } 36 | 37 | func (d *Domain) GetRemote(name string) (*RemoteDomain) { 38 | i, ok := d.remotes.Load(name) 39 | if !ok { 40 | i = d.ResetRemote(name) 41 | } 42 | 43 | return i.(*RemoteDomain) 44 | } 45 | 46 | func (d *Domain) ResetRemote(name string) (*RemoteDomain) { 47 | i := &RemoteDomain{rooms: &sync.Map{}, domain: d, name: name} 48 | d.remotes.Store(name, i) 49 | return i 50 | } 51 | 52 | func (d *Domain) GetRemotes() ([]string) { 53 | r := make(map[string]interface{}) 54 | d.remotes.Range(func(k interface{}, v interface{}) bool { 55 | r[k.(string)] = 1 56 | return true // keep going if we can 57 | }) 58 | 59 | remotes := make([]string, 0, len(r)) 60 | for k := range r { 61 | remotes = append(remotes, k) 62 | } 63 | 64 | return remotes 65 | } 66 | 67 | func (d *Domain) CompareTo(other string) DomainTimings { 68 | otherDomain := GetDomain(other) 69 | remote := d.GetRemote(other) 70 | usRemote := otherDomain.GetRemote(d.name) 71 | 72 | // Calculate the send times (them -> us) 73 | sendTimeTotal := int64(0) 74 | sendTimeCount := 0 75 | for _, roomId := range usRemote.GetRooms() { 76 | room := usRemote.GetRoom(roomId) 77 | 78 | for _, ping := range room.GetPings() { 79 | sendTimeCount++ 80 | sendTimeTotal += ping.Record.ReceivedTs - ping.Record.GeneratedTs 81 | } 82 | } 83 | 84 | // Calculate the receive times (us -> them) 85 | receiveTimeTotal := int64(0) 86 | receiveTimeCount := 0 87 | for _, roomId := range remote.GetRooms() { 88 | room := remote.GetRoom(roomId) 89 | 90 | for _, ping := range room.GetPings() { 91 | receiveTimeCount++ 92 | receiveTimeTotal += ping.Record.ReceivedTs - ping.Record.GeneratedTs 93 | } 94 | } 95 | 96 | times := DomainTimings{} 97 | 98 | if sendTimeCount > 0 { 99 | times.HasSend = true 100 | avg := float64(sendTimeTotal) / float64(sendTimeCount) 101 | times.Send = time.Duration(avg) * time.Millisecond 102 | } else { 103 | times.HasSend = false 104 | } 105 | 106 | if receiveTimeCount > 0 { 107 | times.HasReceive = true 108 | avg := float64(receiveTimeTotal) / float64(receiveTimeCount) 109 | times.Receive = time.Duration(avg) * time.Millisecond 110 | } else { 111 | times.HasReceive = false 112 | } 113 | 114 | return times 115 | } 116 | 117 | func GetDomains() []string { 118 | return GetDomainsExcept() // No exceptions 119 | } 120 | 121 | func GetDomainsExcept(excludeDomains ...string) []string { 122 | d := make(map[string]interface{}) 123 | domainCache.Range(func(k interface{}, v interface{}) bool { 124 | d[k.(string)] = 1 125 | return true // keep going if we can 126 | }) 127 | 128 | domains := make([]string, 0, len(d)) 129 | for k := range d { 130 | excluded := false 131 | for _, e := range excludeDomains { 132 | if e == k { 133 | excluded = true 134 | break 135 | } 136 | } 137 | 138 | if !excluded { 139 | domains = append(domains, k) 140 | } 141 | } 142 | 143 | return domains 144 | } 145 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/tracker/helpers.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | func RecordPing(fromDomain string, toDomain string, roomId string, eventId string, generatedTs int64, originTs int64, receivedTs int64, log *logrus.Entry) (error) { 8 | // TODO: Verify the event was in the specified room 9 | d := GetDomain(toDomain) 10 | r := d.GetRemote(fromDomain) 11 | c := r.GetRoom(roomId) 12 | return c.RecordPing(eventId, generatedTs, originTs, receivedTs, log) 13 | } 14 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/tracker/remote_domain.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "sync" 5 | "github.com/patrickmn/go-cache" 6 | "github.com/turt2live/matrix-monitor-bot/config" 7 | ) 8 | 9 | type RemoteDomain struct { 10 | domain *Domain 11 | name string 12 | rooms *sync.Map 13 | } 14 | 15 | func (r *RemoteDomain) GetRoom(roomId string) (*Room) { 16 | i, ok := r.rooms.Load(roomId) 17 | if !ok { 18 | i = &Room{ 19 | remoteDomain: r, 20 | timings: cache.New(config.WebAverageInterval, config.WebAverageInterval), 21 | } 22 | r.rooms.Store(roomId, i) 23 | } 24 | 25 | return i.(*Room) 26 | } 27 | 28 | func (r *RemoteDomain) GetRooms() ([]string) { 29 | m := make(map[string]interface{}) 30 | r.rooms.Range(func(k interface{}, v interface{}) bool { 31 | m[k.(string)] = 1 32 | return true // keep going if we can 33 | }) 34 | 35 | rooms := make([]string, 0, len(m)) 36 | for k := range m { 37 | rooms = append(rooms, k) 38 | } 39 | 40 | return rooms 41 | } 42 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/tracker/room.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | "github.com/turt2live/matrix-monitor-bot/config" 6 | "time" 7 | "github.com/pkg/errors" 8 | "fmt" 9 | "github.com/sirupsen/logrus" 10 | "github.com/turt2live/matrix-monitor-bot/metrics" 11 | ) 12 | 13 | type Room struct { 14 | remoteDomain *RemoteDomain 15 | timings *cache.Cache 16 | } 17 | 18 | type Record struct { 19 | GeneratedTs int64 `json:"generated_ts"` 20 | OriginTs int64 `json:"origin_ts"` 21 | ReceivedTs int64 `json:"received_ts"` 22 | } 23 | 24 | type PingRecord struct { 25 | EventId string 26 | Record Record 27 | } 28 | 29 | func (r *Room) RecordPing(eventId string, generatedTs int64, originTs int64, receivedTs int64, log *logrus.Entry) (error) { 30 | age := time.Duration(receivedTs-generatedTs) * time.Millisecond 31 | if age >= config.WebAverageInterval { 32 | return errors.New("Event too old to store: " + fmt.Sprint(age)) 33 | } 34 | _, exists := r.timings.Get(eventId) 35 | if exists { 36 | log.Warn("Event ID ", eventId, " already stored") 37 | return nil // Not technically an error, but we don't want to trigger stats 38 | } 39 | 40 | record := Record{ 41 | GeneratedTs: generatedTs, 42 | OriginTs: originTs, 43 | ReceivedTs: receivedTs, 44 | } 45 | r.timings.Set(eventId, record, cache.DefaultExpiration) 46 | r.recordMetrics(record, log) 47 | 48 | return nil 49 | } 50 | 51 | func (r *Room) GetPings() ([]PingRecord) { 52 | records := make([]PingRecord, 0, r.timings.ItemCount()) 53 | 54 | for k, v := range r.timings.Items() { 55 | records = append(records, PingRecord{EventId: k, Record: v.Object.(Record)}) 56 | } 57 | 58 | return records 59 | } 60 | 61 | func (r *Room) recordMetrics(record Record, log *logrus.Entry) { 62 | remoteSendDelay := time.Duration(record.OriginTs-record.GeneratedTs) * time.Millisecond 63 | receiveDelay := time.Duration(record.ReceivedTs-record.OriginTs) * time.Millisecond 64 | pingTime := time.Duration(record.ReceivedTs-record.GeneratedTs) * time.Millisecond 65 | 66 | sourceDomain := r.remoteDomain.name 67 | receivingDomain := r.remoteDomain.domain.name 68 | 69 | metrics.RecordPingSendDelay(sourceDomain, remoteSendDelay) 70 | if remoteSendDelay >= config.RemoteSendDelayWarnThreshold || remoteSendDelay < 0 { 71 | log.Warn(sourceDomain, " has a ", remoteSendDelay, " delay in sending events to their homeserver") 72 | } 73 | 74 | metrics.RecordPingReceiveDelay(sourceDomain, receivingDomain, receiveDelay) 75 | if receiveDelay >= config.ReceiveDelayWarnThreshold || receiveDelay <= 0 { 76 | log.Warn(receivingDomain, " has a ", receiveDelay, " delay in receiving events from ", sourceDomain) 77 | } 78 | 79 | metrics.RecordPingTime(sourceDomain, receivingDomain, pingTime) 80 | if pingTime >= config.PingTimeWarnThreshold || pingTime <= 0 { 81 | log.Warn("Ping time for ", sourceDomain, " -> ", receivingDomain, " is ", pingTime) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/tracker/tree.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | type RemoteTree map[string]RemoteTimings // domain : times 4 | type RemoteTimings map[string]Record // eventId : record 5 | 6 | func CalculateRemoteTree(domainName string, roomId string) (RemoteTree) { 7 | tree := RemoteTree{} 8 | domain := GetDomain(domainName) 9 | 10 | for _, remoteName := range domain.GetRemotes() { 11 | remote := domain.GetRemote(remoteName) 12 | timings := RemoteTimings{} 13 | 14 | room := remote.GetRoom(roomId) 15 | for _, r := range room.GetPings() { 16 | timings[r.EventId] = r.Record 17 | } 18 | 19 | tree[remoteName] = timings 20 | } 21 | 22 | return tree 23 | } 24 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/util/arrays.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func StrArrayContains(a []string, e string) bool { 4 | for _, i := range a { 5 | if i == e { 6 | return true 7 | } 8 | } 9 | 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/util/retry.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // Source: https://gist.github.com/sascha-andres/d1f11fb9bc6abc4f07b4118839b29d7f 9 | 10 | type StopRetry struct { 11 | error 12 | } 13 | 14 | func Retry(attempts int, sleep time.Duration, f func() error) error { 15 | if err := f(); err != nil { 16 | if s, ok := err.(StopRetry); ok { 17 | // Return the original error for later checking 18 | return s.error 19 | } 20 | 21 | if attempts--; attempts > 0 { 22 | // Add some randomness to prevent creating a Thundering Herd 23 | jitter := time.Duration(rand.Int63n(int64(sleep))) 24 | sleep = sleep + jitter/2 25 | 26 | time.Sleep(sleep) 27 | return Retry(attempts, 2*sleep, f) 28 | } 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/util/time.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "time" 4 | 5 | func NowMillis() int64 { 6 | return time.Now().UnixNano() / 1000000 7 | } 8 | 9 | func NowNano() int64 { 10 | return time.Now().UnixNano() 11 | } -------------------------------------------------------------------------------- /src/github.com/turt2live/matrix-monitor-bot/webserver/server.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "net/http" 5 | "github.com/turt2live/matrix-monitor-bot/config" 6 | "html/template" 7 | "github.com/sirupsen/logrus" 8 | "path" 9 | "github.com/turt2live/matrix-monitor-bot/matrix" 10 | "strings" 11 | "sort" 12 | "github.com/turt2live/matrix-monitor-bot/tracker" 13 | "fmt" 14 | "time" 15 | "github.com/turt2live/matrix-monitor-bot/util" 16 | ) 17 | 18 | type ComparedDomain struct { 19 | Domain string 20 | SendTime string 21 | ReceiveTime string 22 | AverageTime string 23 | HasSend bool 24 | HasReceive bool 25 | Status string // TODO: Replace with the HasSend/HasReceive and future IsOnline 26 | Description string 27 | } 28 | 29 | type CompareTemplateFields struct { 30 | SelfDomain string 31 | Domains []ComparedDomain 32 | FeaturedDomains []ComparedDomain 33 | RelativePath string // Needed for the layout.html 34 | } 35 | 36 | var mxClient *matrix.Client 37 | var baseHref string 38 | 39 | func InitServer(mux *http.ServeMux, client *matrix.Client) { 40 | mxClient = client 41 | 42 | fs := http.FileServer(http.Dir(config.Runtime.WebContentDir)) 43 | prefix := config.Get().Webserver.RelativePath 44 | if !strings.HasSuffix(prefix, "/") { 45 | prefix = prefix + "/" 46 | } 47 | if !strings.HasPrefix(prefix, "/") { 48 | prefix = "/" + prefix 49 | } 50 | baseHref = prefix 51 | prefix = prefix + "static/" 52 | mux.Handle(prefix, http.StripPrefix(prefix, fs)) 53 | 54 | mux.HandleFunc("/", serveCompare) 55 | } 56 | 57 | func serveCompare(w http.ResponseWriter, r *http.Request) { 58 | layout := path.Join(config.Runtime.WebContentDir, "layout.html") 59 | file := path.Join(config.Runtime.WebContentDir, "compare.html") 60 | 61 | fields := CompareTemplateFields{ 62 | SelfDomain: config.Get().Webserver.DefaultCompareDomain, 63 | Domains: make([]ComparedDomain, 0), 64 | FeaturedDomains: make([]ComparedDomain, 0), 65 | RelativePath: baseHref, 66 | } 67 | 68 | if fields.SelfDomain == "" { 69 | fields.SelfDomain = mxClient.Domain 70 | } 71 | 72 | us := tracker.GetDomain(fields.SelfDomain) 73 | domainsToUse := make([]string, 0) 74 | domainsToUse = append(domainsToUse, config.Get().Webserver.DefaultCompareToDomains...) 75 | if len(domainsToUse) == 0 { 76 | domainsToUse = tracker.GetDomainsExcept(fields.SelfDomain) 77 | 78 | for _, r := range us.GetRemotes() { 79 | exists := false 80 | for _, e := range domainsToUse { 81 | if e == r { 82 | exists = true 83 | break 84 | } 85 | } 86 | if !exists { 87 | domainsToUse = append(domainsToUse, r) 88 | } 89 | } 90 | 91 | sort.Strings(domainsToUse) 92 | } 93 | domainsToUse = append(domainsToUse, config.Get().Webserver.FeaturedCompareDomains...) 94 | 95 | handledDomains := make(map[string]bool) 96 | for _, domain := range domainsToUse { 97 | if handledDomains[domain] { 98 | continue 99 | } 100 | handledDomains[domain] = true 101 | 102 | remote := us.CompareTo(domain) 103 | avgTime := (time.Duration((remote.Send.Nanoseconds()+remote.Receive.Nanoseconds())/2) * time.Nanosecond).Truncate(time.Millisecond) 104 | description := fmt.Sprint(avgTime) 105 | status := "ok" 106 | if !remote.HasSend && !remote.HasReceive { 107 | status = "danger" 108 | description = "offline" 109 | } else if avgTime > config.WebWarnStatusThreshold || !remote.HasSend || !remote.HasReceive { 110 | status = "warn" 111 | } 112 | compDomain := ComparedDomain{ 113 | Domain: domain, 114 | SendTime: fmt.Sprint(remote.Send.Truncate(time.Millisecond)), 115 | ReceiveTime: fmt.Sprint(remote.Receive.Truncate(time.Millisecond)), 116 | HasSend: remote.HasSend, 117 | HasReceive: remote.HasReceive, 118 | AverageTime: fmt.Sprint(avgTime), 119 | Status: status, 120 | Description: description, 121 | } 122 | 123 | if util.StrArrayContains(config.Get().Webserver.FeaturedCompareDomains, domain) { 124 | fields.FeaturedDomains = append(fields.FeaturedDomains, compDomain) 125 | } else { 126 | fields.Domains = append(fields.Domains, compDomain) 127 | } 128 | } 129 | 130 | tmpl, err := template.ParseFiles(layout, file) 131 | if err != nil { 132 | logrus.Error(err) 133 | http.Error(w, "Failed to load template", http.StatusInternalServerError) 134 | return 135 | } 136 | 137 | err = tmpl.ExecuteTemplate(w, "layout", &fields) 138 | if err != nil { 139 | logrus.Error(err) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /vendor/manifest: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0, 3 | "dependencies": [ 4 | { 5 | "importpath": "github.com/beorn7/perks/quantile", 6 | "repository": "https://github.com/beorn7/perks", 7 | "revision": "3a771d992973f24aa725d07868b467d1ddfceafb", 8 | "branch": "master", 9 | "path": "/quantile" 10 | }, 11 | { 12 | "importpath": "github.com/golang/protobuf/proto", 13 | "repository": "https://github.com/golang/protobuf", 14 | "revision": "e09c5db296004fbe3f74490e84dcd62c3c5ddb1b", 15 | "branch": "master", 16 | "path": "/proto" 17 | }, 18 | { 19 | "importpath": "github.com/hashicorp/errwrap", 20 | "repository": "https://github.com/hashicorp/errwrap", 21 | "revision": "7554cd9344cec97297fa6649b055a8c98c2a1e55", 22 | "branch": "master" 23 | }, 24 | { 25 | "importpath": "github.com/hashicorp/go-multierror", 26 | "repository": "https://github.com/hashicorp/go-multierror", 27 | "revision": "b7773ae218740a7be65057fc60b366a49b538a44", 28 | "branch": "master" 29 | }, 30 | { 31 | "importpath": "github.com/lestrrat/go-file-rotatelogs", 32 | "repository": "https://github.com/lestrrat/go-file-rotatelogs", 33 | "revision": "d3151e2a480fdcd05fb97102f5310a47d96274c4", 34 | "branch": "master" 35 | }, 36 | { 37 | "importpath": "github.com/lestrrat/go-strftime", 38 | "repository": "https://github.com/lestrrat/go-strftime", 39 | "revision": "ba3bf9c1d0421aa146564a632931730344f1f9f1", 40 | "branch": "master" 41 | }, 42 | { 43 | "importpath": "github.com/matrix-org/gomatrix", 44 | "repository": "https://github.com/matrix-org/gomatrix", 45 | "revision": "a7fc80c8060c2544fe5d4dae465b584f8e9b4e27", 46 | "branch": "master" 47 | }, 48 | { 49 | "importpath": "github.com/matttproud/golang_protobuf_extensions/pbutil", 50 | "repository": "https://github.com/matttproud/golang_protobuf_extensions", 51 | "revision": "c12348ce28de40eed0136aa2b644d0ee0650e56c", 52 | "branch": "master", 53 | "path": "/pbutil" 54 | }, 55 | { 56 | "importpath": "github.com/patrickmn/go-cache", 57 | "repository": "https://github.com/patrickmn/go-cache", 58 | "revision": "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0", 59 | "branch": "master" 60 | }, 61 | { 62 | "importpath": "github.com/pkg/errors", 63 | "repository": "https://github.com/pkg/errors", 64 | "revision": "816c9085562cd7ee03e7f8188a1cfd942858cded", 65 | "branch": "master" 66 | }, 67 | { 68 | "importpath": "github.com/prometheus/client_golang/prometheus", 69 | "repository": "https://github.com/prometheus/client_golang", 70 | "revision": "e11c6ff8170beca9d5fd8b938e71165eeec53ac6", 71 | "branch": "master", 72 | "path": "/prometheus" 73 | }, 74 | { 75 | "importpath": "github.com/prometheus/client_golang/prometheus/promhttp", 76 | "repository": "https://github.com/prometheus/client_golang", 77 | "revision": "e11c6ff8170beca9d5fd8b938e71165eeec53ac6", 78 | "branch": "master", 79 | "path": "/prometheus/promhttp" 80 | }, 81 | { 82 | "importpath": "github.com/prometheus/client_model/go", 83 | "repository": "https://github.com/prometheus/client_model", 84 | "revision": "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c", 85 | "branch": "master", 86 | "path": "/go" 87 | }, 88 | { 89 | "importpath": "github.com/prometheus/common/expfmt", 90 | "repository": "https://github.com/prometheus/common", 91 | "revision": "d0f7cd64bda49e08b22ae8a730aa57aa0db125d6", 92 | "branch": "master", 93 | "path": "/expfmt" 94 | }, 95 | { 96 | "importpath": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg", 97 | "repository": "https://github.com/prometheus/common", 98 | "revision": "d0f7cd64bda49e08b22ae8a730aa57aa0db125d6", 99 | "branch": "master", 100 | "path": "/internal/bitbucket.org/ww/goautoneg" 101 | }, 102 | { 103 | "importpath": "github.com/prometheus/common/model", 104 | "repository": "https://github.com/prometheus/common", 105 | "revision": "d0f7cd64bda49e08b22ae8a730aa57aa0db125d6", 106 | "branch": "master", 107 | "path": "/model" 108 | }, 109 | { 110 | "importpath": "github.com/prometheus/procfs", 111 | "repository": "https://github.com/prometheus/procfs", 112 | "revision": "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e", 113 | "branch": "master" 114 | }, 115 | { 116 | "importpath": "github.com/rifflock/lfshook", 117 | "repository": "https://github.com/rifflock/lfshook", 118 | "revision": "62f5e0662816a462697d29f396fd19376d17f65b", 119 | "branch": "master" 120 | }, 121 | { 122 | "importpath": "github.com/sirupsen/logrus", 123 | "repository": "https://github.com/sirupsen/logrus", 124 | "revision": "778f2e774c725116edbc3d039dc0dfc1cc62aae8", 125 | "branch": "master" 126 | }, 127 | { 128 | "importpath": "golang.org/x/crypto/ssh/terminal", 129 | "repository": "https://go.googlesource.com/crypto", 130 | "revision": "d6449816ce06963d9d136eee5a56fca5b0616e7e", 131 | "branch": "master", 132 | "path": "/ssh/terminal" 133 | }, 134 | { 135 | "importpath": "golang.org/x/sys/unix", 136 | "repository": "https://go.googlesource.com/sys", 137 | "revision": "2281fa97ef7b0c26324634d5a22f04babdac8713", 138 | "branch": "master", 139 | "path": "/unix" 140 | }, 141 | { 142 | "importpath": "golang.org/x/sys/windows", 143 | "repository": "https://go.googlesource.com/sys", 144 | "revision": "2281fa97ef7b0c26324634d5a22f04babdac8713", 145 | "branch": "master", 146 | "path": "/windows" 147 | }, 148 | { 149 | "importpath": "gopkg.in/yaml.v2", 150 | "repository": "https://gopkg.in/yaml.v2", 151 | "revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183", 152 | "branch": "master" 153 | } 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /web/compare.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}Matrix Latency: {{.SelfDomain}}{{end}} 2 | 3 | {{define "body"}} 4 |
5 |

{{.SelfDomain}}

6 |
7 | {{range .FeaturedDomains}} 8 |
9 | {{.Domain}} 10 | {{.Description}} 11 | {{if ne .Status "danger"}} 12 | 13 | {{.Domain}}
14 | 15 | {{if .HasSend}}Send: {{.SendTime}} 16 | {{else}}Send: offline 17 | {{end}} 18 |
19 | 20 | {{if .HasReceive}}Receive: {{.ReceiveTime}} 21 | {{else}}Receive: offline 22 | {{end}} 23 |
24 | {{else}} 25 | 26 | {{.Domain}}
27 | No stats available 28 |
29 | {{end}} 30 |
31 | {{end}} 32 |
33 |
34 | {{range .Domains}} 35 |
36 | 37 | {{.Domain}} 38 | 39 | {{.Description}} 40 | 41 | {{if ne .Status "danger"}} 42 | 43 | {{if .HasSend}}Send: {{.SendTime}} 44 | {{else}}Send: offline 45 | {{end}} 46 |
47 | {{if .HasReceive}}Receive: {{.ReceiveTime}} 48 | {{else}}Receive: offline 49 | {{end}} 50 |
51 | {{else}} 52 | 53 | {{.Domain}}
54 | No stats available 55 |
56 | {{end}} 57 |
58 |
59 | {{end}} 60 |
61 |
62 | {{end}} -------------------------------------------------------------------------------- /web/fonts/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* Designed by Mat Douglas */ 2 | /* FontFace Generated by FontPro */ 3 | 4 | @font-face { 5 | font-family: 'WeblySleekUISemilight'; 6 | src: url('weblysleekuisl.eot'); 7 | src: url('weblysleekuisl.eot?#iefix') format('embedded-opentype'), 8 | url('weblysleekuisl.woff') format('woff'), 9 | url('weblysleekuisl.ttf') format('truetype'), 10 | url('weblysleekuisl.svg#WeblySleekUISemilight') format('svg'); 11 | } 12 | -------------------------------------------------------------------------------- /web/fonts/weblysleekuisl.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turt2live/matrix-monitor-bot/f055950b7c3b839f82b027f2181cad801c5e5f3f/web/fonts/weblysleekuisl.eot -------------------------------------------------------------------------------- /web/fonts/weblysleekuisl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 16 | 19 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 40 | 42 | 43 | 45 | 48 | 49 | 52 | 55 | 57 | 58 | 59 | 60 | 61 | 64 | 68 | 69 | 71 | 73 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 88 | 89 | 91 | 93 | 96 | 97 | 98 | 99 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 112 | 114 | 116 | 118 | 120 | 121 | 123 | 124 | 125 | 127 | 128 | 129 | 131 | 132 | 134 | 136 | 138 | 139 | 141 | 142 | 143 | 144 | 146 | 147 | 149 | 150 | 152 | 153 | 155 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /web/fonts/weblysleekuisl.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turt2live/matrix-monitor-bot/f055950b7c3b839f82b027f2181cad801c5e5f3f/web/fonts/weblysleekuisl.ttf -------------------------------------------------------------------------------- /web/fonts/weblysleekuisl.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turt2live/matrix-monitor-bot/f055950b7c3b839f82b027f2181cad801c5e5f3f/web/fonts/weblysleekuisl.woff -------------------------------------------------------------------------------- /web/img/grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turt2live/matrix-monitor-bot/f055950b7c3b839f82b027f2181cad801c5e5f3f/web/img/grey.png -------------------------------------------------------------------------------- /web/layout.html: -------------------------------------------------------------------------------- 1 | {{define "layout"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{template "title" .}} 10 | 11 | 12 | {{template "body" .}} 13 |

Background image retrieved from Subtle Patterns under CC BY-SA 3.0

14 | 15 | 16 | {{end}} -------------------------------------------------------------------------------- /web/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | background: url("img/grey.png") repeat; 4 | font-family: "Segoe UI", "WeblySleekUISemilight", sans-serif; 5 | } 6 | 7 | .circle-container { 8 | margin: auto; 9 | } 10 | 11 | .tile-container { 12 | position: relative; 13 | margin: auto; 14 | padding-top: 20px; 15 | } 16 | 17 | .card { 18 | position: relative; 19 | display: inline-block; 20 | width: 80vw; 21 | max-width: 800px; 22 | margin-top: 4vh; 23 | padding: 0 15px 15px; 24 | background-color: #fff; 25 | border-top: 5px solid #4aa3ff; 26 | box-shadow: 0 2px 6px hsla(0, 0%, 0%, 0.2); 27 | } 28 | 29 | .card.ok { 30 | border-color: #008000; 31 | } 32 | 33 | .card.warn { 34 | border-color: #ffa500; 35 | } 36 | 37 | .card.danger { 38 | border-color: #ff0000; 39 | } 40 | 41 | .tile { 42 | position: relative; 43 | display: inline-block; 44 | width: 200px; 45 | height: 30px; 46 | padding: 5px; 47 | margin: 0 20px 0; 48 | vertical-align: middle; 49 | text-align: left; 50 | } 51 | 52 | .tile .indicator { 53 | display: inline-block; 54 | width: 10px; 55 | height: 10px; 56 | border-radius: 10px; 57 | margin-right: 5px; 58 | } 59 | 60 | .tile .indicator.ok { 61 | box-shadow: #008000dd 0 0 13px; 62 | background-color: #008000; 63 | } 64 | 65 | .tile .indicator.warn { 66 | box-shadow: #ffa500dd 0 0 13px; 67 | background-color: #ffa500; 68 | } 69 | 70 | .tile .indicator.danger { 71 | box-shadow: #ff0000dd 0 0 13px; 72 | background-color: #ff0000; 73 | } 74 | 75 | .tile .domain { 76 | white-space: nowrap; 77 | overflow: hidden; 78 | text-overflow: ellipsis; 79 | } 80 | 81 | .tile .time { 82 | position: relative; 83 | float: right; 84 | color: #CCC; 85 | padding-bottom: 2px; 86 | } 87 | 88 | .circle { 89 | position: relative; 90 | display: inline-block; 91 | width: 120px; 92 | height: 120px; 93 | border-radius: 120px; 94 | text-align: center; 95 | vertical-align: middle; 96 | margin: 5px; 97 | } 98 | 99 | .circle.ok { 100 | border: 2px solid #008000; 101 | } 102 | 103 | .circle.warn { 104 | border: 2px solid #ffa500; 105 | } 106 | 107 | .circle.danger { 108 | border: 2px solid #ff0000; 109 | } 110 | 111 | .text-ok { 112 | color: #008000; 113 | } 114 | 115 | .text-warn { 116 | color: #ffa500; 117 | } 118 | 119 | .text-danger { 120 | color: #ff0000; 121 | } 122 | 123 | .circle .domain { 124 | position: absolute; 125 | width: calc(100% - 12px); 126 | padding: 0 6px; 127 | top: calc(50% - 16px); 128 | left: 0; 129 | font-size: 18px; 130 | white-space: nowrap; 131 | overflow: hidden; 132 | text-overflow: ellipsis; 133 | vertical-align: bottom; 134 | } 135 | 136 | .circle .time { 137 | position: absolute; 138 | width: 100%; 139 | top: calc(50% + 18px); 140 | left: 0; 141 | font-size: 13px; 142 | } 143 | 144 | .tooltip { 145 | visibility: hidden; 146 | width: 120px; 147 | font-size: 13px; 148 | background-color: #000; 149 | color: #fff; 150 | text-align: center; 151 | border-radius: 6px; 152 | padding: 3px 0; 153 | position: absolute; 154 | top: 100%; 155 | left: 50%; 156 | margin-left: -60px; 157 | z-index: 1; 158 | } 159 | 160 | .tooltip::after { 161 | content: " "; 162 | position: absolute; 163 | bottom: 100%; 164 | left: 50%; 165 | margin-left: -5px; 166 | border: 5px solid transparent; 167 | border-bottom-color: black; 168 | } 169 | 170 | .circle:hover .tooltip { 171 | visibility: visible; 172 | } 173 | 174 | .tile .time:hover .tooltip { 175 | visibility: visible; 176 | } 177 | 178 | .attribution { 179 | position: absolute; 180 | bottom: 0; 181 | right: 0; 182 | width: 100%; 183 | text-align: right; 184 | font-size: 12px; 185 | color: #ccc; 186 | margin: 10px; 187 | } 188 | 189 | .attribution a { 190 | color: #ccc; 191 | } --------------------------------------------------------------------------------