├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/dist/partials/config.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MongoDB details
5 |
6 |
25 |
26 |
--------------------------------------------------------------------------------
/dist/partials/query.editor.html:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
--------------------------------------------------------------------------------
/dist/partials/query.options.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MongoDB",
3 | "id": "grafana-mongodb-opensource-datasource",
4 | "type": "datasource",
5 |
6 | "partials": {
7 | "config": "public/app/plugins/datasource/simplejson/partials/config.html"
8 | },
9 |
10 | "metrics": true,
11 | "annotations": false,
12 |
13 | "info": {
14 | "description": "MongoDB datasource (opensource)",
15 | "author": {
16 | "name": "James Osgood"
17 | },
18 | "logos": {
19 | "small": "img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg",
20 | "large": "img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg"
21 | },
22 | "links": [
23 | {"name": "GitHub", "url": "https://github.com/grafana/simple-json-datasource"},
24 | {"name": "MIT License", "url": "https://github.com/grafana/simple-json-datasource/blob/master/LICENSE"}
25 | ],
26 | "version": "0.9.2",
27 | "updated": "2022-09-22"
28 | },
29 |
30 | "dependencies": {
31 | "grafanaVersion": "8.x.x",
32 | "plugins": [ ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/dist/query_ctrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | System.register(['app/plugins/sdk', './css/query-editor.css!'], function (_export, _context) {
4 | "use strict";
5 |
6 | var QueryCtrl, _createClass, GenericDatasourceQueryCtrl;
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 | function _possibleConstructorReturn(self, call) {
15 | if (!self) {
16 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
17 | }
18 |
19 | return call && (typeof call === "object" || typeof call === "function") ? call : self;
20 | }
21 |
22 | function _inherits(subClass, superClass) {
23 | if (typeof superClass !== "function" && superClass !== null) {
24 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
25 | }
26 |
27 | subClass.prototype = Object.create(superClass && superClass.prototype, {
28 | constructor: {
29 | value: subClass,
30 | enumerable: false,
31 | writable: true,
32 | configurable: true
33 | }
34 | });
35 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
36 | }
37 |
38 | return {
39 | setters: [function (_appPluginsSdk) {
40 | QueryCtrl = _appPluginsSdk.QueryCtrl;
41 | }, function (_cssQueryEditorCss) {}],
42 | execute: function () {
43 | _createClass = function () {
44 | function defineProperties(target, props) {
45 | for (var i = 0; i < props.length; i++) {
46 | var descriptor = props[i];
47 | descriptor.enumerable = descriptor.enumerable || false;
48 | descriptor.configurable = true;
49 | if ("value" in descriptor) descriptor.writable = true;
50 | Object.defineProperty(target, descriptor.key, descriptor);
51 | }
52 | }
53 |
54 | return function (Constructor, protoProps, staticProps) {
55 | if (protoProps) defineProperties(Constructor.prototype, protoProps);
56 | if (staticProps) defineProperties(Constructor, staticProps);
57 | return Constructor;
58 | };
59 | }();
60 |
61 | _export('GenericDatasourceQueryCtrl', GenericDatasourceQueryCtrl = function (_QueryCtrl) {
62 | _inherits(GenericDatasourceQueryCtrl, _QueryCtrl);
63 |
64 | function GenericDatasourceQueryCtrl($scope, $injector) {
65 | _classCallCheck(this, GenericDatasourceQueryCtrl);
66 |
67 | var _this = _possibleConstructorReturn(this, (GenericDatasourceQueryCtrl.__proto__ || Object.getPrototypeOf(GenericDatasourceQueryCtrl)).call(this, $scope, $injector));
68 |
69 | _this.scope = $scope;
70 | _this.target.target = _this.target.target || 'select metric';
71 | _this.target.type = _this.target.type || 'timeserie';
72 | _this.target.rawQuery = true;
73 | return _this;
74 | }
75 |
76 | _createClass(GenericDatasourceQueryCtrl, [{
77 | key: 'getOptions',
78 | value: function getOptions(query) {
79 | return this.datasource.metricFindQuery(query || '');
80 | }
81 | }, {
82 | key: 'toggleEditorMode',
83 | value: function toggleEditorMode() {
84 | this.target.rawQuery = !this.target.rawQuery;
85 | }
86 | }, {
87 | key: 'onChangeInternal',
88 | value: function onChangeInternal() {
89 | this.panelCtrl.refresh(); // Asks the panel to refresh data.
90 | }
91 | }]);
92 |
93 | return GenericDatasourceQueryCtrl;
94 | }(QueryCtrl));
95 |
96 | _export('GenericDatasourceQueryCtrl', GenericDatasourceQueryCtrl);
97 |
98 | GenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html';
99 | }
100 | };
101 | });
102 |
--------------------------------------------------------------------------------
/dist/server/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "server":
3 | {
4 | "port": 3333,
5 | "logRequests": false,
6 | "logQueries": false,
7 | "logTimings": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/dist/server/mongodb-grafana-proxy.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | mongodb-grafana-proxy
7 |
8 | EnvironmentVariables
9 |
10 | PATH
11 | /usr/local/bin/:$PATH
12 |
13 |
14 | WorkingDirectory
15 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server
16 |
17 | ProgramArguments
18 |
19 | /usr/local/lib/npm-packages/bin/forever
20 | -l
21 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy-forever.log
22 | -o
23 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy.log
24 | -e
25 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy-err.log
26 | --workingDir
27 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server
28 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy.js
29 |
30 |
31 | RunAtLoad
32 |
33 |
34 | KeepAlive
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/dist/server/mongodb-proxy.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var bodyParser = require('body-parser');
3 | var _ = require('lodash');
4 | var app = express();
5 | const MongoClient = require('mongodb').MongoClient;
6 | const assert = require('assert');
7 | var config = require('config');
8 | var Stopwatch = require("statman-stopwatch");
9 | var moment = require('moment')
10 | const { EJSON } = require('bson');
11 | var mongoParser = require('mongodb-query-parser');
12 | const { MutableDataFrame, FieldType } = require("@grafana/data")
13 |
14 | app.use(bodyParser.json());
15 |
16 | // Called by test
17 | app.all('/', function(req, res, next)
18 | {
19 | logRequest(req.body, "/")
20 | setCORSHeaders(res);
21 |
22 | MongoClient.connect(req.body.db.url, function(err, client)
23 | {
24 | if ( err != null )
25 | {
26 | res.send({ status : "error",
27 | display_status : "Error",
28 | message : 'MongoDB Connection Error: ' + err.message });
29 | }
30 | else
31 | {
32 | res.send( { status : "success",
33 | display_status : "Success",
34 | message : 'MongoDB Connection test OK' });
35 | }
36 | next()
37 | })
38 | });
39 |
40 | // Called by template functions and to look up variables
41 | app.all('/search', function(req, res, next)
42 | {
43 | logRequest(req.body, "/search")
44 | setCORSHeaders(res);
45 |
46 | // Generate an id to track requests
47 | const requestId = ++requestIdCounter
48 | // Add state for the queries in this request
49 | var queryStates = []
50 | requestsPending[requestId] = queryStates
51 | // Parse query string in target
52 | queryArgs = parseQuery(req.body.target, {})
53 | if (queryArgs.err != null)
54 | {
55 | console.log("Error in query parsing")
56 | queryError(requestId, queryArgs.err, next)
57 | }
58 | else
59 | {
60 | doTemplateQuery(requestId, queryArgs, req.body.db, res, next);
61 | }
62 | });
63 |
64 | // State for queries in flight. As results come it, acts as a semaphore and sends the results back
65 | var requestIdCounter = 0
66 | // Map of request id -> array of results. Results is
67 | // { query, err, output }
68 | var requestsPending = {}
69 |
70 | // Called when a query finishes with an error
71 | function queryError(requestId, err, next)
72 | {
73 | // We only 1 return error per query so it may have been removed from the list
74 | if ( requestId in requestsPending )
75 | {
76 | // Remove request
77 | delete requestsPending[requestId]
78 | // Send back error
79 | next(err)
80 | }
81 | }
82 |
83 | // Called when query finished
84 | function queryFinished(requestId, refId, results, res, next)
85 | {
86 | // We only 1 return error per query so it may have been removed from the list
87 | if ( requestId in requestsPending )
88 | {
89 | var queryStatus = requestsPending[requestId]
90 | // Mark this as finished
91 | queryStatus[refId].pending = false
92 | queryStatus[refId].results = results
93 |
94 | // See if we're all done
95 | var done = true
96 | for (const i in queryStatus)
97 | {
98 | if (queryStatus[i].pending == true )
99 | {
100 | done = false
101 | break
102 | }
103 | }
104 |
105 | // If query done, send back results
106 | if (done)
107 | {
108 | // Concatenate results
109 | output = []
110 | for (const i in queryStatus)
111 | {
112 | var queryResults = queryStatus[i].results
113 | var keys = Object.keys(queryResults)
114 | for (var k = 0; k < keys.length; k++)
115 | {
116 | var tg = keys[k]
117 | output.push(queryResults[tg])
118 | }
119 | }
120 | res.json(output);
121 | next()
122 | // Remove request
123 | delete requestsPending[requestId]
124 | }
125 | }
126 | }
127 |
128 | // Called to get graph points
129 | app.all('/query', function(req, res, next)
130 | {
131 | logRequest(req.body, "/query")
132 | setCORSHeaders(res);
133 |
134 | // Parse query string in target
135 | substitutions = { "$from" : new Date(req.body.range.from),
136 | "$to" : new Date(req.body.range.to),
137 | "$dateBucketCount" : getBucketCount(req.body.range.from, req.body.range.to, req.body.intervalMs)
138 | }
139 |
140 | // Generate an id to track requests
141 | const requestId = ++requestIdCounter
142 | // Add state for the queries in this request
143 | var queryStates = {}
144 | requestsPending[requestId] = queryStates
145 | var error = false
146 |
147 | for ( var queryId = 0; queryId < req.body.targets.length && !error; queryId++)
148 | {
149 | tg = req.body.targets[queryId]
150 | refId = tg.refId
151 | queryArgs = parseQuery(tg.target, substitutions)
152 | queryArgs.type = tg.type
153 | if (queryArgs.err != null)
154 | {
155 | console.log("Error in query parsing")
156 | queryError(requestId, queryArgs.err, next)
157 | error = true
158 | }
159 | else
160 | {
161 | // Add to the state
162 | queryStates[refId] = { pending : true }
163 |
164 | // Run the query
165 | runAggregateQuery( requestId, refId, req.body, queryArgs, res, next)
166 | }
167 | }
168 | }
169 | );
170 |
171 | app.use(function(error, req, res, next)
172 | {
173 | // Any request to this server will get here, and will send an HTTP
174 | // response with the error message
175 | res.status(500).json({ message: error.message });
176 | });
177 |
178 | // Get config from server/default.json
179 | var serverConfig = config.get('server');
180 |
181 | app.listen(serverConfig.port);
182 |
183 | console.log("Server is listening on port " + serverConfig.port);
184 |
185 | function setCORSHeaders(res)
186 | {
187 | res.setHeader("Access-Control-Allow-Origin", "*");
188 | res.setHeader("Access-Control-Allow-Methods", "POST");
189 | res.setHeader("Access-Control-Allow-Headers", "accept, content-type");
190 | }
191 |
192 | function forIn(obj, processFunc)
193 | {
194 | var key;
195 | for (key in obj)
196 | {
197 | var value = obj[key]
198 | processFunc(obj, key, value)
199 | if ( value != null && typeof(value) == "object")
200 | {
201 | forIn(value, processFunc)
202 | }
203 | }
204 | }
205 |
206 | function parseQuery(query, substitutions)
207 | {
208 | doc = {}
209 | queryErrors = []
210 |
211 | query = query.trim()
212 | if (query.substring(0,3) != "db.")
213 | {
214 | queryErrors.push("Query must start with db.")
215 | return null
216 | }
217 |
218 | // Query is of the form db..aggregate or db..find
219 | // Split on the first ( after db.
220 | var openBracketIndex = query.indexOf('(', 3)
221 | if (openBracketIndex == -1)
222 | {
223 | queryErrors.push("Can't find opening bracket")
224 | }
225 | else
226 | {
227 | // Split the first bit - it's the collection name and operation ( must be aggregate )
228 | var parts = query.substring(3, openBracketIndex).split('.')
229 | // Collection names can have .s so last part is operation, rest is the collection name
230 | if (parts.length >= 2)
231 | {
232 | doc.operation = parts.pop().trim()
233 | doc.collection = parts.join('.')
234 | }
235 | else
236 | {
237 | queryErrors.push("Invalid collection and operation syntax")
238 | }
239 |
240 | // Args is the rest up to the last bracket
241 | var closeBracketIndex = query.lastIndexOf(')')
242 | if (closeBracketIndex == -1)
243 | {
244 | queryErrors.push("Can't find last bracket")
245 | }
246 | else
247 | {
248 | var args = query.substring(openBracketIndex + 1, closeBracketIndex)
249 | if ( doc.operation == 'aggregate')
250 | {
251 | // Wrap args in array syntax so we can check for optional options arg
252 | args = '[' + args + ']'
253 | docs = mongoParser(args)
254 | // First Arg is pipeline
255 | doc.pipeline = docs[0]
256 | // If we have 2 top level args, second is agg options
257 | if ( docs.length == 2 )
258 | {
259 | doc.agg_options = docs[1]
260 | }
261 | // Replace with substitutions
262 | for ( var i = 0; i < doc.pipeline.length; i++)
263 | {
264 | var stage = doc.pipeline[i]
265 | forIn(stage, function (obj, key, value)
266 | {
267 | if ( typeof(value) == "string" )
268 | {
269 | if ( value in substitutions )
270 | {
271 | obj[key] = substitutions[value]
272 | }
273 | }
274 | })
275 | }
276 | }
277 | else
278 | {
279 | queryErrors.push("Unknown operation " + doc.operation + ", only aggregate supported")
280 | }
281 | }
282 | }
283 |
284 | if (queryErrors.length > 0 )
285 | {
286 | doc.err = new Error('Failed to parse query - ' + queryErrors.join(':'))
287 | }
288 |
289 | return doc
290 | }
291 |
292 | // Run an aggregate query. Must return documents of the form
293 | // { value : 0.34334, ts : }
294 |
295 | function runAggregateQuery( requestId, refId, body, queryArgs, res, next )
296 | {
297 | MongoClient.connect(body.db.url, function(err, client)
298 | {
299 | if ( err != null )
300 | {
301 | queryError(requestId, err, next)
302 | }
303 | else
304 | {
305 | const db = client.db(body.db.db);
306 |
307 | // Get the documents collection
308 | const collection = db.collection(queryArgs.collection);
309 | logQuery(queryArgs.pipeline, queryArgs.agg_options)
310 | var stopwatch = new Stopwatch(true)
311 |
312 | collection.aggregate(queryArgs.pipeline, queryArgs.agg_options).toArray(function(err, docs)
313 | {
314 | if ( err != null )
315 | {
316 | client.close();
317 | console.log("Error running aggregation query")
318 | queryError(requestId, err, next)
319 | }
320 | else
321 | {
322 | try
323 | {
324 | var results = {}
325 | if ( queryArgs.type == 'timeserie' )
326 | {
327 | results = getTimeseriesResults(docs, refId)
328 | }
329 | else
330 | {
331 | results = getTableResults(docs, refId)
332 | }
333 |
334 | client.close();
335 | var elapsedTimeMs = stopwatch.stop()
336 | logTiming(body, elapsedTimeMs)
337 | // Mark query as finished - will send back results when all queries finished
338 | queryFinished(requestId, refId, results, res, next)
339 | }
340 | catch(err)
341 | {
342 | console.log("Error returning results")
343 | queryError(requestId, err, next)
344 | }
345 | }
346 | })
347 | }
348 | })
349 | }
350 |
351 | function getTableResults(docs, refId)
352 | {
353 | var columns = {}
354 |
355 | // Build superset of columns
356 | for ( var i = 0; i < docs.length; i++)
357 | {
358 | var doc = docs[i]
359 | // Go through all properties
360 | for (var propName in doc )
361 | {
362 | // See if we need to add a new column
363 | if ( !(propName in columns) )
364 | {
365 | columns[propName] =
366 | {
367 | text : propName,
368 | type : "text"
369 | }
370 | }
371 | }
372 | }
373 |
374 | // Build return rows
375 | rows = []
376 | for ( var i = 0; i < docs.length; i++)
377 | {
378 | var doc = docs[i]
379 | row = []
380 | // All cols
381 | for ( var colName in columns )
382 | {
383 | var col = columns[colName]
384 | if ( col.text in doc )
385 | {
386 | row.push(doc[col.text])
387 | }
388 | else
389 | {
390 | row.push(null)
391 | }
392 | }
393 | rows.push(row)
394 | }
395 |
396 | var results = {}
397 | results["table"] = {
398 | refId : refId,
399 | columns : Object.values(columns),
400 | rows : rows,
401 | type : "table"
402 | }
403 | return results
404 | }
405 |
406 | function getTimeseriesResults(docs, refId)
407 | {
408 | var results = {}
409 | for ( var i = 0; i < docs.length; i++)
410 | {
411 | var doc = docs[i]
412 | if (results[refId] === undefined)
413 | {
414 | const dataframeCreator = { refId: refId, fields: [] }
415 |
416 | for (const field in doc)
417 | {
418 | switch(field)
419 | {
420 | case "time":
421 | dataframeCreator.fields.push({ name: 'time', type: FieldType.time })
422 | break;
423 | case "metric":
424 | dataframeCreator.fields.push({ name: 'metric', type: FieldType.string })
425 | break;
426 | default:
427 | dataframeCreator.fields.push({ name: field, type: FieldType.number })
428 | break;
429 | }
430 | }
431 | results[refId] = new MutableDataFrame(dataframeCreator)
432 | }
433 |
434 | const dataFrameValue = {}
435 | for (const field in doc)
436 | {
437 | switch(field)
438 | {
439 | case "time":
440 | if (doc["time"] === null)
441 | {
442 | dataFrameValue["time"] = 0 // TODO??
443 | }
444 | else
445 | {
446 | dataFrameValue["time"] = doc["time"].getTime()
447 | }
448 | break;
449 | default:
450 | dataFrameValue[field] = doc[field]
451 | break;
452 | }
453 | }
454 |
455 | results[refId].add(dataFrameValue)
456 | }
457 | return results
458 | }
459 | // Runs a query to support templates. Must returns documents of the form
460 | // { _id : }
461 | // { text : __text, value: __value }
462 | function doTemplateQuery(requestId, queryArgs, db, res, next)
463 | {
464 | if ( queryArgs.err == null)
465 | {
466 | // Database Name
467 | const dbName = db.db
468 |
469 | // Use connect method to connect to the server
470 | MongoClient.connect(db.url, function(err, client)
471 | {
472 | if ( err != null )
473 | {
474 | queryError(requestId, err, next )
475 | }
476 | else
477 | {
478 | // Remove request from list
479 | if ( requestId in requestsPending )
480 | {
481 | delete requestsPending[requestId]
482 | }
483 | const db = client.db(dbName);
484 | // Get the documents collection
485 | const collection = db.collection(queryArgs.collection);
486 |
487 | collection.aggregate(queryArgs.pipeline).toArray(function(err, result)
488 | {
489 | assert.equal(err, null)
490 |
491 | output = []
492 | for ( var i = 0; i < result.length; i++)
493 | {
494 | var doc = result[i]
495 | if ((doc["__text"] !== undefined) && (doc["__value"] !== undefined))
496 | {
497 | output.push({ text: doc["__text"], value: doc["__value"]})
498 | }
499 | else
500 | {
501 | output.push(doc["_id"])
502 | }
503 | }
504 | res.json(output);
505 | client.close()
506 | next()
507 | })
508 | }
509 | })
510 | }
511 | else
512 | {
513 | next(queryArgs.err)
514 | }
515 | }
516 |
517 | function logRequest(body, type)
518 | {
519 | if (serverConfig.logRequests)
520 | {
521 | console.log("REQUEST: " + type + ":\n" + EJSON.stringify(body, null, 2))
522 | }
523 | }
524 |
525 | function logQuery(query, options)
526 | {
527 | if (serverConfig.logQueries)
528 | {
529 | console.log("Query:")
530 | console.log(EJSON.stringify(query,null,2))
531 | if ( options != null )
532 | {
533 | console.log("Query Options:")
534 | console.log(EJSON.stringify(options,null,2))
535 | }
536 | }
537 | }
538 |
539 | function logTiming(body, elapsedTimeMs)
540 | {
541 |
542 | if (serverConfig.logTimings)
543 | {
544 | var range = new Date(body.range.to) - new Date(body.range.from)
545 | var diff = moment.duration(range)
546 |
547 | console.log("Request: " + intervalCount(diff, body.interval, body.intervalMs) + " - Returned in " + elapsedTimeMs.toFixed(2) + "ms")
548 | }
549 | }
550 |
551 | // Take a range as a moment.duration and a grafana interval like 30s, 1m etc
552 | // And return the number of intervals that represents
553 | function intervalCount(range, intervalString, intervalMs)
554 | {
555 | // Convert everything to seconds
556 | var rangeSeconds = range.asSeconds()
557 | var intervalsInRange = rangeSeconds / (intervalMs / 1000)
558 |
559 | var output = intervalsInRange.toFixed(0) + ' ' + intervalString + ' intervals'
560 | return output
561 | }
562 |
563 | function getBucketCount(from, to, intervalMs)
564 | {
565 | var boundaries = []
566 | var current = new Date(from).getTime()
567 | var toMs = new Date(to).getTime()
568 | var count = 0
569 | while ( current < toMs )
570 | {
571 | current += intervalMs
572 | count++
573 | }
574 |
575 | return count
576 | }
577 |
--------------------------------------------------------------------------------
/dist/test/datasource.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.GenericDatasource = undefined;
7 |
8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
9 |
10 | var _lodash = require('lodash');
11 |
12 | var _lodash2 = _interopRequireDefault(_lodash);
13 |
14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15 |
16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
17 |
18 | var GenericDatasource = exports.GenericDatasource = function () {
19 | function GenericDatasource(instanceSettings, $q, backendSrv, templateSrv) {
20 | _classCallCheck(this, GenericDatasource);
21 |
22 | this.type = instanceSettings.type;
23 | this.url = instanceSettings.url;
24 | this.name = instanceSettings.name;
25 | this.db = { 'url': instanceSettings.jsonData.mongodb_url, 'db': instanceSettings.jsonData.mongodb_db };
26 | this.q = $q;
27 | this.backendSrv = backendSrv;
28 | this.templateSrv = templateSrv;
29 | this.withCredentials = instanceSettings.withCredentials;
30 | this.headers = { 'Content-Type': 'application/json' };
31 | if (typeof instanceSettings.basicAuth === 'string' && instanceSettings.basicAuth.length > 0) {
32 | this.headers['Authorization'] = instanceSettings.basicAuth;
33 | }
34 | }
35 |
36 | _createClass(GenericDatasource, [{
37 | key: 'query',
38 | value: function query(options) {
39 | var query = this.buildQueryParameters(options);
40 | query.targets = query.targets.filter(function (t) {
41 | return !t.hide;
42 | });
43 | query.db = this.db;
44 |
45 | if (query.targets.length <= 0) {
46 | return this.q.when({ data: [] });
47 | }
48 |
49 | return this.doRequest({
50 | url: this.url + '/query',
51 | data: query,
52 | method: 'POST'
53 | });
54 | }
55 | }, {
56 | key: 'testDatasource',
57 | value: function testDatasource() {
58 | return this.doRequest({
59 | url: this.url + '/',
60 | data: { db: this.db },
61 | method: 'POST'
62 | }).then(function (response) {
63 | if (response.status === 200) {
64 | return { status: response.data.status, message: response.data.message, title: response.data.display_status };
65 | }
66 | });
67 | }
68 | }, {
69 | key: 'annotationQuery',
70 | value: function annotationQuery(options) {
71 | var query = this.templateSrv.replace(options.annotation.query, {}, 'glob');
72 | var annotationQuery = {
73 | range: options.range,
74 | annotation: {
75 | name: options.annotation.name,
76 | datasource: options.annotation.datasource,
77 | enable: options.annotation.enable,
78 | iconColor: options.annotation.iconColor,
79 | query: query
80 | },
81 | rangeRaw: options.rangeRaw
82 | };
83 |
84 | return this.doRequest({
85 | url: this.url + '/annotations',
86 | method: 'POST',
87 | data: annotationQuery
88 | }).then(function (result) {
89 | response.data.$$status = result.status;
90 | response.data.$$config = result.config;
91 | return result.data;
92 | });
93 | }
94 | }, {
95 | key: 'metricFindQuery',
96 | value: function metricFindQuery(query) {
97 | var interpolated = {
98 | target: this.templateSrv.replace(query, null, '')
99 | };
100 | interpolated.db = this.db;
101 |
102 | return this.doRequest({
103 | url: this.url + '/search',
104 | data: interpolated,
105 | method: 'POST'
106 | }).then(this.mapToTextValue);
107 | }
108 | }, {
109 | key: 'mapToTextValue',
110 | value: function mapToTextValue(result) {
111 | return _lodash2.default.map(result.data, function (d, i) {
112 | if (d && d.text && d.value) {
113 | return { text: d.text, value: d.value };
114 | } else if (_lodash2.default.isObject(d)) {
115 | return { text: d, value: i };
116 | }
117 | return { text: d, value: d };
118 | });
119 | }
120 | }, {
121 | key: 'doRequest',
122 | value: function doRequest(options) {
123 | options.withCredentials = this.withCredentials;
124 | options.headers = this.headers;
125 |
126 | return this.backendSrv.datasourceRequest(options);
127 | }
128 | }, {
129 | key: 'buildQueryParameters',
130 | value: function buildQueryParameters(options) {
131 | var _this = this;
132 |
133 | //remove place holder targets
134 | options.targets = _lodash2.default.filter(options.targets, function (target) {
135 | return target.target !== 'select metric';
136 | });
137 |
138 | var targets = _lodash2.default.map(options.targets, function (target) {
139 | return {
140 | target: _this.templateSrv.replace(target.target, options.scopedVars, ''),
141 | refId: target.refId,
142 | hide: target.hide,
143 | type: target.type || 'timeserie'
144 | };
145 | });
146 |
147 | options.targets = targets;
148 |
149 | return options;
150 | }
151 | }]);
152 |
153 | return GenericDatasource;
154 | }();
155 |
--------------------------------------------------------------------------------
/dist/test/module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.AnnotationsQueryCtrl = exports.QueryOptionsCtrl = exports.ConfigCtrl = exports.QueryCtrl = exports.Datasource = undefined;
7 |
8 | var _datasource = require('./datasource');
9 |
10 | var _query_ctrl = require('./query_ctrl');
11 |
12 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
13 |
14 | var GenericConfigCtrl = function GenericConfigCtrl() {
15 | _classCallCheck(this, GenericConfigCtrl);
16 | };
17 |
18 | GenericConfigCtrl.templateUrl = 'partials/config.html';
19 |
20 | var GenericQueryOptionsCtrl = function GenericQueryOptionsCtrl() {
21 | _classCallCheck(this, GenericQueryOptionsCtrl);
22 | };
23 |
24 | GenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html';
25 |
26 | var GenericAnnotationsQueryCtrl = function GenericAnnotationsQueryCtrl() {
27 | _classCallCheck(this, GenericAnnotationsQueryCtrl);
28 | };
29 |
30 | GenericAnnotationsQueryCtrl.templateUrl = 'partials/annotations.editor.html';
31 |
32 | exports.Datasource = _datasource.GenericDatasource;
33 | exports.QueryCtrl = _query_ctrl.GenericDatasourceQueryCtrl;
34 | exports.ConfigCtrl = GenericConfigCtrl;
35 | exports.QueryOptionsCtrl = GenericQueryOptionsCtrl;
36 | exports.AnnotationsQueryCtrl = GenericAnnotationsQueryCtrl;
37 |
--------------------------------------------------------------------------------
/dist/test/query_ctrl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.GenericDatasourceQueryCtrl = undefined;
7 |
8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
9 |
10 | var _sdk = require('app/plugins/sdk');
11 |
12 | require('./css/query-editor.css!');
13 |
14 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
15 |
16 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
17 |
18 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
19 |
20 | var GenericDatasourceQueryCtrl = exports.GenericDatasourceQueryCtrl = function (_QueryCtrl) {
21 | _inherits(GenericDatasourceQueryCtrl, _QueryCtrl);
22 |
23 | function GenericDatasourceQueryCtrl($scope, $injector) {
24 | _classCallCheck(this, GenericDatasourceQueryCtrl);
25 |
26 | var _this = _possibleConstructorReturn(this, (GenericDatasourceQueryCtrl.__proto__ || Object.getPrototypeOf(GenericDatasourceQueryCtrl)).call(this, $scope, $injector));
27 |
28 | _this.scope = $scope;
29 | _this.target.target = _this.target.target || 'select metric';
30 | _this.target.type = _this.target.type || 'timeserie';
31 | _this.target.rawQuery = true;
32 | return _this;
33 | }
34 |
35 | _createClass(GenericDatasourceQueryCtrl, [{
36 | key: 'getOptions',
37 | value: function getOptions(query) {
38 | return this.datasource.metricFindQuery(query || '');
39 | }
40 | }, {
41 | key: 'toggleEditorMode',
42 | value: function toggleEditorMode() {
43 | this.target.rawQuery = !this.target.rawQuery;
44 | }
45 | }, {
46 | key: 'onChangeInternal',
47 | value: function onChangeInternal() {
48 | this.panelCtrl.refresh(); // Asks the panel to refresh data.
49 | }
50 | }]);
51 |
52 | return GenericDatasourceQueryCtrl;
53 | }(_sdk.QueryCtrl);
54 |
55 | GenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html';
56 |
--------------------------------------------------------------------------------
/dist/test/spec/datasource_spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _module = require("../module");
4 |
5 | var _q = require("q");
6 |
7 | var _q2 = _interopRequireDefault(_q);
8 |
9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10 |
11 | describe('GenericDatasource', function () {
12 | var ctx = {};
13 |
14 | beforeEach(function () {
15 | ctx.$q = _q2.default;
16 | ctx.backendSrv = {};
17 | ctx.templateSrv = {};
18 | var jsonData = { mongodb_url: 'mongodb://localhost:27017', mongodb_db: 'test_db' };
19 | ctx.ds = new _module.Datasource({ jsonData: jsonData }, ctx.$q, ctx.backendSrv, ctx.templateSrv);
20 | });
21 |
22 | it('should return an empty array when no targets are set', function (done) {
23 | ctx.ds.query({ targets: [] }).then(function (result) {
24 | expect(result.data).to.have.length(0);
25 | done();
26 | });
27 | });
28 |
29 | it('should return the server results when a target is set', function (done) {
30 | ctx.backendSrv.datasourceRequest = function (request) {
31 | return ctx.$q.when({
32 | _request: request,
33 | data: [{
34 | target: 'X',
35 | datapoints: [1, 2, 3]
36 | }]
37 | });
38 | };
39 |
40 | ctx.templateSrv.replace = function (data) {
41 | return data;
42 | };
43 |
44 | ctx.ds.query({ targets: ['hits'] }).then(function (result) {
45 | expect(result._request.data.targets).to.have.length(1);
46 | expect(result._request.data.db.url).to.equal('mongodb://localhost:27017');
47 | expect(result._request.data.db.db).to.equal('test_db');
48 |
49 | var series = result.data[0];
50 | expect(series.target).to.equal('X');
51 | expect(series.datapoints).to.have.length(3);
52 | done();
53 | });
54 | });
55 |
56 | it('should return the metric results when a target is null', function (done) {
57 | ctx.backendSrv.datasourceRequest = function (request) {
58 | return ctx.$q.when({
59 | _request: request,
60 | data: ["metric_0", "metric_1", "metric_2"]
61 | });
62 | };
63 |
64 | ctx.templateSrv.replace = function (data) {
65 | return data;
66 | };
67 |
68 | ctx.ds.metricFindQuery({ target: null }).then(function (result) {
69 | expect(result).to.have.length(3);
70 | expect(result[0].text).to.equal('metric_0');
71 | expect(result[0].value).to.equal('metric_0');
72 | expect(result[1].text).to.equal('metric_1');
73 | expect(result[1].value).to.equal('metric_1');
74 | expect(result[2].text).to.equal('metric_2');
75 | expect(result[2].value).to.equal('metric_2');
76 | done();
77 | });
78 | });
79 |
80 | it('should return the metric target results when a target is set', function (done) {
81 | ctx.backendSrv.datasourceRequest = function (request) {
82 | var target = request.data.target;
83 | var result = [target + "_0", target + "_1", target + "_2"];
84 |
85 | return ctx.$q.when({
86 | _request: request,
87 | data: result
88 | });
89 | };
90 |
91 | ctx.templateSrv.replace = function (data) {
92 | return data;
93 | };
94 |
95 | ctx.ds.metricFindQuery('search').then(function (result) {
96 | expect(result).to.have.length(3);
97 | expect(result[0].text).to.equal('search_0');
98 | expect(result[0].value).to.equal('search_0');
99 | expect(result[1].text).to.equal('search_1');
100 | expect(result[1].value).to.equal('search_1');
101 | expect(result[2].text).to.equal('search_2');
102 | expect(result[2].value).to.equal('search_2');
103 | done();
104 | });
105 | });
106 |
107 | it('should return the metric results when the target is an empty string', function (done) {
108 | ctx.backendSrv.datasourceRequest = function (request) {
109 | return ctx.$q.when({
110 | _request: request,
111 | data: ["metric_0", "metric_1", "metric_2"]
112 | });
113 | };
114 |
115 | ctx.templateSrv.replace = function (data) {
116 | return data;
117 | };
118 |
119 | ctx.ds.metricFindQuery('').then(function (result) {
120 | expect(result).to.have.length(3);
121 | expect(result[0].text).to.equal('metric_0');
122 | expect(result[0].value).to.equal('metric_0');
123 | expect(result[1].text).to.equal('metric_1');
124 | expect(result[1].value).to.equal('metric_1');
125 | expect(result[2].text).to.equal('metric_2');
126 | expect(result[2].value).to.equal('metric_2');
127 | done();
128 | });
129 | });
130 |
131 | it('should return the metric results when the args are an empty object', function (done) {
132 | ctx.backendSrv.datasourceRequest = function (request) {
133 | return ctx.$q.when({
134 | _request: request,
135 | data: ["metric_0", "metric_1", "metric_2"]
136 | });
137 | };
138 |
139 | ctx.templateSrv.replace = function (data) {
140 | return data;
141 | };
142 |
143 | ctx.ds.metricFindQuery().then(function (result) {
144 | expect(result).to.have.length(3);
145 | expect(result[0].text).to.equal('metric_0');
146 | expect(result[0].value).to.equal('metric_0');
147 | expect(result[1].text).to.equal('metric_1');
148 | expect(result[1].value).to.equal('metric_1');
149 | expect(result[2].text).to.equal('metric_2');
150 | expect(result[2].value).to.equal('metric_2');
151 | done();
152 | });
153 | });
154 |
155 | it('should return the metric target results when the args are a string', function (done) {
156 | ctx.backendSrv.datasourceRequest = function (request) {
157 | var target = request.data.target;
158 | var result = [target + "_0", target + "_1", target + "_2"];
159 |
160 | return ctx.$q.when({
161 | _request: request,
162 | data: result
163 | });
164 | };
165 |
166 | ctx.templateSrv.replace = function (data) {
167 | return data;
168 | };
169 |
170 | ctx.ds.metricFindQuery('search').then(function (result) {
171 | expect(result).to.have.length(3);
172 | expect(result[0].text).to.equal('search_0');
173 | expect(result[0].value).to.equal('search_0');
174 | expect(result[1].text).to.equal('search_1');
175 | expect(result[1].value).to.equal('search_1');
176 | expect(result[2].text).to.equal('search_2');
177 | expect(result[2].value).to.equal('search_2');
178 | done();
179 | });
180 | });
181 |
182 | it('should return data as text and as value', function (done) {
183 | var result = ctx.ds.mapToTextValue({ data: ["zero", "one", "two"] });
184 |
185 | expect(result).to.have.length(3);
186 | expect(result[0].text).to.equal('zero');
187 | expect(result[0].value).to.equal('zero');
188 | expect(result[1].text).to.equal('one');
189 | expect(result[1].value).to.equal('one');
190 | expect(result[2].text).to.equal('two');
191 | expect(result[2].value).to.equal('two');
192 | done();
193 | });
194 |
195 | it('should return text as text and value as value', function (done) {
196 | var data = [{ text: "zero", value: "value_0" }, { text: "one", value: "value_1" }, { text: "two", value: "value_2" }];
197 |
198 | var result = ctx.ds.mapToTextValue({ data: data });
199 |
200 | expect(result).to.have.length(3);
201 | expect(result[0].text).to.equal('zero');
202 | expect(result[0].value).to.equal('value_0');
203 | expect(result[1].text).to.equal('one');
204 | expect(result[1].value).to.equal('value_1');
205 | expect(result[2].text).to.equal('two');
206 | expect(result[2].value).to.equal('value_2');
207 | done();
208 | });
209 |
210 | it('should return data as text and index as value', function (done) {
211 | var data = [{ a: "zero", b: "value_0" }, { a: "one", b: "value_1" }, { a: "two", b: "value_2" }];
212 |
213 | var result = ctx.ds.mapToTextValue({ data: data });
214 |
215 | expect(result).to.have.length(3);
216 | expect(result[0].text).to.equal(data[0]);
217 | expect(result[0].value).to.equal(0);
218 | expect(result[1].text).to.equal(data[1]);
219 | expect(result[1].value).to.equal(1);
220 | expect(result[2].text).to.equal(data[2]);
221 | expect(result[2].value).to.equal(2);
222 | done();
223 | });
224 | });
225 |
--------------------------------------------------------------------------------
/dist/test/spec/test-main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _prunk = require('prunk');
4 |
5 | var _prunk2 = _interopRequireDefault(_prunk);
6 |
7 | var _jsdom = require('jsdom');
8 |
9 | var _chai = require('chai');
10 |
11 | var _chai2 = _interopRequireDefault(_chai);
12 |
13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14 |
15 | // Mock Grafana modules that are not available outside of the core project
16 | // Required for loading module.js
17 | _prunk2.default.mock('./css/query-editor.css!', 'no css, dude.');
18 | _prunk2.default.mock('app/plugins/sdk', {
19 | QueryCtrl: null
20 | });
21 |
22 | // Setup jsdom
23 | // Required for loading angularjs
24 | global.document = new _jsdom.JSDOM('');
25 | global.window = global.document.parentWindow;
26 |
27 | // Setup Chai
28 | _chai2.default.should();
29 | global.assert = _chai2.default.assert;
30 | global.expect = _chai2.default.expect;
31 |
--------------------------------------------------------------------------------
/examples/RPI Mongo Bucket - Atlas CS.json:
--------------------------------------------------------------------------------
1 | {
2 | "__inputs": [
3 | {
4 | "name": "DS_RPI_- ATLAS",
5 | "label": "RPI - Atlas",
6 | "description": "",
7 | "type": "datasource",
8 | "pluginId": "grafana-mongodb-opensource-datasource",
9 | "pluginName": "MongoDB"
10 | }
11 | ],
12 | "__requires": [
13 | {
14 | "type": "grafana",
15 | "id": "grafana",
16 | "name": "Grafana",
17 | "version": "5.1.3"
18 | },
19 | {
20 | "type": "datasource",
21 | "id": "grafana-mongodb-opensource-datasource",
22 | "name": "MongoDB",
23 | "version": "0.8.1"
24 | },
25 | {
26 | "type": "panel",
27 | "id": "graph",
28 | "name": "Graph",
29 | "version": "5.0.0"
30 | }
31 | ],
32 | "annotations": {
33 | "list": [
34 | {
35 | "builtIn": 1,
36 | "datasource": "-- Grafana --",
37 | "enable": true,
38 | "hide": true,
39 | "iconColor": "rgba(0, 211, 255, 1)",
40 | "name": "Annotations & Alerts",
41 | "type": "dashboard"
42 | }
43 | ]
44 | },
45 | "editable": true,
46 | "gnetId": null,
47 | "graphTooltip": 0,
48 | "id": null,
49 | "iteration": 1535309747386,
50 | "links": [],
51 | "panels": [
52 | {
53 | "aliasColors": {},
54 | "bars": false,
55 | "dashLength": 10,
56 | "dashes": false,
57 | "datasource": "${DS_RPI_- ATLAS}",
58 | "fill": 1,
59 | "gridPos": {
60 | "h": 11,
61 | "w": 24,
62 | "x": 0,
63 | "y": 0
64 | },
65 | "id": 1,
66 | "legend": {
67 | "avg": false,
68 | "current": false,
69 | "max": false,
70 | "min": false,
71 | "show": true,
72 | "total": false,
73 | "values": false
74 | },
75 | "lines": true,
76 | "linewidth": 1,
77 | "links": [],
78 | "nullPointMode": "null",
79 | "percentage": false,
80 | "pointradius": 5,
81 | "points": false,
82 | "renderer": "flot",
83 | "seriesOverrides": [
84 | {
85 | "alias": "value"
86 | }
87 | ],
88 | "spaceLength": 10,
89 | "stack": false,
90 | "steppedLine": false,
91 | "targets": [
92 | {
93 | "rawQuery": true,
94 | "refId": "B",
95 | "target": "db.sensor_value.aggregate( [ \n{ \"$match\" : { \"sensor_type\" : \"$var_sensor_type\", \"host_name\" : \"$var_host\" , \"sensor_name\" : \"ep\" , \"sensor_type\" : \"temperature\" , \"ts\" : { \"$gte\" : \"$from\", \"$lt\" : \"$to\" }}},\n{ \"$bucketAuto\" : { \"groupBy\" : \"$ts\", \n \"buckets\" : \"$dateBucketCount\", \n \"output\" : { \"maxValue\" : { \"$max\" : \"$sensor_value\" } } } }, \n{ \"$project\" : { \"name\" : \"Enviro pHAT\", \"value\" : \"$maxValue\", \"ts\" : \"$_id.min\", \"_id\" : 0 } } ] )",
96 | "type": "timeserie"
97 | }
98 | ],
99 | "thresholds": [],
100 | "timeFrom": null,
101 | "timeShift": null,
102 | "title": "Sensor Value",
103 | "tooltip": {
104 | "shared": true,
105 | "sort": 0,
106 | "value_type": "individual"
107 | },
108 | "type": "graph",
109 | "xaxis": {
110 | "buckets": null,
111 | "mode": "time",
112 | "name": null,
113 | "show": true,
114 | "values": []
115 | },
116 | "yaxes": [
117 | {
118 | "format": "none",
119 | "label": null,
120 | "logBase": 1,
121 | "max": null,
122 | "min": null,
123 | "show": true
124 | },
125 | {
126 | "format": "short",
127 | "label": null,
128 | "logBase": 1,
129 | "max": null,
130 | "min": null,
131 | "show": true
132 | }
133 | ],
134 | "yaxis": {
135 | "align": false,
136 | "alignLevel": null
137 | }
138 | },
139 | {
140 | "aliasColors": {},
141 | "bars": true,
142 | "dashLength": 10,
143 | "dashes": false,
144 | "datasource": "${DS_RPI_- ATLAS}",
145 | "fill": 1,
146 | "gridPos": {
147 | "h": 7,
148 | "w": 24,
149 | "x": 0,
150 | "y": 11
151 | },
152 | "id": 2,
153 | "legend": {
154 | "avg": false,
155 | "current": false,
156 | "max": false,
157 | "min": false,
158 | "show": true,
159 | "total": false,
160 | "values": false
161 | },
162 | "lines": false,
163 | "linewidth": 1,
164 | "links": [],
165 | "nullPointMode": "null",
166 | "percentage": false,
167 | "pointradius": 5,
168 | "points": false,
169 | "renderer": "flot",
170 | "seriesOverrides": [
171 | {
172 | "alias": "DS18B20",
173 | "color": "#eab839"
174 | }
175 | ],
176 | "spaceLength": 10,
177 | "stack": false,
178 | "steppedLine": false,
179 | "targets": [
180 | {
181 | "groupByAliases": [],
182 | "groupByColumns": [],
183 | "metricAggs": [
184 | {
185 | "column": "value",
186 | "type": "avg"
187 | }
188 | ],
189 | "rawQuery": true,
190 | "refId": "A",
191 | "resultFormat": "time_series",
192 | "target": "db.trend_value.aggregate( [ \n{ \"$match\" : { \"sensor_type\" : \"$var_sensor_type\", \"host_name\" : \"$var_host\" , \"sensor_name\" : \"ep\" , \"sensor_type\" : \"temperature\" , \"ts\" : { \"$gte\" : \"$from\", \"$lt\" : \"$to\" }}},\n{ \"$bucketAuto\" : { \"groupBy\" : \"$ts\", \n \"buckets\" : \"$dateBucketCount\", \n \"output\" : { \"maxValue\" : { \"$max\" : \"$trend_value\" } } } }, \n{ \"$project\" : { \"name\" : \"Enviro pHAT\", \"value\" : \"$maxValue\", \"ts\" : \"$_id.min\", \"_id\" : 0 } } ] )",
193 | "timeInterval": "auto_gf",
194 | "type": "timeserie",
195 | "whereClauses": []
196 | }
197 | ],
198 | "thresholds": [
199 | {
200 | "colorMode": "critical",
201 | "fill": true,
202 | "line": true,
203 | "op": "lt",
204 | "value": 0
205 | },
206 | {
207 | "colorMode": "ok",
208 | "fill": true,
209 | "line": true,
210 | "op": "gt",
211 | "value": 0
212 | }
213 | ],
214 | "timeFrom": null,
215 | "timeShift": null,
216 | "title": "Trend",
217 | "tooltip": {
218 | "shared": true,
219 | "sort": 0,
220 | "value_type": "individual"
221 | },
222 | "type": "graph",
223 | "xaxis": {
224 | "buckets": null,
225 | "mode": "time",
226 | "name": null,
227 | "show": true,
228 | "values": []
229 | },
230 | "yaxes": [
231 | {
232 | "decimals": 0,
233 | "format": "short",
234 | "label": null,
235 | "logBase": 1,
236 | "max": null,
237 | "min": null,
238 | "show": true
239 | },
240 | {
241 | "format": "short",
242 | "label": null,
243 | "logBase": 1,
244 | "max": null,
245 | "min": null,
246 | "show": true
247 | }
248 | ],
249 | "yaxis": {
250 | "align": false,
251 | "alignLevel": null
252 | }
253 | }
254 | ],
255 | "refresh": "5s",
256 | "schemaVersion": 16,
257 | "style": "dark",
258 | "tags": [],
259 | "templating": {
260 | "list": [
261 | {
262 | "allValue": null,
263 | "current": {},
264 | "datasource": "${DS_RPI_- ATLAS}",
265 | "hide": 0,
266 | "includeAll": false,
267 | "label": "host",
268 | "multi": false,
269 | "name": "var_host",
270 | "options": [],
271 | "query": "db.sensor_value.aggregate ( [ { \"$group\" : { \"_id\" : \"$host_name\" } } ] ) ",
272 | "refresh": 1,
273 | "regex": "",
274 | "sort": 1,
275 | "tagValuesQuery": "",
276 | "tags": [],
277 | "tagsQuery": "",
278 | "type": "query",
279 | "useTags": false
280 | },
281 | {
282 | "allValue": null,
283 | "current": {},
284 | "datasource": "${DS_RPI_- ATLAS}",
285 | "hide": 0,
286 | "includeAll": false,
287 | "label": "sensor",
288 | "multi": false,
289 | "name": "var_sensor_type",
290 | "options": [],
291 | "query": "db.sensor_value.aggregate ( [ { \"$group\" : { \"_id\" : \"$sensor_type\" } } ] )",
292 | "refresh": 1,
293 | "regex": "",
294 | "sort": 1,
295 | "tagValuesQuery": "",
296 | "tags": [],
297 | "tagsQuery": "",
298 | "type": "query",
299 | "useTags": false
300 | }
301 | ]
302 | },
303 | "time": {
304 | "from": "now-15m",
305 | "to": "now"
306 | },
307 | "timepicker": {
308 | "refresh_intervals": [
309 | "5s",
310 | "10s",
311 | "30s",
312 | "1m",
313 | "5m",
314 | "15m",
315 | "30m",
316 | "1h",
317 | "2h",
318 | "1d"
319 | ],
320 | "time_options": [
321 | "5m",
322 | "15m",
323 | "1h",
324 | "6h",
325 | "12h",
326 | "24h",
327 | "2d",
328 | "7d",
329 | "30d"
330 | ]
331 | },
332 | "timezone": "",
333 | "title": "RPI Mongo Bucket - Atlas CS",
334 | "uid": "000000016",
335 | "version": 9
336 | }
--------------------------------------------------------------------------------
/examples/RPI Mongo Bucket - Atlas Temp.json:
--------------------------------------------------------------------------------
1 | {
2 | "__inputs": [
3 | {
4 | "name": "DS_RPI_- ATLAS",
5 | "label": "RPI - Atlas",
6 | "description": "",
7 | "type": "datasource",
8 | "pluginId": "grafana-mongodb-opensource-datasource",
9 | "pluginName": "MongoDB"
10 | }
11 | ],
12 | "__requires": [
13 | {
14 | "type": "grafana",
15 | "id": "grafana",
16 | "name": "Grafana",
17 | "version": "5.1.3"
18 | },
19 | {
20 | "type": "datasource",
21 | "id": "grafana-mongodb-opensource-datasource",
22 | "name": "MongoDB",
23 | "version": "0.8.1"
24 | },
25 | {
26 | "type": "panel",
27 | "id": "graph",
28 | "name": "Graph",
29 | "version": "5.0.0"
30 | }
31 | ],
32 | "annotations": {
33 | "list": [
34 | {
35 | "builtIn": 1,
36 | "datasource": "-- Grafana --",
37 | "enable": true,
38 | "hide": true,
39 | "iconColor": "rgba(0, 211, 255, 1)",
40 | "name": "Annotations & Alerts",
41 | "type": "dashboard"
42 | }
43 | ]
44 | },
45 | "editable": true,
46 | "gnetId": null,
47 | "graphTooltip": 0,
48 | "id": null,
49 | "iteration": 1535309830712,
50 | "links": [],
51 | "panels": [
52 | {
53 | "aliasColors": {},
54 | "bars": false,
55 | "dashLength": 10,
56 | "dashes": false,
57 | "datasource": "${DS_RPI_- ATLAS}",
58 | "fill": 1,
59 | "gridPos": {
60 | "h": 14,
61 | "w": 24,
62 | "x": 0,
63 | "y": 0
64 | },
65 | "id": 1,
66 | "legend": {
67 | "avg": false,
68 | "current": false,
69 | "max": false,
70 | "min": false,
71 | "show": true,
72 | "total": false,
73 | "values": false
74 | },
75 | "lines": true,
76 | "linewidth": 1,
77 | "links": [],
78 | "nullPointMode": "null",
79 | "percentage": false,
80 | "pointradius": 5,
81 | "points": false,
82 | "renderer": "flot",
83 | "seriesOverrides": [
84 | {
85 | "alias": "value"
86 | }
87 | ],
88 | "spaceLength": 10,
89 | "stack": false,
90 | "steppedLine": false,
91 | "targets": [
92 | {
93 | "groupByAliases": [],
94 | "groupByColumns": [],
95 | "metricAggs": [
96 | {
97 | "column": "value",
98 | "type": "avg"
99 | }
100 | ],
101 | "rawQuery": true,
102 | "refId": "A",
103 | "resultFormat": "time_series",
104 | "target": "db.sensor_value.aggregate( [ \n{ \"$match\" : { \"sensor_type\" : \"$var_sensor_type\", \"host_name\" : \"$var_host\" , \"sensor_name\" : \"ep\" , \"ts\" : { \"$gte\" : \"$from\", \"$lt\" : \"$to\" }}},\n{ \"$bucketAuto\" : { \"groupBy\" : \"$ts\", \n \"buckets\" : \"$dateBucketCount\", \n \"output\" : { \"maxValue\" : { \"$max\" : \"$sensor_value\" } } } }, \n{ \"$project\" : { \"name\" : \"ep\", \"value\" : \"$maxValue\", \"ts\" : \"$_id.min\", \"_id\" : 0 } } ] \n,{ \"allowDiskUse\" : true } )",
105 | "timeInterval": "auto_gf",
106 | "type": "timeserie",
107 | "whereClauses": []
108 | },
109 | {
110 | "rawQuery": true,
111 | "refId": "B",
112 | "target": "db.sensor_value.aggregate( [ \n{ \"$match\" : { \"sensor_type\" : \"$var_sensor_type\", \"host_name\" : \"$var_host\" , \"sensor_name\" : \"lg-DS18B20\" , \"ts\" : { \"$gte\" : \"$from\", \"$lt\" : \"$to\" }}},\n{ \"$bucketAuto\" : { \"groupBy\" : \"$ts\", \n \"buckets\" : \"$dateBucketCount\", \n \"output\" : { \"maxValue\" : { \"$max\" : \"$sensor_value\" } } } }, \n{ \"$project\" : { \"name\" : \"DS18B20\", \"value\" : \"$maxValue\", \"ts\" : \"$_id.min\", \"_id\" : 0 } } ] \n,{ \"allowDiskUse\" : true })",
113 | "type": "timeserie"
114 | }
115 | ],
116 | "thresholds": [],
117 | "timeFrom": null,
118 | "timeShift": null,
119 | "title": "Sensor Value",
120 | "tooltip": {
121 | "shared": true,
122 | "sort": 0,
123 | "value_type": "individual"
124 | },
125 | "type": "graph",
126 | "xaxis": {
127 | "buckets": null,
128 | "mode": "time",
129 | "name": null,
130 | "show": true,
131 | "values": []
132 | },
133 | "yaxes": [
134 | {
135 | "format": "none",
136 | "label": null,
137 | "logBase": 1,
138 | "max": null,
139 | "min": null,
140 | "show": true
141 | },
142 | {
143 | "format": "short",
144 | "label": null,
145 | "logBase": 1,
146 | "max": null,
147 | "min": null,
148 | "show": true
149 | }
150 | ],
151 | "yaxis": {
152 | "align": false,
153 | "alignLevel": null
154 | }
155 | }
156 | ],
157 | "refresh": false,
158 | "schemaVersion": 16,
159 | "style": "dark",
160 | "tags": [],
161 | "templating": {
162 | "list": [
163 | {
164 | "allValue": null,
165 | "current": {},
166 | "datasource": "${DS_RPI_- ATLAS}",
167 | "hide": 0,
168 | "includeAll": false,
169 | "label": "host",
170 | "multi": false,
171 | "name": "var_host",
172 | "options": [],
173 | "query": "db.sensor_value.aggregate ( [ { \"$group\" : { \"_id\" : \"$host_name\" } } ] ) ",
174 | "refresh": 1,
175 | "regex": "",
176 | "sort": 1,
177 | "tagValuesQuery": "",
178 | "tags": [],
179 | "tagsQuery": "",
180 | "type": "query",
181 | "useTags": false
182 | },
183 | {
184 | "allValue": null,
185 | "current": {},
186 | "datasource": "${DS_RPI_- ATLAS}",
187 | "hide": 0,
188 | "includeAll": false,
189 | "label": "sensor",
190 | "multi": false,
191 | "name": "var_sensor_type",
192 | "options": [],
193 | "query": "db.sensor_value.aggregate ( [ { \"$group\" : { \"_id\" : \"$sensor_type\" } } ] )",
194 | "refresh": 1,
195 | "regex": "",
196 | "sort": 1,
197 | "tagValuesQuery": "",
198 | "tags": [],
199 | "tagsQuery": "",
200 | "type": "query",
201 | "useTags": false
202 | }
203 | ]
204 | },
205 | "time": {
206 | "from": "now/M",
207 | "to": "now"
208 | },
209 | "timepicker": {
210 | "refresh_intervals": [
211 | "5s",
212 | "10s",
213 | "30s",
214 | "1m",
215 | "5m",
216 | "15m",
217 | "30m",
218 | "1h",
219 | "2h",
220 | "1d"
221 | ],
222 | "time_options": [
223 | "5m",
224 | "15m",
225 | "1h",
226 | "6h",
227 | "12h",
228 | "24h",
229 | "2d",
230 | "7d",
231 | "30d"
232 | ]
233 | },
234 | "timezone": "",
235 | "title": "RPI Mongo Bucket - Atlas Temp",
236 | "uid": "000000015",
237 | "version": 7
238 | }
--------------------------------------------------------------------------------
/examples/Sensor Value Counts - Atlas.json:
--------------------------------------------------------------------------------
1 | {
2 | "__inputs": [
3 | {
4 | "name": "DS_RPI_- ATLAS",
5 | "label": "RPI - Atlas",
6 | "description": "",
7 | "type": "datasource",
8 | "pluginId": "grafana-mongodb-opensource-datasource",
9 | "pluginName": "MongoDB"
10 | }
11 | ],
12 | "__requires": [
13 | {
14 | "type": "grafana",
15 | "id": "grafana",
16 | "name": "Grafana",
17 | "version": "5.1.3"
18 | },
19 | {
20 | "type": "datasource",
21 | "id": "grafana-mongodb-opensource-datasource",
22 | "name": "MongoDB",
23 | "version": "1.3.5"
24 | },
25 | {
26 | "type": "panel",
27 | "id": "table",
28 | "name": "Table",
29 | "version": "5.0.0"
30 | }
31 | ],
32 | "annotations": {
33 | "list": [
34 | {
35 | "builtIn": 1,
36 | "datasource": "-- Grafana --",
37 | "enable": true,
38 | "hide": true,
39 | "iconColor": "rgba(0, 211, 255, 1)",
40 | "name": "Annotations & Alerts",
41 | "type": "dashboard"
42 | }
43 | ]
44 | },
45 | "editable": true,
46 | "gnetId": null,
47 | "graphTooltip": 0,
48 | "id": null,
49 | "links": [],
50 | "panels": [
51 | {
52 | "columns": [],
53 | "datasource": "${DS_RPI_- ATLAS}",
54 | "fontSize": "100%",
55 | "gridPos": {
56 | "h": 13,
57 | "w": 18,
58 | "x": 0,
59 | "y": 0
60 | },
61 | "id": 2,
62 | "links": [],
63 | "pageSize": null,
64 | "scroll": true,
65 | "showHeader": true,
66 | "sort": {
67 | "col": 0,
68 | "desc": true
69 | },
70 | "styles": [
71 | {
72 | "alias": "",
73 | "colorMode": null,
74 | "colors": [
75 | "rgba(245, 54, 54, 0.9)",
76 | "rgba(237, 129, 40, 0.89)",
77 | "rgba(50, 172, 45, 0.97)"
78 | ],
79 | "dateFormat": "YYYY-MM-DD HH:mm:ss",
80 | "decimals": 0,
81 | "mappingType": 1,
82 | "pattern": "Value",
83 | "thresholds": [],
84 | "type": "number",
85 | "unit": "none"
86 | },
87 | {
88 | "alias": "",
89 | "colorMode": null,
90 | "colors": [
91 | "rgba(245, 54, 54, 0.9)",
92 | "rgba(237, 129, 40, 0.89)",
93 | "rgba(50, 172, 45, 0.97)"
94 | ],
95 | "dateFormat": "YYYY-MM-DD HH:mm:ss",
96 | "decimals": 2,
97 | "mappingType": 1,
98 | "pattern": "Time",
99 | "thresholds": [],
100 | "type": "date",
101 | "unit": "short"
102 | },
103 | {
104 | "alias": "",
105 | "colorMode": null,
106 | "colors": [
107 | "rgba(245, 54, 54, 0.9)",
108 | "rgba(237, 129, 40, 0.89)",
109 | "rgba(50, 172, 45, 0.97)"
110 | ],
111 | "decimals": 2,
112 | "pattern": "/.*/",
113 | "thresholds": [],
114 | "type": "number",
115 | "unit": "short"
116 | }
117 | ],
118 | "targets": [
119 | {
120 | "groupByAliases": [],
121 | "groupByColumns": [],
122 | "metricAggs": [
123 | {
124 | "column": "value",
125 | "type": "avg"
126 | }
127 | ],
128 | "rawQuery": true,
129 | "refId": "A",
130 | "resultFormat": "time_series",
131 | "target": "db.sensor_value.aggregate(\n[\n { \"$match\" : { \"ts\" : { \"$gte\" : \"$from\", \"$lt\" : \"$to\" }}},\n { \"$group\": { \"_id\": { \"sensor_name\" : \"$sensor_name\", \"sensor_type\" : \"$sensor_type\" }, \"cnt\" : { \"$sum\" : 1 }, \"ts\" : { \"$max\" : \"$ts\" } } }, \n { \"$project\": { \"name\" : { \"$concat\" : [\"$_id.sensor_name\",\":\",\"$_id.sensor_type\" ]}, \"value\" : \"$cnt\", \"ts\" : 1, \"_id\" : 0} } \n])",
132 | "timeInterval": "auto_gf",
133 | "type": "timeserie",
134 | "whereClauses": []
135 | }
136 | ],
137 | "title": "Counts",
138 | "transform": "timeseries_to_rows",
139 | "type": "table"
140 | }
141 | ],
142 | "refresh": "5s",
143 | "schemaVersion": 16,
144 | "style": "dark",
145 | "tags": [],
146 | "templating": {
147 | "list": []
148 | },
149 | "time": {
150 | "from": "now/d",
151 | "to": "now"
152 | },
153 | "timepicker": {
154 | "refresh_intervals": [
155 | "5s",
156 | "10s",
157 | "30s",
158 | "1m",
159 | "5m",
160 | "15m",
161 | "30m",
162 | "1h",
163 | "2h",
164 | "1d"
165 | ],
166 | "time_options": [
167 | "5m",
168 | "15m",
169 | "1h",
170 | "6h",
171 | "12h",
172 | "24h",
173 | "2d",
174 | "7d",
175 | "30d"
176 | ]
177 | },
178 | "timezone": "",
179 | "title": "Sensor Value Counts",
180 | "uid": "9A9EQ3Omk",
181 | "version": 4
182 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grafana-mongodb",
3 | "private": true,
4 | "version": "0.9.2",
5 | "description": "",
6 | "main": "server/mongodb-proxy.js",
7 | "scripts": {
8 | "build": "NODE_ENV=production npx grunt",
9 | "test": "npx grunt mochaTest",
10 | "server": "cd dist/server && node mongodb-proxy.js",
11 | "sign": "grafana-toolkit plugin:sign --rootUrls https://grafana.cycling.market"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/JamesOsgood/mongodb-grafana.git"
16 | },
17 | "author": "James Osgood",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/JamesOsgood/mongodb-grafana/issues"
21 | },
22 | "devDependencies": {
23 | "babel": "^6.23.0",
24 | "babel-plugin-transform-es2015-for-of": "^6.6.0",
25 | "babel-preset-es2015": "^6.24.1",
26 | "chai": "^4.3.6",
27 | "grunt": "^1.0.3",
28 | "grunt-babel": "~6.0.0",
29 | "grunt-cli": "^1.2.0",
30 | "grunt-contrib-clean": "^1.1.0",
31 | "grunt-contrib-copy": "^1.0.0",
32 | "grunt-contrib-watch": "^1.1.0",
33 | "grunt-mocha-test": "^0.13.3",
34 | "grunt-systemjs-builder": "^1.0.0",
35 | "jsdom": "^20.0.0",
36 | "load-grunt-tasks": "^3.5.2",
37 | "mocha": "^10.0.0",
38 | "prunk": "^1.3.0",
39 | "q": "^1.5.0"
40 | },
41 | "dependencies": {
42 | "@grafana/toolkit": "^9.0.6",
43 | "body-parser": "^1.18.3",
44 | "bson": "^4.1.0",
45 | "config": "^3.3.7",
46 | "express": "^4.16.3",
47 | "lodash": "^4.17.10",
48 | "moment": "^2.22.1",
49 | "mongodb": "^4.8.1",
50 | "mongodb-query-parser": "^2.1.2",
51 | "statman-stopwatch": "^2.7.0"
52 | },
53 | "homepage": "https://github.com/JamesOsgood/mongodb-grafana#readme"
54 | }
55 |
--------------------------------------------------------------------------------
/server/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "server":
3 | {
4 | "port": 3333,
5 | "logRequests": false,
6 | "logQueries": false,
7 | "logTimings": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/mongodb-grafana-proxy.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | mongodb-grafana-proxy
7 |
8 | EnvironmentVariables
9 |
10 | PATH
11 | /usr/local/bin/:$PATH
12 |
13 |
14 | WorkingDirectory
15 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server
16 |
17 | ProgramArguments
18 |
19 | /usr/local/lib/npm-packages/bin/forever
20 | -l
21 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy-forever.log
22 | -o
23 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy.log
24 | -e
25 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy-err.log
26 | --workingDir
27 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server
28 | /usr/local/var/lib/grafana/plugins/mongodb-grafana/dist/server/mongodb-proxy.js
29 |
30 |
31 | RunAtLoad
32 |
33 |
34 | KeepAlive
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/server/mongodb-proxy.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var bodyParser = require('body-parser');
3 | var _ = require('lodash');
4 | var app = express();
5 | const MongoClient = require('mongodb').MongoClient;
6 | const assert = require('assert');
7 | var config = require('config');
8 | var Stopwatch = require("statman-stopwatch");
9 | var moment = require('moment')
10 | const { EJSON } = require('bson');
11 | var mongoParser = require('mongodb-query-parser');
12 | const { MutableDataFrame, FieldType } = require("@grafana/data")
13 |
14 | app.use(bodyParser.json());
15 |
16 | // Called by test
17 | app.all('/', function(req, res, next)
18 | {
19 | logRequest(req.body, "/")
20 | setCORSHeaders(res);
21 |
22 | MongoClient.connect(req.body.db.url, function(err, client)
23 | {
24 | if ( err != null )
25 | {
26 | res.send({ status : "error",
27 | display_status : "Error",
28 | message : 'MongoDB Connection Error: ' + err.message });
29 | }
30 | else
31 | {
32 | res.send( { status : "success",
33 | display_status : "Success",
34 | message : 'MongoDB Connection test OK' });
35 | }
36 | next()
37 | })
38 | });
39 |
40 | // Called by template functions and to look up variables
41 | app.all('/search', function(req, res, next)
42 | {
43 | logRequest(req.body, "/search")
44 | setCORSHeaders(res);
45 |
46 | // Generate an id to track requests
47 | const requestId = ++requestIdCounter
48 | // Add state for the queries in this request
49 | var queryStates = []
50 | requestsPending[requestId] = queryStates
51 | // Parse query string in target
52 | queryArgs = parseQuery(req.body.target, {})
53 | if (queryArgs.err != null)
54 | {
55 | console.log("Error in query parsing")
56 | queryError(requestId, queryArgs.err, next)
57 | }
58 | else
59 | {
60 | doTemplateQuery(requestId, queryArgs, req.body.db, res, next);
61 | }
62 | });
63 |
64 | // State for queries in flight. As results come it, acts as a semaphore and sends the results back
65 | var requestIdCounter = 0
66 | // Map of request id -> array of results. Results is
67 | // { query, err, output }
68 | var requestsPending = {}
69 |
70 | // Called when a query finishes with an error
71 | function queryError(requestId, err, next)
72 | {
73 | // We only 1 return error per query so it may have been removed from the list
74 | if ( requestId in requestsPending )
75 | {
76 | // Remove request
77 | delete requestsPending[requestId]
78 | // Send back error
79 | next(err)
80 | }
81 | }
82 |
83 | // Called when query finished
84 | function queryFinished(requestId, refId, results, res, next)
85 | {
86 | // We only 1 return error per query so it may have been removed from the list
87 | if ( requestId in requestsPending )
88 | {
89 | var queryStatus = requestsPending[requestId]
90 | // Mark this as finished
91 | queryStatus[refId].pending = false
92 | queryStatus[refId].results = results
93 |
94 | // See if we're all done
95 | var done = true
96 | for (const i in queryStatus)
97 | {
98 | if (queryStatus[i].pending == true )
99 | {
100 | done = false
101 | break
102 | }
103 | }
104 |
105 | // If query done, send back results
106 | if (done)
107 | {
108 | // Concatenate results
109 | output = []
110 | for (const i in queryStatus)
111 | {
112 | var queryResults = queryStatus[i].results
113 | var keys = Object.keys(queryResults)
114 | for (var k = 0; k < keys.length; k++)
115 | {
116 | var tg = keys[k]
117 | output.push(queryResults[tg])
118 | }
119 | }
120 | res.json(output);
121 | next()
122 | // Remove request
123 | delete requestsPending[requestId]
124 | }
125 | }
126 | }
127 |
128 | // Called to get graph points
129 | app.all('/query', function(req, res, next)
130 | {
131 | logRequest(req.body, "/query")
132 | setCORSHeaders(res);
133 |
134 | // Parse query string in target
135 | substitutions = { "$from" : new Date(req.body.range.from),
136 | "$to" : new Date(req.body.range.to),
137 | "$dateBucketCount" : getBucketCount(req.body.range.from, req.body.range.to, req.body.intervalMs)
138 | }
139 |
140 | // Generate an id to track requests
141 | const requestId = ++requestIdCounter
142 | // Add state for the queries in this request
143 | var queryStates = {}
144 | requestsPending[requestId] = queryStates
145 | var error = false
146 |
147 | for ( var queryId = 0; queryId < req.body.targets.length && !error; queryId++)
148 | {
149 | tg = req.body.targets[queryId]
150 | refId = tg.refId
151 | queryArgs = parseQuery(tg.target, substitutions)
152 | queryArgs.type = tg.type
153 | if (queryArgs.err != null)
154 | {
155 | console.log("Error in query parsing")
156 | queryError(requestId, queryArgs.err, next)
157 | error = true
158 | }
159 | else
160 | {
161 | // Add to the state
162 | queryStates[refId] = { pending : true }
163 |
164 | // Run the query
165 | runAggregateQuery( requestId, refId, req.body, queryArgs, res, next)
166 | }
167 | }
168 | }
169 | );
170 |
171 | app.use(function(error, req, res, next)
172 | {
173 | // Any request to this server will get here, and will send an HTTP
174 | // response with the error message
175 | res.status(500).json({ message: error.message });
176 | });
177 |
178 | // Get config from server/default.json
179 | var serverConfig = config.get('server');
180 |
181 | app.listen(serverConfig.port);
182 |
183 | console.log("Server is listening on port " + serverConfig.port);
184 |
185 | function setCORSHeaders(res)
186 | {
187 | res.setHeader("Access-Control-Allow-Origin", "*");
188 | res.setHeader("Access-Control-Allow-Methods", "POST");
189 | res.setHeader("Access-Control-Allow-Headers", "accept, content-type");
190 | }
191 |
192 | function forIn(obj, processFunc)
193 | {
194 | var key;
195 | for (key in obj)
196 | {
197 | var value = obj[key]
198 | processFunc(obj, key, value)
199 | if ( value != null && typeof(value) == "object")
200 | {
201 | forIn(value, processFunc)
202 | }
203 | }
204 | }
205 |
206 | function parseQuery(query, substitutions)
207 | {
208 | doc = {}
209 | queryErrors = []
210 |
211 | query = query.trim()
212 | if (query.substring(0,3) != "db.")
213 | {
214 | queryErrors.push("Query must start with db.")
215 | return null
216 | }
217 |
218 | // Query is of the form db..aggregate or db..find
219 | // Split on the first ( after db.
220 | var openBracketIndex = query.indexOf('(', 3)
221 | if (openBracketIndex == -1)
222 | {
223 | queryErrors.push("Can't find opening bracket")
224 | }
225 | else
226 | {
227 | // Split the first bit - it's the collection name and operation ( must be aggregate )
228 | var parts = query.substring(3, openBracketIndex).split('.')
229 | // Collection names can have .s so last part is operation, rest is the collection name
230 | if (parts.length >= 2)
231 | {
232 | doc.operation = parts.pop().trim()
233 | doc.collection = parts.join('.')
234 | }
235 | else
236 | {
237 | queryErrors.push("Invalid collection and operation syntax")
238 | }
239 |
240 | // Args is the rest up to the last bracket
241 | var closeBracketIndex = query.lastIndexOf(')')
242 | if (closeBracketIndex == -1)
243 | {
244 | queryErrors.push("Can't find last bracket")
245 | }
246 | else
247 | {
248 | var args = query.substring(openBracketIndex + 1, closeBracketIndex)
249 | if ( doc.operation == 'aggregate')
250 | {
251 | // Wrap args in array syntax so we can check for optional options arg
252 | args = '[' + args + ']'
253 | docs = mongoParser(args)
254 | // First Arg is pipeline
255 | doc.pipeline = docs[0]
256 | // If we have 2 top level args, second is agg options
257 | if ( docs.length == 2 )
258 | {
259 | doc.agg_options = docs[1]
260 | }
261 | // Replace with substitutions
262 | for ( var i = 0; i < doc.pipeline.length; i++)
263 | {
264 | var stage = doc.pipeline[i]
265 | forIn(stage, function (obj, key, value)
266 | {
267 | if ( typeof(value) == "string" )
268 | {
269 | if ( value in substitutions )
270 | {
271 | obj[key] = substitutions[value]
272 | }
273 | }
274 | })
275 | }
276 | }
277 | else
278 | {
279 | queryErrors.push("Unknown operation " + doc.operation + ", only aggregate supported")
280 | }
281 | }
282 | }
283 |
284 | if (queryErrors.length > 0 )
285 | {
286 | doc.err = new Error('Failed to parse query - ' + queryErrors.join(':'))
287 | }
288 |
289 | return doc
290 | }
291 |
292 | // Run an aggregate query. Must return documents of the form
293 | // { value : 0.34334, ts : }
294 |
295 | function runAggregateQuery( requestId, refId, body, queryArgs, res, next )
296 | {
297 | MongoClient.connect(body.db.url, function(err, client)
298 | {
299 | if ( err != null )
300 | {
301 | queryError(requestId, err, next)
302 | }
303 | else
304 | {
305 | const db = client.db(body.db.db);
306 |
307 | // Get the documents collection
308 | const collection = db.collection(queryArgs.collection);
309 | logQuery(queryArgs.pipeline, queryArgs.agg_options)
310 | var stopwatch = new Stopwatch(true)
311 |
312 | collection.aggregate(queryArgs.pipeline, queryArgs.agg_options).toArray(function(err, docs)
313 | {
314 | if ( err != null )
315 | {
316 | client.close();
317 | console.log("Error running aggregation query")
318 | queryError(requestId, err, next)
319 | }
320 | else
321 | {
322 | try
323 | {
324 | var results = {}
325 | if ( queryArgs.type == 'timeserie' )
326 | {
327 | results = getTimeseriesResults(docs, refId)
328 | }
329 | else
330 | {
331 | results = getTableResults(docs, refId)
332 | }
333 |
334 | client.close();
335 | var elapsedTimeMs = stopwatch.stop()
336 | logTiming(body, elapsedTimeMs)
337 | // Mark query as finished - will send back results when all queries finished
338 | queryFinished(requestId, refId, results, res, next)
339 | }
340 | catch(err)
341 | {
342 | console.log("Error returning results")
343 | queryError(requestId, err, next)
344 | }
345 | }
346 | })
347 | }
348 | })
349 | }
350 |
351 | function getTableResults(docs, refId)
352 | {
353 | var columns = {}
354 |
355 | // Build superset of columns
356 | for ( var i = 0; i < docs.length; i++)
357 | {
358 | var doc = docs[i]
359 | // Go through all properties
360 | for (var propName in doc )
361 | {
362 | // See if we need to add a new column
363 | if ( !(propName in columns) )
364 | {
365 | columns[propName] =
366 | {
367 | text : propName,
368 | type : "text"
369 | }
370 | }
371 | }
372 | }
373 |
374 | // Build return rows
375 | rows = []
376 | for ( var i = 0; i < docs.length; i++)
377 | {
378 | var doc = docs[i]
379 | row = []
380 | // All cols
381 | for ( var colName in columns )
382 | {
383 | var col = columns[colName]
384 | if ( col.text in doc )
385 | {
386 | row.push(doc[col.text])
387 | }
388 | else
389 | {
390 | row.push(null)
391 | }
392 | }
393 | rows.push(row)
394 | }
395 |
396 | var results = {}
397 | results["table"] = {
398 | refId : refId,
399 | columns : Object.values(columns),
400 | rows : rows,
401 | type : "table"
402 | }
403 | return results
404 | }
405 |
406 | function getTimeseriesResults(docs, refId)
407 | {
408 | var results = {}
409 | for ( var i = 0; i < docs.length; i++)
410 | {
411 | var doc = docs[i]
412 | if (results[refId] === undefined)
413 | {
414 | const dataframeCreator = { refId: refId, fields: [] }
415 |
416 | for (const field in doc)
417 | {
418 | switch(field)
419 | {
420 | case "time":
421 | dataframeCreator.fields.push({ name: 'time', type: FieldType.time })
422 | break;
423 | case "metric":
424 | dataframeCreator.fields.push({ name: 'metric', type: FieldType.string })
425 | break;
426 | default:
427 | dataframeCreator.fields.push({ name: field, type: FieldType.number })
428 | break;
429 | }
430 | }
431 | results[refId] = new MutableDataFrame(dataframeCreator)
432 | }
433 |
434 | const dataFrameValue = {}
435 | for (const field in doc)
436 | {
437 | switch(field)
438 | {
439 | case "time":
440 | if (doc["time"] === null)
441 | {
442 | dataFrameValue["time"] = 0 // TODO??
443 | }
444 | else
445 | {
446 | dataFrameValue["time"] = doc["time"].getTime()
447 | }
448 | break;
449 | default:
450 | dataFrameValue[field] = doc[field]
451 | break;
452 | }
453 | }
454 |
455 | results[refId].add(dataFrameValue)
456 | }
457 | return results
458 | }
459 | // Runs a query to support templates. Must returns documents of the form
460 | // { _id : }
461 | // { text : __text, value: __value }
462 | function doTemplateQuery(requestId, queryArgs, db, res, next)
463 | {
464 | if ( queryArgs.err == null)
465 | {
466 | // Database Name
467 | const dbName = db.db
468 |
469 | // Use connect method to connect to the server
470 | MongoClient.connect(db.url, function(err, client)
471 | {
472 | if ( err != null )
473 | {
474 | queryError(requestId, err, next )
475 | }
476 | else
477 | {
478 | // Remove request from list
479 | if ( requestId in requestsPending )
480 | {
481 | delete requestsPending[requestId]
482 | }
483 | const db = client.db(dbName);
484 | // Get the documents collection
485 | const collection = db.collection(queryArgs.collection);
486 |
487 | collection.aggregate(queryArgs.pipeline).toArray(function(err, result)
488 | {
489 | assert.equal(err, null)
490 |
491 | output = []
492 | for ( var i = 0; i < result.length; i++)
493 | {
494 | var doc = result[i]
495 | if ((doc["__text"] !== undefined) && (doc["__value"] !== undefined))
496 | {
497 | output.push({ text: doc["__text"], value: doc["__value"]})
498 | }
499 | else
500 | {
501 | output.push(doc["_id"])
502 | }
503 | }
504 | res.json(output);
505 | client.close()
506 | next()
507 | })
508 | }
509 | })
510 | }
511 | else
512 | {
513 | next(queryArgs.err)
514 | }
515 | }
516 |
517 | function logRequest(body, type)
518 | {
519 | if (serverConfig.logRequests)
520 | {
521 | console.log("REQUEST: " + type + ":\n" + EJSON.stringify(body, null, 2))
522 | }
523 | }
524 |
525 | function logQuery(query, options)
526 | {
527 | if (serverConfig.logQueries)
528 | {
529 | console.log("Query:")
530 | console.log(EJSON.stringify(query,null,2))
531 | if ( options != null )
532 | {
533 | console.log("Query Options:")
534 | console.log(EJSON.stringify(options,null,2))
535 | }
536 | }
537 | }
538 |
539 | function logTiming(body, elapsedTimeMs)
540 | {
541 |
542 | if (serverConfig.logTimings)
543 | {
544 | var range = new Date(body.range.to) - new Date(body.range.from)
545 | var diff = moment.duration(range)
546 |
547 | console.log("Request: " + intervalCount(diff, body.interval, body.intervalMs) + " - Returned in " + elapsedTimeMs.toFixed(2) + "ms")
548 | }
549 | }
550 |
551 | // Take a range as a moment.duration and a grafana interval like 30s, 1m etc
552 | // And return the number of intervals that represents
553 | function intervalCount(range, intervalString, intervalMs)
554 | {
555 | // Convert everything to seconds
556 | var rangeSeconds = range.asSeconds()
557 | var intervalsInRange = rangeSeconds / (intervalMs / 1000)
558 |
559 | var output = intervalsInRange.toFixed(0) + ' ' + intervalString + ' intervals'
560 | return output
561 | }
562 |
563 | function getBucketCount(from, to, intervalMs)
564 | {
565 | var boundaries = []
566 | var current = new Date(from).getTime()
567 | var toMs = new Date(to).getTime()
568 | var count = 0
569 | while ( current < toMs )
570 | {
571 | current += intervalMs
572 | count++
573 | }
574 |
575 | return count
576 | }
577 |
--------------------------------------------------------------------------------
/spec/datasource_spec.js:
--------------------------------------------------------------------------------
1 | import {Datasource} from "../module";
2 | import Q from "q";
3 |
4 | describe('GenericDatasource', function() {
5 | var ctx = {};
6 |
7 | beforeEach(function() {
8 | ctx.$q = Q;
9 | ctx.backendSrv = {};
10 | ctx.templateSrv = {};
11 | var jsonData = { mongodb_url : 'mongodb://localhost:27017', mongodb_db : 'test_db' }
12 | ctx.ds = new Datasource({jsonData : jsonData}, ctx.$q, ctx.backendSrv, ctx.templateSrv);
13 | });
14 |
15 | it('should return an empty array when no targets are set', function(done) {
16 | ctx.ds.query({targets: []}).then(function(result) {
17 | expect(result.data).to.have.length(0);
18 | done();
19 | });
20 | });
21 |
22 | it('should return the server results when a target is set', function(done) {
23 | ctx.backendSrv.datasourceRequest = function(request) {
24 | return ctx.$q.when({
25 | _request: request,
26 | data: [
27 | {
28 | target: 'X',
29 | datapoints: [1, 2, 3]
30 | }
31 | ]
32 | });
33 | };
34 |
35 | ctx.templateSrv.replace = function(data) {
36 | return data;
37 | }
38 |
39 | ctx.ds.query({targets: ['hits']}).then(function(result) {
40 | expect(result._request.data.targets).to.have.length(1);
41 | expect(result._request.data.db.url).to.equal('mongodb://localhost:27017');
42 | expect(result._request.data.db.db).to.equal('test_db');
43 |
44 | var series = result.data[0];
45 | expect(series.target).to.equal('X');
46 | expect(series.datapoints).to.have.length(3);
47 | done();
48 | });
49 | });
50 |
51 | it ('should return the metric results when a target is null', function(done) {
52 | ctx.backendSrv.datasourceRequest = function(request) {
53 | return ctx.$q.when({
54 | _request: request,
55 | data: [
56 | "metric_0",
57 | "metric_1",
58 | "metric_2",
59 | ]
60 | });
61 | };
62 |
63 | ctx.templateSrv.replace = function(data) {
64 | return data;
65 | }
66 |
67 | ctx.ds.metricFindQuery({target: null}).then(function(result) {
68 | expect(result).to.have.length(3);
69 | expect(result[0].text).to.equal('metric_0');
70 | expect(result[0].value).to.equal('metric_0');
71 | expect(result[1].text).to.equal('metric_1');
72 | expect(result[1].value).to.equal('metric_1');
73 | expect(result[2].text).to.equal('metric_2');
74 | expect(result[2].value).to.equal('metric_2');
75 | done();
76 | });
77 | });
78 |
79 | it ('should return the metric target results when a target is set', function(done) {
80 | ctx.backendSrv.datasourceRequest = function(request) {
81 | var target = request.data.target;
82 | var result = [target + "_0", target + "_1", target + "_2"];
83 |
84 | return ctx.$q.when({
85 | _request: request,
86 | data: result
87 | });
88 | };
89 |
90 | ctx.templateSrv.replace = function(data) {
91 | return data;
92 | }
93 |
94 | ctx.ds.metricFindQuery('search').then(function(result) {
95 | expect(result).to.have.length(3);
96 | expect(result[0].text).to.equal('search_0');
97 | expect(result[0].value).to.equal('search_0');
98 | expect(result[1].text).to.equal('search_1');
99 | expect(result[1].value).to.equal('search_1');
100 | expect(result[2].text).to.equal('search_2');
101 | expect(result[2].value).to.equal('search_2');
102 | done();
103 | });
104 | });
105 |
106 | it ('should return the metric results when the target is an empty string', function(done) {
107 | ctx.backendSrv.datasourceRequest = function(request) {
108 | return ctx.$q.when({
109 | _request: request,
110 | data: [
111 | "metric_0",
112 | "metric_1",
113 | "metric_2",
114 | ]
115 | });
116 | };
117 |
118 | ctx.templateSrv.replace = function(data) {
119 | return data;
120 | }
121 |
122 | ctx.ds.metricFindQuery('').then(function(result) {
123 | expect(result).to.have.length(3);
124 | expect(result[0].text).to.equal('metric_0');
125 | expect(result[0].value).to.equal('metric_0');
126 | expect(result[1].text).to.equal('metric_1');
127 | expect(result[1].value).to.equal('metric_1');
128 | expect(result[2].text).to.equal('metric_2');
129 | expect(result[2].value).to.equal('metric_2');
130 | done();
131 | });
132 | });
133 |
134 | it ('should return the metric results when the args are an empty object', function(done) {
135 | ctx.backendSrv.datasourceRequest = function(request) {
136 | return ctx.$q.when({
137 | _request: request,
138 | data: [
139 | "metric_0",
140 | "metric_1",
141 | "metric_2",
142 | ]
143 | });
144 | };
145 |
146 | ctx.templateSrv.replace = function(data) {
147 | return data;
148 | }
149 |
150 | ctx.ds.metricFindQuery().then(function(result) {
151 | expect(result).to.have.length(3);
152 | expect(result[0].text).to.equal('metric_0');
153 | expect(result[0].value).to.equal('metric_0');
154 | expect(result[1].text).to.equal('metric_1');
155 | expect(result[1].value).to.equal('metric_1');
156 | expect(result[2].text).to.equal('metric_2');
157 | expect(result[2].value).to.equal('metric_2');
158 | done();
159 | });
160 | });
161 |
162 | it ('should return the metric target results when the args are a string', function(done) {
163 | ctx.backendSrv.datasourceRequest = function(request) {
164 | var target = request.data.target;
165 | var result = [target + "_0", target + "_1", target + "_2"];
166 |
167 | return ctx.$q.when({
168 | _request: request,
169 | data: result
170 | });
171 | };
172 |
173 | ctx.templateSrv.replace = function(data) {
174 | return data;
175 | }
176 |
177 | ctx.ds.metricFindQuery('search').then(function(result) {
178 | expect(result).to.have.length(3);
179 | expect(result[0].text).to.equal('search_0');
180 | expect(result[0].value).to.equal('search_0');
181 | expect(result[1].text).to.equal('search_1');
182 | expect(result[1].value).to.equal('search_1');
183 | expect(result[2].text).to.equal('search_2');
184 | expect(result[2].value).to.equal('search_2');
185 | done();
186 | });
187 | });
188 |
189 | it ('should return data as text and as value', function(done) {
190 | var result = ctx.ds.mapToTextValue({data: ["zero", "one", "two"]});
191 |
192 | expect(result).to.have.length(3);
193 | expect(result[0].text).to.equal('zero');
194 | expect(result[0].value).to.equal('zero');
195 | expect(result[1].text).to.equal('one');
196 | expect(result[1].value).to.equal('one');
197 | expect(result[2].text).to.equal('two');
198 | expect(result[2].value).to.equal('two');
199 | done();
200 | });
201 |
202 | it ('should return text as text and value as value', function(done) {
203 | var data = [
204 | {text: "zero", value: "value_0"},
205 | {text: "one", value: "value_1"},
206 | {text: "two", value: "value_2"},
207 | ];
208 |
209 | var result = ctx.ds.mapToTextValue({data: data});
210 |
211 | expect(result).to.have.length(3);
212 | expect(result[0].text).to.equal('zero');
213 | expect(result[0].value).to.equal('value_0');
214 | expect(result[1].text).to.equal('one');
215 | expect(result[1].value).to.equal('value_1');
216 | expect(result[2].text).to.equal('two');
217 | expect(result[2].value).to.equal('value_2');
218 | done();
219 | });
220 |
221 | it ('should return data as text and index as value', function(done) {
222 | var data = [
223 | {a: "zero", b: "value_0"},
224 | {a: "one", b: "value_1"},
225 | {a: "two", b: "value_2"},
226 | ];
227 |
228 | var result = ctx.ds.mapToTextValue({data: data});
229 |
230 | expect(result).to.have.length(3);
231 | expect(result[0].text).to.equal(data[0]);
232 | expect(result[0].value).to.equal(0);
233 | expect(result[1].text).to.equal(data[1]);
234 | expect(result[1].value).to.equal(1);
235 | expect(result[2].text).to.equal(data[2]);
236 | expect(result[2].value).to.equal(2);
237 | done();
238 | });
239 | });
240 |
--------------------------------------------------------------------------------
/spec/test-main.js:
--------------------------------------------------------------------------------
1 | import prunk from 'prunk';
2 | import { JSDOM } from 'jsdom';
3 | import chai from 'chai';
4 |
5 | // Mock Grafana modules that are not available outside of the core project
6 | // Required for loading module.js
7 | prunk.mock('./css/query-editor.css!', 'no css, dude.');
8 | prunk.mock('app/plugins/sdk', {
9 | QueryCtrl: null
10 | });
11 |
12 | // Setup jsdom
13 | // Required for loading angularjs
14 | global.document = new JSDOM('');
15 | global.window = global.document.parentWindow;
16 |
17 | // Setup Chai
18 | chai.should();
19 | global.assert = chai.assert;
20 | global.expect = chai.expect;
21 |
--------------------------------------------------------------------------------
/src/css/query-editor.css:
--------------------------------------------------------------------------------
1 | .generic-datasource-query-row .query-keyword {
2 | width: 75px;
3 | }
--------------------------------------------------------------------------------
/src/datasource.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | export class GenericDatasource {
4 |
5 | constructor(instanceSettings, $q, backendSrv, templateSrv) {
6 | this.type = instanceSettings.type;
7 | this.url = instanceSettings.url;
8 | this.name = instanceSettings.name;
9 | this.db = { 'url' : instanceSettings.jsonData.mongodb_url, 'db' : instanceSettings.jsonData.mongodb_db }
10 | this.q = $q;
11 | this.backendSrv = backendSrv;
12 | this.templateSrv = templateSrv;
13 | this.withCredentials = instanceSettings.withCredentials;
14 | this.headers = {'Content-Type': 'application/json'};
15 | if (typeof instanceSettings.basicAuth === 'string' && instanceSettings.basicAuth.length > 0) {
16 | this.headers['Authorization'] = instanceSettings.basicAuth;
17 | }
18 | }
19 |
20 | query(options) {
21 | var query = this.buildQueryParameters(options);
22 | query.targets = query.targets.filter(t => !t.hide);
23 | query.db = this.db
24 |
25 | if (query.targets.length <= 0) {
26 | return this.q.when({data: []});
27 | }
28 |
29 | return this.doRequest({
30 | url: this.url + '/query',
31 | data: query,
32 | method: 'POST'
33 | });
34 | }
35 |
36 | testDatasource() {
37 | return this.doRequest({
38 | url: this.url + '/',
39 | data : { db : this.db },
40 | method: 'POST',
41 | }).then(response => {
42 | if (response.status === 200) {
43 | return { status: response.data.status, message: response.data.message, title: response.data.display_status };
44 | }
45 | });
46 | }
47 |
48 | annotationQuery(options) {
49 | var query = this.templateSrv.replace(options.annotation.query, {}, 'glob');
50 | var annotationQuery = {
51 | range: options.range,
52 | annotation: {
53 | name: options.annotation.name,
54 | datasource: options.annotation.datasource,
55 | enable: options.annotation.enable,
56 | iconColor: options.annotation.iconColor,
57 | query: query
58 | },
59 | rangeRaw: options.rangeRaw
60 | };
61 |
62 | return this.doRequest({
63 | url: this.url + '/annotations',
64 | method: 'POST',
65 | data: annotationQuery
66 | }).then(result => {
67 | response.data.$$status = result.status;
68 | response.data.$$config = result.config;
69 | return result.data;
70 | });
71 | }
72 |
73 | metricFindQuery(query) {
74 | var interpolated = {
75 | target: this.templateSrv.replace(query, null, '')
76 | };
77 | interpolated.db = this.db
78 |
79 | return this.doRequest({
80 | url: this.url + '/search',
81 | data: interpolated,
82 | method: 'POST',
83 | }).then(this.mapToTextValue);
84 | }
85 |
86 | mapToTextValue(result) {
87 | return _.map(result.data, (d, i) => {
88 | if (d && d.text && d.value) {
89 | return { text: d.text, value: d.value };
90 | } else if (_.isObject(d)) {
91 | return { text: d, value: i};
92 | }
93 | return { text: d, value: d };
94 | });
95 | }
96 |
97 | doRequest(options) {
98 | options.withCredentials = this.withCredentials;
99 | options.headers = this.headers;
100 |
101 | return this.backendSrv.datasourceRequest(options);
102 | }
103 |
104 | buildQueryParameters(options) {
105 | //remove place holder targets
106 | options.targets = _.filter(options.targets, target => {
107 | return target.target !== 'select metric';
108 | });
109 |
110 | var targets = _.map(options.targets, target => {
111 | return {
112 | target: this.templateSrv.replace(target.target, options.scopedVars, ''),
113 | refId: target.refId,
114 | hide: target.hide,
115 | type: target.type || 'timeserie'
116 | };
117 | });
118 |
119 | options.targets = targets;
120 |
121 | return options;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/src/img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg
--------------------------------------------------------------------------------
/src/img/sample_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/src/img/sample_dashboard.png
--------------------------------------------------------------------------------
/src/img/sample_datasource.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/src/img/sample_datasource.png
--------------------------------------------------------------------------------
/src/img/sample_query.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/src/img/sample_query.png
--------------------------------------------------------------------------------
/src/img/sample_template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/src/img/sample_template.png
--------------------------------------------------------------------------------
/src/img/table_panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SiemaApplications-attic/mongodb-grafana/5a436f99ace7f149c81ec655aaa28127077c8e32/src/img/table_panel.png
--------------------------------------------------------------------------------
/src/module.js:
--------------------------------------------------------------------------------
1 | import {GenericDatasource} from './datasource';
2 | import {GenericDatasourceQueryCtrl} from './query_ctrl';
3 |
4 | class GenericConfigCtrl {}
5 | GenericConfigCtrl.templateUrl = 'partials/config.html';
6 |
7 | class GenericQueryOptionsCtrl {}
8 | GenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html';
9 |
10 | class GenericAnnotationsQueryCtrl {}
11 | GenericAnnotationsQueryCtrl.templateUrl = 'partials/annotations.editor.html'
12 |
13 | export {
14 | GenericDatasource as Datasource,
15 | GenericDatasourceQueryCtrl as QueryCtrl,
16 | GenericConfigCtrl as ConfigCtrl,
17 | GenericQueryOptionsCtrl as QueryOptionsCtrl,
18 | GenericAnnotationsQueryCtrl as AnnotationsQueryCtrl
19 | };
20 |
--------------------------------------------------------------------------------
/src/partials/annotations.editor.html:
--------------------------------------------------------------------------------
1 |
2 | Query
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/partials/config.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MongoDB details
5 |
6 |
25 |
26 |
--------------------------------------------------------------------------------
/src/partials/query.editor.html:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
--------------------------------------------------------------------------------
/src/partials/query.options.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MongoDB",
3 | "id": "grafana-mongodb-opensource-datasource",
4 | "type": "datasource",
5 |
6 | "partials": {
7 | "config": "public/app/plugins/datasource/simplejson/partials/config.html"
8 | },
9 |
10 | "metrics": true,
11 | "annotations": false,
12 |
13 | "info": {
14 | "description": "MongoDB datasource (opensource)",
15 | "author": {
16 | "name": "James Osgood"
17 | },
18 | "logos": {
19 | "small": "img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg",
20 | "large": "img/MongoDB_Gray_Logo_FullColor_RGB-01.jpg"
21 | },
22 | "links": [
23 | {"name": "GitHub", "url": "https://github.com/grafana/simple-json-datasource"},
24 | {"name": "MIT License", "url": "https://github.com/grafana/simple-json-datasource/blob/master/LICENSE"}
25 | ],
26 | "version": "0.9.2",
27 | "updated": "2022-09-22"
28 | },
29 |
30 | "dependencies": {
31 | "grafanaVersion": "8.x.x",
32 | "plugins": [ ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/query_ctrl.js:
--------------------------------------------------------------------------------
1 | import {QueryCtrl} from 'app/plugins/sdk';
2 | import './css/query-editor.css!'
3 |
4 | export class GenericDatasourceQueryCtrl extends QueryCtrl {
5 |
6 | constructor($scope, $injector) {
7 | super($scope, $injector);
8 |
9 | this.scope = $scope;
10 | this.target.target = this.target.target || 'select metric';
11 | this.target.type = this.target.type || 'timeserie';
12 | this.target.rawQuery = true;
13 | }
14 |
15 | getOptions(query) {
16 | return this.datasource.metricFindQuery(query || '');
17 | }
18 |
19 | toggleEditorMode() {
20 | this.target.rawQuery = !this.target.rawQuery;
21 | }
22 |
23 | onChangeInternal() {
24 | this.panelCtrl.refresh(); // Asks the panel to refresh data.
25 | }
26 | }
27 |
28 | GenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html';
29 |
30 |
--------------------------------------------------------------------------------