├── .dockerignore ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── Gruntfile.js ├── LICENSE ├── README.md ├── debugging ├── grafana.ini └── start_grafana.sh ├── dist ├── README.md ├── css │ └── query-editor.css ├── datasource.js ├── img │ ├── MongoDB_Gray_Logo_FullColor_RGB-01.jpg │ ├── sample_dashboard.png │ ├── sample_datasource.png │ ├── sample_query.png │ ├── sample_template.png │ └── table_panel.png ├── module.js ├── partials │ ├── annotations.editor.html │ ├── config.html │ ├── query.editor.html │ └── query.options.html ├── plugin.json ├── query_ctrl.js ├── server │ ├── config │ │ └── default.json │ ├── mongodb-grafana-proxy.plist │ └── mongodb-proxy.js └── test │ ├── datasource.js │ ├── module.js │ ├── query_ctrl.js │ └── spec │ ├── datasource_spec.js │ └── test-main.js ├── examples ├── RPI Mongo Bucket - Atlas CS.json ├── RPI Mongo Bucket - Atlas Temp.json └── Sensor Value Counts - Atlas.json ├── package-lock.json ├── package.json ├── server ├── config │ └── default.json ├── mongodb-grafana-proxy.plist └── mongodb-proxy.js ├── spec ├── datasource_spec.js └── test-main.js └── src ├── css └── query-editor.css ├── datasource.js ├── img ├── MongoDB_Gray_Logo_FullColor_RGB-01.jpg ├── sample_dashboard.png ├── sample_datasource.png ├── sample_query.png ├── sample_template.png └── table_panel.png ├── module.js ├── partials ├── annotations.editor.html ├── config.html ├── query.editor.html └── query.options.html ├── plugin.json └── query_ctrl.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | dist/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .gradle/ 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/server/mongodb-proxy.js", 12 | "cwd" : "${workspaceFolder}/server" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "grafana", 4 | "timeserie" 5 | ] 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine AS build 2 | WORKDIR /build 3 | COPY package*.json ./ 4 | RUN npm ci 5 | 6 | COPY Gruntfile.js . 7 | COPY server ./server 8 | RUN npm run build 9 | 10 | 11 | 12 | FROM node:16-alpine 13 | ENV NODE_ENV=production 14 | WORKDIR /app 15 | 16 | COPY package*.json ./ 17 | COPY --from=build /build/node_modules ./node_modules 18 | RUN npm install --omit=dev 19 | 20 | COPY --from=build /build/dist/server /app 21 | 22 | EXPOSE 3333 23 | CMD ["node" ,"mongodb-proxy.js"] 24 | 25 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | require('load-grunt-tasks')(grunt); 4 | 5 | grunt.loadNpmTasks('grunt-contrib-clean'); 6 | 7 | grunt.initConfig({ 8 | 9 | clean: ["dist"], 10 | 11 | copy: { 12 | src_to_dist: { 13 | cwd: 'src', 14 | expand: true, 15 | src: ['**/*', '!**/*.js', '!**/*.scss'], 16 | dest: 'dist' 17 | }, 18 | server_to_dist: { 19 | cwd: 'server', 20 | expand: true, 21 | src: ['**/*'], 22 | dest: 'dist/server' 23 | }, 24 | pluginDef: { 25 | expand: true, 26 | src: ['README.md'], 27 | dest: 'dist' 28 | } 29 | }, 30 | 31 | watch: { 32 | rebuild_all: { 33 | files: ['src/**/*'], 34 | tasks: ['default'], 35 | options: {spawn: false} 36 | } 37 | }, 38 | 39 | babel: { 40 | options: { 41 | sourceMap: process.env.NODE_ENV === "development", 42 | presets: ['es2015'] 43 | }, 44 | dist: { 45 | options: { 46 | plugins: ['transform-es2015-modules-systemjs', 'transform-es2015-for-of'] 47 | }, 48 | files: [{ 49 | cwd: 'src', 50 | expand: true, 51 | src: ['**/*.js'], 52 | dest: 'dist', 53 | ext:'.js' 54 | }] 55 | }, 56 | distTestNoSystemJs: { 57 | files: [{ 58 | cwd: 'src', 59 | expand: true, 60 | src: ['**/*.js'], 61 | dest: 'dist/test', 62 | ext:'.js' 63 | }] 64 | }, 65 | distTestsSpecsNoSystemJs: { 66 | files: [{ 67 | expand: true, 68 | cwd: 'spec', 69 | src: ['**/*.js'], 70 | dest: 'dist/test/spec', 71 | ext:'.js' 72 | }] 73 | } 74 | }, 75 | 76 | mochaTest: { 77 | test: { 78 | options: { 79 | reporter: 'spec' 80 | }, 81 | src: ['dist/test/spec/test-main.js', 'dist/test/spec/*_spec.js'] 82 | } 83 | } 84 | }); 85 | 86 | grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:server_to_dist', 'copy:pluginDef', 'babel', 'mochaTest']); 87 | }; 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 JamesOsgood 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MongoDB datasource for Grafana 2 | 3 | ## Features 4 | Allows MongoDB to be used as a data source for Grafana by providing a proxy to convert the Grafana Data source [API](http://docs.grafana.org/plugins/developing/datasources/) into MongoDB aggregation queries 5 | 6 | ## Requirements 7 | 8 | * **Grafana** > 3.x.x 9 | * **MongoDB** > 3.4.x 10 | 11 | ## Installation 12 | 13 | ### Install the Grafana plugin components 14 | 15 | * Copy the whole mongodb-grafana dir into the Grafana plugins dir ( /usr/local/var/lib/grafana/plugins ) 16 | * Restart the Grafana server. If installed via Homebrew, this will be `brew services restart grafana` 17 | 18 | ### Install and Start the MongoDB proxy server 19 | 20 | * Open a command prompt in the mongodb-grafana directory 21 | * Run `npm install` to install the node.js dependencies 22 | * Run `npm run server` to start the REST API proxy to MongoDB. By default, the server listens on http://localhost:3333 23 | 24 | ## Examples 25 | 26 | Create a new data source of type MongoDB as shown below. The MongoDB details are : 27 | 28 | * **MongoDB URL** - `mongodb://rpiread:rpiread@rpi-sensor-data-shard-00-00-ifxxs.mongodb.net:27017,rpi-sensor-data-shard-00-01-ifxxs.mongodb.net:27017,rpi-sensor-data-shard-00-02-ifxxs.mongodb.net:27017/test?ssl=true&replicaSet=rpi-sensor-data-shard-0&authSource=admin` 29 | * **MongoDB Database** - `rpi` 30 | 31 | Sample Data Source 32 | 33 | Then save the data source 34 | 35 | #### Example 1 - Simple aggregate to rename fields 36 | 37 | Import the dashboard in `examples\RPI MongoDB - Atlas.json` 38 | 39 | This should show a graph of light sensor values from a Raspberry PI with an [EnviroPHAT](https://thepihut.com/products/enviro-phat) board feeding readings every minute into a MongoDB Atlas database. 40 | 41 | Sample Dashboard 42 | 43 | Clicking on the title of the graph allows you to see the aggregation query being run against the 'RPI Atlas' data source 44 | 45 | Sample Query 46 | 47 | The query here is 48 | 49 | ```javascript 50 | db.sensor_value.aggregate ( [ 51 | { "$match" : { "sensor_type" : "$sensor", "host_name" : "$host", "ts" : { "$gte" : "$from", "$lte" : "$to" } } }, 52 | {"$sort" : {"ts" : 1}}, 53 | {"$project" : { "name" : "value", "value" : "$sensor_value", "ts" : "$ts", "_id" : 0} } ]) 54 | ``` 55 | 56 | The API is expecting back documents with the following fields 57 | 58 | * `name` - Name of the series ( will be displayed on the graph) 59 | * `value` - The float value of the point 60 | * `ts` - The time of the point as a BSON date 61 | 62 | These documents are then converted into the [Grafana API](http://docs.grafana.org/plugins/developing/datasources/) 63 | 64 | `$from` and `$to` are expanded by the plugin as BSON dates based on the range settings on the UI. 65 | 66 | ## Template Variables 67 | 68 | `$sensor` and `$host` are template variables that are filled in by Grafana based on the drop down. The sample template queries are shown below. They expect documents to be returned with a single `_id` field. 69 | 70 | 71 | Sample Templates 72 | 73 | #### Example 2 - Using $bucketAuto to push data point aggregation to the server 74 | 75 | Grafana tells the backend server the date range along with the size of the buckets that should be used to calculate points. Therefore it's possible to use the MongoDB aggregation operator [$bucketAuto](https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/) to automatically bucket the data points into display points. To support this the backend provides the `$dateBucketCount` macro so that queries such as the one below can be written 76 | 77 | ```javascript 78 | db.sensor_value.aggregate( [ 79 | { "$match" : { "sensor_type" : "$sensor", "host_name" : "$host" , "ts" : { "$gte" : "$from", "$lt" : "$to" }}}, 80 | { "$bucketAuto" : { "groupBy" : "$ts", 81 | "buckets" : "$dateBucketCount", 82 | "output" : { "maxValue" : { "$max" : "$sensor_value" } } } }, 83 | { "$project" : { "name" : "value", "value" : "$maxValue", "ts" : "$_id.min", "_id" : 0 } } ] ) 84 | ``` 85 | Note that ```_id``` field of the bucketAuto output contains the start and end of the bucket so we can use that as the ```ts``` value 86 | 87 | The dashboard in `examples\RPI MongoDB Bucket - Atlas.json` shows this. 88 | 89 | #### Example 3 - Using a Tabel Panel 90 | 91 | Table Panel 92 | 93 | Table panels are now supported with queries of the form 94 | 95 | ```javascript 96 | db.sensor_value.aggregate( 97 | [ 98 | { "$match" : { "ts" : { "$gte" : "$from", "$lt" : "$to" }}}, 99 | { "$group": { "_id": { "sensor_name" : "$sensor_name", "sensor_type" : "$sensor_type" }, "cnt" : { "$sum" : 1 }, "ts" : { "$max" : "$ts" } } }, 100 | { "$project": { "name" : { "$concat" : ["$_id.sensor_name",":","$_id.sensor_type" ]}, "value" : "$cnt", "ts" : 1, "_id" : 0} } 101 | ]) 102 | ``` 103 | 104 | The dashboard in `examples\Sensor Values Count - Atlas.json` shows this. 105 | 106 | ## Running the proxy as a service on a Mac 107 | 108 | * Install [forever-mac](https://www.npmjs.com/package/forever-mac) 109 | * Copy server/mongodb-grafana-proxy.plist to ~/Library/LaunchAgents 110 | * run `launchctl load mongodb-grafana-proxy` from ~/Library/LaunchAgents 111 | 112 | This launch ctrl plist runs the node script via forever. To check it's running, use `forever list`. Logs go into /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server 113 | 114 | ## Development 115 | 116 | To run grafana against a dev version of the plugin on a mac using grafana installed via Homebrew 117 | 118 | * Stop the grafana service `brew services stop grafana` 119 | * Open a command prompt in /debugging 120 | * Run ./start_grafana.sh 121 | * Alter code 122 | * npm run build to build the UI 123 | * Developer tools -> empty cache and hard reload 124 | 125 | Note 126 | 127 | * Homebrew grafana versions in /usr/local/Cellar 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /debugging/grafana.ini: -------------------------------------------------------------------------------- 1 | ##################### Grafana Configuration Example ##################### 2 | # 3 | # Everything has defaults so you only need to uncomment things you want to 4 | # change 5 | 6 | # possible values : production, development 7 | ; app_mode = production 8 | 9 | # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty 10 | ; instance_name = ${HOSTNAME} 11 | 12 | #################################### Paths #################################### 13 | [paths] 14 | # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) 15 | # 16 | data = /usr/local/var/lib/grafana 17 | # 18 | # Directory where grafana can store logs 19 | # 20 | logs = ./logs 21 | # 22 | # Directory where grafana will automatically scan and look for plugins 23 | # 24 | plugins = /Users/james/code/github/grafana/plugins 25 | 26 | # 27 | #################################### Server #################################### 28 | [server] 29 | # Protocol (http, https, socket) 30 | ;protocol = http 31 | 32 | # The ip address to bind to, empty will bind to all interfaces 33 | ;http_addr = 34 | 35 | # The http port to use 36 | ;http_port = 3000 37 | 38 | # The public facing domain name used to access grafana from a browser 39 | ;domain = localhost 40 | 41 | # Redirect to correct domain if host header does not match domain 42 | # Prevents DNS rebinding attacks 43 | ;enforce_domain = false 44 | 45 | # The full public facing url you use in browser, used for redirects and emails 46 | # If you use reverse proxy and sub path specify full url (with sub path) 47 | ;root_url = http://localhost:3000 48 | 49 | # Log web requests 50 | ;router_logging = false 51 | 52 | # the path relative working path 53 | ;static_root_path = public 54 | 55 | # enable gzip 56 | ;enable_gzip = false 57 | 58 | # https certs & key file 59 | ;cert_file = 60 | ;cert_key = 61 | 62 | # Unix socket path 63 | ;socket = 64 | 65 | #################################### Database #################################### 66 | [database] 67 | # You can configure the database connection by specifying type, host, name, user and password 68 | # as seperate properties or as on string using the url propertie. 69 | 70 | # Either "mysql", "postgres" or "sqlite3", it's your choice 71 | type = mysql 72 | host = 127.0.0.1:3306 73 | ;name = grafana 74 | ;user = root 75 | # If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" 76 | ;password = 77 | 78 | # Use either URL or the previous fields to configure the database 79 | # Example: mysql://user:secret@host:port/database 80 | ;url = 81 | 82 | # For "postgres" only, either "disable", "require" or "verify-full" 83 | ;ssl_mode = disable 84 | 85 | # For "sqlite3" only, path relative to data_path setting 86 | ;path = grafana.db 87 | 88 | # Max idle conn setting default is 2 89 | ;max_idle_conn = 2 90 | 91 | # Max conn setting default is 0 (mean not set) 92 | ;max_open_conn = 93 | 94 | 95 | #################################### Session #################################### 96 | [session] 97 | # Either "memory", "file", "redis", "mysql", "postgres", default is "file" 98 | ;provider = file 99 | 100 | # Provider config options 101 | # memory: not have any config yet 102 | # file: session dir path, is relative to grafana data_path 103 | # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` 104 | # mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` 105 | # postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable 106 | ;provider_config = sessions 107 | 108 | # Session cookie name 109 | ;cookie_name = grafana_sess 110 | 111 | # If you use session in https only, default is false 112 | ;cookie_secure = false 113 | 114 | # Session life time, default is 86400 115 | ;session_life_time = 86400 116 | 117 | #################################### Data proxy ########################### 118 | [dataproxy] 119 | 120 | # This enables data proxy logging, default is false 121 | ;logging = false 122 | 123 | 124 | #################################### Analytics #################################### 125 | [analytics] 126 | # Server reporting, sends usage counters to stats.grafana.org every 24 hours. 127 | # No ip addresses are being tracked, only simple counters to track 128 | # running instances, dashboard and error counts. It is very helpful to us. 129 | # Change this option to false to disable reporting. 130 | ;reporting_enabled = true 131 | 132 | # Set to false to disable all checks to https://grafana.net 133 | # for new vesions (grafana itself and plugins), check is used 134 | # in some UI views to notify that grafana or plugin update exists 135 | # This option does not cause any auto updates, nor send any information 136 | # only a GET request to http://grafana.com to get latest versions 137 | ;check_for_updates = true 138 | 139 | # Google Analytics universal tracking code, only enabled if you specify an id here 140 | ;google_analytics_ua_id = 141 | 142 | #################################### Security #################################### 143 | [security] 144 | # default admin user, created on startup 145 | ;admin_user = admin 146 | 147 | # default admin password, can be changed before first start of grafana, or in profile settings 148 | ;admin_password = admin 149 | 150 | # used for signing 151 | ;secret_key = SW2YcwTIb9zpOOhoPsMm 152 | 153 | # Auto-login remember days 154 | ;login_remember_days = 7 155 | ;cookie_username = grafana_user 156 | ;cookie_remember_name = grafana_remember 157 | 158 | # disable gravatar profile images 159 | ;disable_gravatar = false 160 | 161 | # data source proxy whitelist (ip_or_domain:port separated by spaces) 162 | ;data_source_proxy_whitelist = 163 | 164 | [snapshots] 165 | # snapshot sharing options 166 | ;external_enabled = true 167 | ;external_snapshot_url = https://snapshots-origin.raintank.io 168 | ;external_snapshot_name = Publish to snapshot.raintank.io 169 | 170 | # remove expired snapshot 171 | ;snapshot_remove_expired = true 172 | 173 | # remove snapshots after 90 days 174 | ;snapshot_TTL_days = 90 175 | 176 | #################################### Users #################################### 177 | [users] 178 | # disable user signup / registration 179 | ;allow_sign_up = true 180 | 181 | # Allow non admin users to create organizations 182 | ;allow_org_create = true 183 | 184 | # Set to true to automatically assign new users to the default organization (id 1) 185 | ;auto_assign_org = true 186 | 187 | # Default role new users will be automatically assigned (if disabled above is set to true) 188 | ;auto_assign_org_role = Viewer 189 | 190 | # Background text for the user field on the login page 191 | ;login_hint = email or username 192 | 193 | # Default UI theme ("dark" or "light") 194 | ;default_theme = dark 195 | 196 | # External user management, these options affect the organization users view 197 | ;external_manage_link_url = 198 | ;external_manage_link_name = 199 | ;external_manage_info = 200 | 201 | [auth] 202 | # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false 203 | ;disable_login_form = false 204 | 205 | # Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false 206 | ;disable_signout_menu = false 207 | 208 | #################################### Anonymous Auth ########################## 209 | [auth.anonymous] 210 | # enable anonymous access 211 | ;enabled = false 212 | 213 | # specify organization name that should be used for unauthenticated users 214 | ;org_name = Main Org. 215 | 216 | # specify role for unauthenticated users 217 | ;org_role = Viewer 218 | 219 | #################################### Github Auth ########################## 220 | [auth.github] 221 | ;enabled = false 222 | ;allow_sign_up = true 223 | ;client_id = some_id 224 | ;client_secret = some_secret 225 | ;scopes = user:email,read:org 226 | ;auth_url = https://github.com/login/oauth/authorize 227 | ;token_url = https://github.com/login/oauth/access_token 228 | ;api_url = https://api.github.com/user 229 | ;team_ids = 230 | ;allowed_organizations = 231 | 232 | #################################### Google Auth ########################## 233 | [auth.google] 234 | ;enabled = false 235 | ;allow_sign_up = true 236 | ;client_id = some_client_id 237 | ;client_secret = some_client_secret 238 | ;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email 239 | ;auth_url = https://accounts.google.com/o/oauth2/auth 240 | ;token_url = https://accounts.google.com/o/oauth2/token 241 | ;api_url = https://www.googleapis.com/oauth2/v1/userinfo 242 | ;allowed_domains = 243 | 244 | #################################### Generic OAuth ########################## 245 | [auth.generic_oauth] 246 | ;enabled = false 247 | ;name = OAuth 248 | ;allow_sign_up = true 249 | ;client_id = some_id 250 | ;client_secret = some_secret 251 | ;scopes = user:email,read:org 252 | ;auth_url = https://foo.bar/login/oauth/authorize 253 | ;token_url = https://foo.bar/login/oauth/access_token 254 | ;api_url = https://foo.bar/user 255 | ;team_ids = 256 | ;allowed_organizations = 257 | 258 | #################################### Grafana.com Auth #################### 259 | [auth.grafana_com] 260 | ;enabled = false 261 | ;allow_sign_up = true 262 | ;client_id = some_id 263 | ;client_secret = some_secret 264 | ;scopes = user:email 265 | ;allowed_organizations = 266 | 267 | #################################### Auth Proxy ########################## 268 | [auth.proxy] 269 | ;enabled = false 270 | ;header_name = X-WEBAUTH-USER 271 | ;header_property = username 272 | ;auto_sign_up = true 273 | ;ldap_sync_ttl = 60 274 | ;whitelist = 192.168.1.1, 192.168.2.1 275 | 276 | #################################### Basic Auth ########################## 277 | [auth.basic] 278 | ;enabled = true 279 | 280 | #################################### Auth LDAP ########################## 281 | [auth.ldap] 282 | ;enabled = false 283 | ;config_file = /etc/grafana/ldap.toml 284 | ;allow_sign_up = true 285 | 286 | #################################### SMTP / Emailing ########################## 287 | [smtp] 288 | ;enabled = false 289 | ;host = localhost:25 290 | ;user = 291 | # If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" 292 | ;password = 293 | ;cert_file = 294 | ;key_file = 295 | ;skip_verify = false 296 | ;from_address = admin@grafana.localhost 297 | ;from_name = Grafana 298 | 299 | [emails] 300 | ;welcome_email_on_sign_up = false 301 | 302 | #################################### Logging ########################## 303 | [log] 304 | # Either "console", "file", "syslog". Default is console and file 305 | # Use space to separate multiple modes, e.g. "console file" 306 | ;mode = console file 307 | 308 | # Either "debug", "info", "warn", "error", "critical", default is "info" 309 | level = debug 310 | 311 | # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug 312 | ;filters = 313 | 314 | 315 | # For "console" mode only 316 | [log.console] 317 | ;level = 318 | 319 | # log line format, valid options are text, console and json 320 | ;format = console 321 | 322 | # For "file" mode only 323 | [log.file] 324 | ;level = 325 | 326 | # log line format, valid options are text, console and json 327 | ;format = text 328 | 329 | # This enables automated log rotate(switch of following options), default is true 330 | ;log_rotate = true 331 | 332 | # Max line number of single file, default is 1000000 333 | ;max_lines = 1000000 334 | 335 | # Max size shift of single file, default is 28 means 1 << 28, 256MB 336 | ;max_size_shift = 28 337 | 338 | # Segment log daily, default is true 339 | ;daily_rotate = true 340 | 341 | # Expired days of log file(delete after max days), default is 7 342 | ;max_days = 7 343 | 344 | [log.syslog] 345 | ;level = 346 | 347 | # log line format, valid options are text, console and json 348 | ;format = text 349 | 350 | # Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used. 351 | ;network = 352 | ;address = 353 | 354 | # Syslog facility. user, daemon and local0 through local7 are valid. 355 | ;facility = 356 | 357 | # Syslog tag. By default, the process' argv[0] is used. 358 | ;tag = 359 | 360 | 361 | #################################### AMQP Event Publisher ########################## 362 | [event_publisher] 363 | ;enabled = false 364 | ;rabbitmq_url = amqp://localhost/ 365 | ;exchange = grafana_events 366 | 367 | ;#################################### Dashboard JSON files ########################## 368 | [dashboards.json] 369 | ;enabled = false 370 | ;path = /var/lib/grafana/dashboards 371 | 372 | #################################### Alerting ############################ 373 | [alerting] 374 | # Disable alerting engine & UI features 375 | ;enabled = true 376 | # Makes it possible to turn off alert rule execution but alerting UI is visible 377 | ;execute_alerts = true 378 | 379 | #################################### Internal Grafana Metrics ########################## 380 | # Metrics available at HTTP API Url /api/metrics 381 | [metrics] 382 | # Disable / Enable internal metrics 383 | ;enabled = true 384 | 385 | # Publish interval 386 | ;interval_seconds = 10 387 | 388 | # Send internal metrics to Graphite 389 | [metrics.graphite] 390 | # Enable by setting the address setting (ex localhost:2003) 391 | ;address = 392 | ;prefix = prod.grafana.%(instance_name)s. 393 | 394 | #################################### Grafana.com integration ########################## 395 | # Url used to to import dashboards directly from Grafana.com 396 | [grafana_com] 397 | ;url = https://grafana.com 398 | 399 | #################################### External image storage ########################## 400 | [external_image_storage] 401 | # Used for uploading images to public servers so they can be included in slack/email messages. 402 | # you can choose between (s3, webdav) 403 | ;provider = 404 | 405 | [external_image_storage.s3] 406 | ;bucket_url = 407 | ;access_key = 408 | ;secret_key = 409 | 410 | [external_image_storage.webdav] 411 | ;url = 412 | ;public_url = 413 | ;username = 414 | ;password = 415 | -------------------------------------------------------------------------------- /debugging/start_grafana.sh: -------------------------------------------------------------------------------- 1 | /usr/local/opt/grafana/bin/grafana-server --config /Users/james/code/github/grafana/plugins/mongodb-grafana/debugging/grafana.ini --homepath /usr/local/opt/grafana/share/grafana -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # MongoDB datasource for Grafana 2 | 3 | ## Features 4 | Allows MongoDB to be used as a data source for Grafana by providing a proxy to convert the Grafana Data source [API](http://docs.grafana.org/plugins/developing/datasources/) into MongoDB aggregation queries 5 | 6 | ## Requirements 7 | 8 | * **Grafana** > 3.x.x 9 | * **MongoDB** > 3.4.x 10 | 11 | ## Installation 12 | 13 | ### Install the Grafana plugin components 14 | 15 | * Copy the whole mongodb-grafana dir into the Grafana plugins dir ( /usr/local/var/lib/grafana/plugins ) 16 | * Restart the Grafana server. If installed via Homebrew, this will be `brew services restart grafana` 17 | 18 | ### Install and Start the MongoDB proxy server 19 | 20 | * Open a command prompt in the mongodb-grafana directory 21 | * Run `npm install` to install the node.js dependencies 22 | * Run `npm run server` to start the REST API proxy to MongoDB. By default, the server listens on http://localhost:3333 23 | 24 | ## Examples 25 | 26 | Create a new data source of type MongoDB as shown below. The MongoDB details are : 27 | 28 | * **MongoDB URL** - `mongodb://rpiread:rpiread@rpi-sensor-data-shard-00-00-ifxxs.mongodb.net:27017,rpi-sensor-data-shard-00-01-ifxxs.mongodb.net:27017,rpi-sensor-data-shard-00-02-ifxxs.mongodb.net:27017/test?ssl=true&replicaSet=rpi-sensor-data-shard-0&authSource=admin` 29 | * **MongoDB Database** - `rpi` 30 | 31 | Sample Data Source 32 | 33 | Then save the data source 34 | 35 | #### Example 1 - Simple aggregate to rename fields 36 | 37 | Import the dashboard in `examples\RPI MongoDB - Atlas.json` 38 | 39 | This should show a graph of light sensor values from a Raspberry PI with an [EnviroPHAT](https://thepihut.com/products/enviro-phat) board feeding readings every minute into a MongoDB Atlas database. 40 | 41 | Sample Dashboard 42 | 43 | Clicking on the title of the graph allows you to see the aggregation query being run against the 'RPI Atlas' data source 44 | 45 | Sample Query 46 | 47 | The query here is 48 | 49 | ```javascript 50 | db.sensor_value.aggregate ( [ 51 | { "$match" : { "sensor_type" : "$sensor", "host_name" : "$host", "ts" : { "$gte" : "$from", "$lte" : "$to" } } }, 52 | {"$sort" : {"ts" : 1}}, 53 | {"$project" : { "name" : "value", "value" : "$sensor_value", "ts" : "$ts", "_id" : 0} } ]) 54 | ``` 55 | 56 | The API is expecting back documents with the following fields 57 | 58 | * `name` - Name of the series ( will be displayed on the graph) 59 | * `value` - The float value of the point 60 | * `ts` - The time of the point as a BSON date 61 | 62 | These documents are then converted into the [Grafana API](http://docs.grafana.org/plugins/developing/datasources/) 63 | 64 | `$from` and `$to` are expanded by the plugin as BSON dates based on the range settings on the UI. 65 | 66 | ## Template Variables 67 | 68 | `$sensor` and `$host` are template variables that are filled in by Grafana based on the drop down. The sample template queries are shown below. They expect documents to be returned with a single `_id` field. 69 | 70 | 71 | Sample Templates 72 | 73 | #### Example 2 - Using $bucketAuto to push data point aggregation to the server 74 | 75 | Grafana tells the backend server the date range along with the size of the buckets that should be used to calculate points. Therefore it's possible to use the MongoDB aggregation operator [$bucketAuto](https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/) to automatically bucket the data points into display points. To support this the backend provides the `$dateBucketCount` macro so that queries such as the one below can be written 76 | 77 | ```javascript 78 | db.sensor_value.aggregate( [ 79 | { "$match" : { "sensor_type" : "$sensor", "host_name" : "$host" , "ts" : { "$gte" : "$from", "$lt" : "$to" }}}, 80 | { "$bucketAuto" : { "groupBy" : "$ts", 81 | "buckets" : "$dateBucketCount", 82 | "output" : { "maxValue" : { "$max" : "$sensor_value" } } } }, 83 | { "$project" : { "name" : "value", "value" : "$maxValue", "ts" : "$_id.min", "_id" : 0 } } ] ) 84 | ``` 85 | Note that ```_id``` field of the bucketAuto output contains the start and end of the bucket so we can use that as the ```ts``` value 86 | 87 | The dashboard in `examples\RPI MongoDB Bucket - Atlas.json` shows this. 88 | 89 | #### Example 3 - Using a Tabel Panel 90 | 91 | Table Panel 92 | 93 | Table panels are now supported with queries of the form 94 | 95 | ```javascript 96 | db.sensor_value.aggregate( 97 | [ 98 | { "$match" : { "ts" : { "$gte" : "$from", "$lt" : "$to" }}}, 99 | { "$group": { "_id": { "sensor_name" : "$sensor_name", "sensor_type" : "$sensor_type" }, "cnt" : { "$sum" : 1 }, "ts" : { "$max" : "$ts" } } }, 100 | { "$project": { "name" : { "$concat" : ["$_id.sensor_name",":","$_id.sensor_type" ]}, "value" : "$cnt", "ts" : 1, "_id" : 0} } 101 | ]) 102 | ``` 103 | 104 | The dashboard in `examples\Sensor Values Count - Atlas.json` shows this. 105 | 106 | ## Running the proxy as a service on a Mac 107 | 108 | * Install [forever-mac](https://www.npmjs.com/package/forever-mac) 109 | * Copy server/mongodb-grafana-proxy.plist to ~/Library/LaunchAgents 110 | * run `launchctl load mongodb-grafana-proxy` from ~/Library/LaunchAgents 111 | 112 | This launch ctrl plist runs the node script via forever. To check it's running, use `forever list`. Logs go into /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server 113 | 114 | ## Development 115 | 116 | To run grafana against a dev version of the plugin on a mac using grafana installed via Homebrew 117 | 118 | * Stop the grafana service `brew services stop grafana` 119 | * Open a command prompt in /debugging 120 | * Run ./start_grafana.sh 121 | * Alter code 122 | * npm run build to build the UI 123 | * Developer tools -> empty cache and hard reload 124 | 125 | Note 126 | 127 | * Homebrew grafana versions in /usr/local/Cellar 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /dist/css/query-editor.css: -------------------------------------------------------------------------------- 1 | .generic-datasource-query-row .query-keyword { 2 | width: 75px; 3 | } -------------------------------------------------------------------------------- /dist/datasource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | System.register(['lodash'], function (_export, _context) { 4 | "use strict"; 5 | 6 | var _, _createClass, GenericDatasource; 7 | 8 | function _classCallCheck(instance, Constructor) { 9 | if (!(instance instanceof Constructor)) { 10 | throw new TypeError("Cannot call a class as a function"); 11 | } 12 | } 13 | 14 | return { 15 | setters: [function (_lodash) { 16 | _ = _lodash.default; 17 | }], 18 | execute: function () { 19 | _createClass = function () { 20 | function defineProperties(target, props) { 21 | for (var i = 0; i < props.length; i++) { 22 | var descriptor = props[i]; 23 | descriptor.enumerable = descriptor.enumerable || false; 24 | descriptor.configurable = true; 25 | if ("value" in descriptor) descriptor.writable = true; 26 | Object.defineProperty(target, descriptor.key, descriptor); 27 | } 28 | } 29 | 30 | return function (Constructor, protoProps, staticProps) { 31 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 32 | if (staticProps) defineProperties(Constructor, staticProps); 33 | return Constructor; 34 | }; 35 | }(); 36 | 37 | _export('GenericDatasource', GenericDatasource = function () { 38 | function GenericDatasource(instanceSettings, $q, backendSrv, templateSrv) { 39 | _classCallCheck(this, GenericDatasource); 40 | 41 | this.type = instanceSettings.type; 42 | this.url = instanceSettings.url; 43 | this.name = instanceSettings.name; 44 | this.db = { 'url': instanceSettings.jsonData.mongodb_url, 'db': instanceSettings.jsonData.mongodb_db }; 45 | this.q = $q; 46 | this.backendSrv = backendSrv; 47 | this.templateSrv = templateSrv; 48 | this.withCredentials = instanceSettings.withCredentials; 49 | this.headers = { 'Content-Type': 'application/json' }; 50 | if (typeof instanceSettings.basicAuth === 'string' && instanceSettings.basicAuth.length > 0) { 51 | this.headers['Authorization'] = instanceSettings.basicAuth; 52 | } 53 | } 54 | 55 | _createClass(GenericDatasource, [{ 56 | key: 'query', 57 | value: function query(options) { 58 | var query = this.buildQueryParameters(options); 59 | query.targets = query.targets.filter(function (t) { 60 | return !t.hide; 61 | }); 62 | query.db = this.db; 63 | 64 | if (query.targets.length <= 0) { 65 | return this.q.when({ data: [] }); 66 | } 67 | 68 | return this.doRequest({ 69 | url: this.url + '/query', 70 | data: query, 71 | method: 'POST' 72 | }); 73 | } 74 | }, { 75 | key: 'testDatasource', 76 | value: function testDatasource() { 77 | return this.doRequest({ 78 | url: this.url + '/', 79 | data: { db: this.db }, 80 | method: 'POST' 81 | }).then(function (response) { 82 | if (response.status === 200) { 83 | return { status: response.data.status, message: response.data.message, title: response.data.display_status }; 84 | } 85 | }); 86 | } 87 | }, { 88 | key: 'annotationQuery', 89 | value: function annotationQuery(options) { 90 | var query = this.templateSrv.replace(options.annotation.query, {}, 'glob'); 91 | var annotationQuery = { 92 | range: options.range, 93 | annotation: { 94 | name: options.annotation.name, 95 | datasource: options.annotation.datasource, 96 | enable: options.annotation.enable, 97 | iconColor: options.annotation.iconColor, 98 | query: query 99 | }, 100 | rangeRaw: options.rangeRaw 101 | }; 102 | 103 | return this.doRequest({ 104 | url: this.url + '/annotations', 105 | method: 'POST', 106 | data: annotationQuery 107 | }).then(function (result) { 108 | response.data.$$status = result.status; 109 | response.data.$$config = result.config; 110 | return result.data; 111 | }); 112 | } 113 | }, { 114 | key: 'metricFindQuery', 115 | value: function metricFindQuery(query) { 116 | var interpolated = { 117 | target: this.templateSrv.replace(query, null, '') 118 | }; 119 | interpolated.db = this.db; 120 | 121 | return this.doRequest({ 122 | url: this.url + '/search', 123 | data: interpolated, 124 | method: 'POST' 125 | }).then(this.mapToTextValue); 126 | } 127 | }, { 128 | key: 'mapToTextValue', 129 | value: function mapToTextValue(result) { 130 | return _.map(result.data, function (d, i) { 131 | if (d && d.text && d.value) { 132 | return { text: d.text, value: d.value }; 133 | } else if (_.isObject(d)) { 134 | return { text: d, value: i }; 135 | } 136 | return { text: d, value: d }; 137 | }); 138 | } 139 | }, { 140 | key: 'doRequest', 141 | value: function doRequest(options) { 142 | options.withCredentials = this.withCredentials; 143 | options.headers = this.headers; 144 | 145 | return this.backendSrv.datasourceRequest(options); 146 | } 147 | }, { 148 | key: 'buildQueryParameters', 149 | value: function buildQueryParameters(options) { 150 | var _this = this; 151 | 152 | //remove place holder targets 153 | options.targets = _.filter(options.targets, function (target) { 154 | return target.target !== 'select metric'; 155 | }); 156 | 157 | var targets = _.map(options.targets, function (target) { 158 | return { 159 | target: _this.templateSrv.replace(target.target, options.scopedVars, ''), 160 | refId: target.refId, 161 | hide: target.hide, 162 | type: target.type || 'timeserie' 163 | }; 164 | }); 165 | 166 | options.targets = targets; 167 | 168 | return options; 169 | } 170 | }]); 171 | 172 | return GenericDatasource; 173 | }()); 174 | 175 | _export('GenericDatasource', GenericDatasource); 176 | } 177 | }; 178 | }); 179 | -------------------------------------------------------------------------------- /dist/img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/dist/img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg -------------------------------------------------------------------------------- /dist/img/sample_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/dist/img/sample_dashboard.png -------------------------------------------------------------------------------- /dist/img/sample_datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/dist/img/sample_datasource.png -------------------------------------------------------------------------------- /dist/img/sample_query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/dist/img/sample_query.png -------------------------------------------------------------------------------- /dist/img/sample_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/dist/img/sample_template.png -------------------------------------------------------------------------------- /dist/img/table_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/dist/img/table_panel.png -------------------------------------------------------------------------------- /dist/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | System.register(['./datasource', './query_ctrl'], function (_export, _context) { 4 | "use strict"; 5 | 6 | var GenericDatasource, GenericDatasourceQueryCtrl, GenericConfigCtrl, GenericQueryOptionsCtrl, GenericAnnotationsQueryCtrl; 7 | 8 | function _classCallCheck(instance, Constructor) { 9 | if (!(instance instanceof Constructor)) { 10 | throw new TypeError("Cannot call a class as a function"); 11 | } 12 | } 13 | 14 | return { 15 | setters: [function (_datasource) { 16 | GenericDatasource = _datasource.GenericDatasource; 17 | }, function (_query_ctrl) { 18 | GenericDatasourceQueryCtrl = _query_ctrl.GenericDatasourceQueryCtrl; 19 | }], 20 | execute: function () { 21 | _export('ConfigCtrl', GenericConfigCtrl = function GenericConfigCtrl() { 22 | _classCallCheck(this, GenericConfigCtrl); 23 | }); 24 | 25 | GenericConfigCtrl.templateUrl = 'partials/config.html'; 26 | 27 | _export('QueryOptionsCtrl', GenericQueryOptionsCtrl = function GenericQueryOptionsCtrl() { 28 | _classCallCheck(this, GenericQueryOptionsCtrl); 29 | }); 30 | 31 | GenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html'; 32 | 33 | _export('AnnotationsQueryCtrl', GenericAnnotationsQueryCtrl = function GenericAnnotationsQueryCtrl() { 34 | _classCallCheck(this, GenericAnnotationsQueryCtrl); 35 | }); 36 | 37 | GenericAnnotationsQueryCtrl.templateUrl = 'partials/annotations.editor.html'; 38 | 39 | _export('Datasource', GenericDatasource); 40 | 41 | _export('QueryCtrl', GenericDatasourceQueryCtrl); 42 | 43 | _export('ConfigCtrl', GenericConfigCtrl); 44 | 45 | _export('QueryOptionsCtrl', GenericQueryOptionsCtrl); 46 | 47 | _export('AnnotationsQueryCtrl', GenericAnnotationsQueryCtrl); 48 | } 49 | }; 50 | }); 51 | -------------------------------------------------------------------------------- /dist/partials/annotations.editor.html: -------------------------------------------------------------------------------- 1 | 2 |
Query
3 |
4 |
5 | 6 |
7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /dist/partials/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

MongoDB details

5 | 6 |
7 |
8 |
9 | MongoDB URL 10 | 13 | 14 |
15 | 16 |
17 | MongoDB Database 18 | 21 | 22 |
23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /dist/partials/query.editor.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 | 7 |
8 |