Skip to content

Commit 5be3cb4

Browse files
authored
Support more aggregate options and Improve Editor UI (#33)
* Support more aggregate options; Update UI * Add link to legacy plugin * Improve language selection UI * Add SetBypassDocumentValidation in the backend * Improve loggings * Change JavaScriptShadow label in test * Remove \t from label * Update editor layout * Update screenshot * Update README
1 parent dab570f commit 5be3cb4

File tree

9 files changed

+176
-55
lines changed

9 files changed

+176
-55
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
# Grafana MongoDB Data Source
22

3-
Integrate MongoDB to Grafana
3+
Integrate MongoDB to Grafana. A free, open source, community-driven alternative to Grafana Lab's MongoDB enterprise plugin and MongoDB Atlas Charts.
44

55
![ci](https://github.com/haohanyang/mongodb-datasource/actions/workflows/ci.yml/badge.svg?branch=master)
66

77
This plugin enables you to query and visualize data from your MongoDB databases directly within Grafana. Leverage the flexibility of MongoDB's aggregation pipeline to create insightful dashboards and panels.
88

9-
![screenshot](/static/screenshot.png)
9+
![screenshot](/static/screenshot-2.png)
1010

1111
## Features
1212

1313
- **Flexible Querying:** Query data using MongoDB's aggregation pipeline syntax in JSON or JavaScript. Support query variables to create dynamic dashboards.
1414
- **Time Series & Table Data:** Visualize time-based data or display results in tabular format for various Grafana panels.
1515
- **MongoDB Atlas Support** Connect to MongoDB Atlas Services.
1616
- **Grafana Alerting Support** Set up alerting rules based on query result
17-
- **Legacy Plugin Compatibility:** Easy migrate from the legacy plugin with support for its query syntax.
17+
- **[Legacy Plugin](https://github.com/JamesOsgood/mongodb-grafana) Compatibility:** Easy migrate from the [legacy plugin](https://github.com/JamesOsgood/mongodb-grafana) with support for its query syntax.
1818

1919
## Authentication methods
2020
* No authentication
@@ -73,7 +73,7 @@ Provide the collection name and your MongoDB aggregation pipeline in standard JS
7373

7474
### JavaScript (Legacy & ShadowRealm)
7575

76-
- **Legacy:** Maintain compatibility with the older plugin's syntax:
76+
- **Legacy:** Maintain compatibility with the [legacy plugin](https://github.com/JamesOsgood/mongodb-grafana)'s syntax:
7777
```javascript
7878
db.listingsAndReviews.aggregate([ /* Your aggregation pipeline (JSON) */ ]);
7979
```

pkg/plugin/datasource.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func NewDatasource(ctx context.Context, source backend.DataSourceInstanceSetting
3131

3232
config, err := models.LoadPluginSettings(source)
3333
if err != nil {
34-
backend.Logger.Error(fmt.Sprintf("Failed to load plugin settings: %s", err.Error()))
34+
backend.Logger.Error("Failed to load plugin settings", "error", err)
3535
return nil, err
3636
}
3737

@@ -84,8 +84,9 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques
8484
}
8585

8686
func (d *Datasource) query(ctx context.Context, _ backend.PluginContext, query backend.DataQuery) backend.DataResponse {
87+
backend.Logger.Debug("Executing query", "refId", query.RefID, "json", query.JSON)
88+
8789
var response backend.DataResponse
88-
backend.Logger.Debug("Raw query", query.JSON)
8990
var qm queryModel
9091
db := d.client.Database(d.database)
9192

@@ -105,23 +106,58 @@ func (d *Datasource) query(ctx context.Context, _ backend.PluginContext, query b
105106
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Failed to unmarshal JsonExt: %v", err.Error()))
106107
}
107108

109+
// Set aggregate options
108110
aggregateOpts := options.AggregateOptions{}
109-
if qm.Timeout > 0 {
110-
aggregateOpts.MaxTime = pointer(time.Hour * time.Duration(qm.Timeout))
111-
backend.Logger.Debug("Aggregate timeout was set", "timeout", qm.Timeout)
111+
112+
if qm.AggregateMaxTimeMS > 0 {
113+
aggregateOpts.SetMaxTime(time.Hour * time.Duration(qm.AggregateMaxTimeMS))
114+
115+
backend.Logger.Debug("Aggregate option was set", "maxTime", qm.AggregateMaxTimeMS)
116+
}
117+
118+
if qm.AggregateComment != "" {
119+
aggregateOpts.SetComment(qm.AggregateComment)
120+
121+
backend.Logger.Debug("Aggregate option was set", "comment", qm.AggregateComment)
122+
}
123+
124+
if qm.AggregateBatchSize > 0 {
125+
aggregateOpts.SetBatchSize(qm.AggregateBatchSize)
126+
127+
backend.Logger.Debug("Aggregate option was set", "batchSize", qm.AggregateBatchSize)
128+
}
129+
130+
if qm.AggregateAllowDiskUse {
131+
aggregateOpts.SetAllowDiskUse(qm.AggregateAllowDiskUse)
132+
133+
backend.Logger.Debug("Aggregate option was set", "allowDiskUse", qm.AggregateAllowDiskUse)
134+
}
135+
136+
if qm.AggregateMaxAwaitTime > 0 {
137+
aggregateOpts.SetMaxAwaitTime(time.Hour * time.Duration(qm.AggregateMaxAwaitTime))
138+
139+
backend.Logger.Debug("Aggregate option was set", "maxAwaitTime", qm.AggregateMaxAwaitTime)
140+
}
141+
142+
if qm.AggregateBypassDocumentValidation {
143+
aggregateOpts.SetBypassDocumentValidation(qm.AggregateBypassDocumentValidation)
144+
145+
backend.Logger.Debug("Aggregate option was set", "bypassDocumentValidation", qm.AggregateBypassDocumentValidation)
112146
}
113147

114148
cursor, err := db.Collection(qm.Collection).Aggregate(ctx, pipeline, &aggregateOpts)
115149
if err != nil {
116-
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Failed to query: %v", err.Error()))
150+
backend.Logger.Error("Failed to execute aggregate", "error", err)
117151

152+
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Failed to query: %v", err.Error()))
118153
}
154+
119155
defer cursor.Close(ctx)
120156

121157
if qm.QueryType == "table" {
122158
frame, err := CreateTableFramesFromQuery(ctx, query.RefID, cursor)
123159
if err != nil {
124-
backend.Logger.Error(err.Error())
160+
backend.Logger.Error("Failed to create data frame from query", "error", err)
125161
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Failed to query: %v", err.Error()))
126162
}
127163

@@ -130,7 +166,8 @@ func (d *Datasource) query(ctx context.Context, _ backend.PluginContext, query b
130166
} else {
131167
frames, err := CreateTimeSeriesFramesFromQuery(ctx, cursor)
132168
if err != nil {
133-
backend.Logger.Error(err.Error())
169+
backend.Logger.Error("Failed to create time series frames from query", "error", err)
170+
134171
return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("Failed to query: %v", err.Error()))
135172
}
136173

@@ -148,10 +185,13 @@ func (d *Datasource) query(ctx context.Context, _ backend.PluginContext, query b
148185
// a datasource is working as expected.
149186
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
150187
res := &backend.CheckHealthResult{}
188+
151189
backend.Logger.Debug("Checking health")
152190

153191
config, err := models.LoadPluginSettings(*req.PluginContext.DataSourceInstanceSettings)
154192
if err != nil {
193+
backend.Logger.Error("Failed to load settings", "error", err)
194+
155195
res.Status = backend.HealthStatusError
156196
res.Message = "Unable to load settings"
157197
return res, nil
@@ -168,12 +208,13 @@ func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRe
168208
if err != nil {
169209
res.Status = backend.HealthStatusError
170210
res.Message = err.Error()
171-
backend.Logger.Error(err.Error())
211+
212+
backend.Logger.Error("Failed to connect to db", "error", err)
172213
return res, nil
173214
}
174215
defer func() {
175216
if err = client.Disconnect(ctx); err != nil {
176-
backend.Logger.Error(fmt.Sprintf("Failed to disconnect db: %s", err.Error()))
217+
backend.Logger.Error("Failed to disconnect to db", "error", err)
177218
}
178219
}()
179220

pkg/plugin/types.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ type queryModel struct {
1818
Collection string `json:"collection"`
1919
QueryType string `json:"queryType"`
2020
QueryLanguage string `json:"queryLanguage"`
21-
Timeout int `json:"timeout"`
22-
}
2321

22+
// Aggregate options
23+
AggregateComment string `json:"aggregateComment"`
24+
AggregateMaxTimeMS int `json:"aggregateMaxTimeMS"`
25+
AggregateBatchSize int32 `json:"aggregateBatchSize"`
26+
AggregateAllowDiskUse bool `json:"aggregateAllowDiskUse"`
27+
AggregateMaxAwaitTime int `json:"aggregateMaxAwaitTime"`
28+
AggregateBypassDocumentValidation bool `json:"aggregateBypassDocumentValidation"`
29+
}
2430
type TimeSeriesRow[T any] struct {
2531
Timestamp time.Time `bson:"ts"`
2632
Name string `bson:"name"`

src/components/ConfigEditor.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, { ChangeEvent } from "react";
22
import { Divider, Field, FieldSet, InlineField, InlineFieldRow, Input, RadioButtonGroup, SecretInput } from "@grafana/ui";
33
import { DataSourcePluginOptionsEditorProps, SelectableValue } from "@grafana/data";
44
import { MongoDataSourceOptions, MySecureJsonData, MongoDBAuthMethod, ConnectionStringScheme } from "../types";
5-
;
65

76
interface Props extends DataSourcePluginOptionsEditorProps<MongoDataSourceOptions, MySecureJsonData> { }
87

src/components/QueryEditor.tsx

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,52 @@ import {
33
ActionMeta,
44
Button,
55
CodeEditor,
6-
Divider,
76
Field,
87
InlineField,
98
InlineFieldRow,
109
Input,
1110
Select,
12-
11+
ControlledCollapse,
12+
InlineSwitch,
13+
RadioButtonGroup
1314
} from "@grafana/ui";
1415
import { QueryEditorProps, SelectableValue } from "@grafana/data";
1516
import { DataSource } from "../datasource";
1617
import { MongoDataSourceOptions, MongoQuery, QueryLanguage, QueryType, DEFAULT_QUERY } from "../types";
17-
import { parseJsQuery, parseJsQueryLegacy, validateJsonQueryText, validateTimeout } from "../utils";
18+
import { parseJsQuery, parseJsQueryLegacy, validateJsonQueryText, validatePositiveNumber } from "../utils";
1819
import * as monacoType from "monaco-editor/esm/vs/editor/editor.api";
1920

2021
type Props = QueryEditorProps<DataSource, MongoQuery, MongoDataSourceOptions>;
2122

2223
const queryTypes: Array<SelectableValue<string>> = [
2324
{
2425
label: "Time series",
25-
value: QueryType.TIMESERIES
26+
value: QueryType.TIMESERIES,
27+
icon: "chart-line"
2628
},
2729
{
28-
label: "Table",
29-
value: QueryType.TABLE
30+
label: "Data Table",
31+
value: QueryType.TABLE,
32+
icon: "table"
3033
}
3134
];
3235

36+
const languageOptions: Array<SelectableValue<string>> = [
37+
{ label: "JSON", value: QueryLanguage.JSON },
38+
{ label: "JavaScript", value: QueryLanguage.JAVASCRIPT, description: "JavaScript Legacy" },
39+
{ label: "JavaScript Shadow", value: QueryLanguage.JAVASCRIPT_SHADOW, description: "JavaScript with Evaluation" }
40+
];
41+
3342

3443
export function QueryEditor({ query, onChange, onRunQuery }: Props) {
3544

3645
const codeEditorRef = useRef<monacoType.editor.IStandaloneCodeEditor | null>(null);
3746
const [queryTextError, setQueryTextError] = useState<string | null>(null);
38-
const [timeoutText, setTimeoutText] = useState<string>(query.timeout ? query.timeout.toString() : "");
47+
const [isOpen, setIsOpen] = useState(false);
3948

40-
const optionsLanguage = [
41-
{ label: "JSON", value: QueryLanguage.JSON },
42-
{ label: "JavaScript", value: QueryLanguage.JAVASCRIPT, description: "javascript legacy" },
43-
{ label: "JavaScriptShadow", value: QueryLanguage.JAVASCRIPT_SHADOW, description: "javascript with evaluation" }
44-
];
49+
const [maxTimeMSText, setMaxTimeMSText] = useState<string>(query.aggregateMaxTimeMS ? query.aggregateMaxTimeMS.toString() : "");
50+
const [maxAwaitTimeMSText, setMaxAwaitTimeMSText] = useState<string>(query.aggregateMaxAwaitTime ? query.aggregateMaxAwaitTime.toString() : "");
51+
const [batchSizeText, setBatchSizeText] = useState<string>(query.aggregateBatchSize ? query.aggregateBatchSize.toString() : "");
4552

4653
const onQueryTextChange = (queryText: string) => {
4754
if (query.queryLanguage === QueryLanguage.JAVASCRIPT || query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW) {
@@ -67,24 +74,56 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
6774
onChange({ ...query, queryLanguage: value.value });
6875
};
6976

70-
const onQueryTypeChange = (sv: SelectableValue<string>) => {
71-
onChange({ ...query, queryType: sv.value });
77+
const onQueryTypeChange = (value: string) => {
78+
onChange({ ...query, queryType: value });
7279
};
7380

7481
const onCollectionChange = (event: ChangeEvent<HTMLInputElement>) => {
7582
onChange({ ...query, collection: event.target.value });
7683
};
7784

78-
const onTimeoutChange = (event: ChangeEvent<HTMLInputElement>) => {
79-
setTimeoutText(event.target.value);
85+
const onMaxTimeMSChange = (event: ChangeEvent<HTMLInputElement>) => {
86+
setMaxTimeMSText(event.target.value);
8087
console.log(event.target.value);
8188
if (!event.target.value) {
82-
onChange({ ...query, timeout: undefined });
83-
} else if (validateTimeout(event.target.value)) {
84-
onChange({ ...query, timeout: parseInt(event.target.value, 10) });
89+
onChange({ ...query, aggregateMaxTimeMS: undefined });
90+
} else if (validatePositiveNumber(event.target.value)) {
91+
onChange({ ...query, aggregateMaxTimeMS: parseInt(event.target.value, 10) });
92+
}
93+
};
94+
95+
const onMaxAwaitTimeMSChange = (event: ChangeEvent<HTMLInputElement>) => {
96+
setMaxAwaitTimeMSText(event.target.value);
97+
if (!event.target.value) {
98+
onChange({ ...query, aggregateMaxAwaitTime: undefined });
99+
} else if (validatePositiveNumber(event.target.value)) {
100+
onChange({ ...query, aggregateMaxAwaitTime: parseInt(event.target.value, 10) });
85101
}
86102
};
87103

104+
const onAllowDiskUseChange = (event: ChangeEvent<HTMLInputElement>) => {
105+
onChange({
106+
...query, aggregateAllowDiskUse: event.target.checked
107+
});
108+
};
109+
110+
const onBatchSizeChange = (event: ChangeEvent<HTMLInputElement>) => {
111+
setBatchSizeText(event.target.value);
112+
if (!event.target.value) {
113+
onChange({ ...query, aggregateBatchSize: undefined });
114+
} else if (validatePositiveNumber(event.target.value)) {
115+
onChange({ ...query, aggregateBatchSize: parseInt(event.target.value, 10) });
116+
}
117+
};
118+
119+
const onBypassDocumentValidationChange = (event: ChangeEvent<HTMLInputElement>) => {
120+
onChange({ ...query, aggregateBypassDocumentValidation: event.target.checked });
121+
};
122+
123+
const onCommentChange = (event: ChangeEvent<HTMLInputElement>) => {
124+
onChange({ ...query, aggregateComment: event.target.value });
125+
};
126+
88127
const onCodeEditorDidMount = (e: monacoType.editor.IStandaloneCodeEditor) => {
89128
codeEditorRef.current = e;
90129
};
@@ -101,23 +140,51 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
101140

102141
return (
103142
<>
143+
<Field label="Query Type" description="Choose to query time series or table">
144+
<RadioButtonGroup id="query-editor-query-type" options={queryTypes} onChange={onQueryTypeChange} value={query.queryType || QueryType.TIMESERIES} />
145+
</Field>
104146
<InlineFieldRow>
105-
<InlineField label="Query Type">
106-
<Select id="query-editor-query-type" options={queryTypes} value={query.queryType || QueryType.TIMESERIES} onChange={onQueryTypeChange}></Select>
107-
</InlineField>
108-
<InlineField label="Collection" tooltip="Enter the collection to query"
109-
error="Please enter the collection" invalid={!query.collection}>
110-
<Input id="query-editor-collection" onChange={onCollectionChange} value={query.collection} required />
147+
<InlineField label="Collection" error="Collection is required" invalid={query.queryLanguage !== QueryLanguage.JAVASCRIPT && !query.collection} tooltip="Name of the MongoDB collection to query">
148+
<Input width={25} id="query-editor-collection" onChange={onCollectionChange} value={query.collection} disabled={query.queryLanguage === QueryLanguage.JAVASCRIPT} />
111149
</InlineField>
112-
<InlineField label="Timeout" tooltip="(Optional) The maximum amount of time (in milisecond) that the query can run on the server."
113-
error="Invalid timeout" invalid={timeoutText !== "" && !validateTimeout(timeoutText)}>
114-
<Input id="query-editor-timeout" onChange={onTimeoutChange} value={timeoutText} />
150+
<InlineField label="Query language">
151+
<Select id="query-editor-query-language" onChange={onQueryLanguageChange} options={languageOptions} value={query.queryLanguage} width={25} />
115152
</InlineField>
116153
</InlineFieldRow>
117-
<Divider />
118-
<InlineField label="Query language">
119-
<Select id="query-editor-use-js-query" onChange={onQueryLanguageChange} options={optionsLanguage} value={query.queryLanguage} />
120-
</InlineField>
154+
<ControlledCollapse label="Aggregate Options" isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)}>
155+
<InlineFieldRow>
156+
<InlineField label="Max time(ms)" tooltip="The maximum amount of time that the query can run on the server. The default value is nil, meaning that there is no time limit for query execution."
157+
error="Invalid time" invalid={maxTimeMSText !== "" && !validatePositiveNumber(maxTimeMSText)}>
158+
<Input id="query-editor-max-time-ms" onChange={onMaxTimeMSChange} value={maxTimeMSText} />
159+
</InlineField>
160+
</InlineFieldRow>
161+
<InlineFieldRow>
162+
<InlineField label="Max Await Time(ms)" tooltip="The maximum amount of time that the server should wait for new documents to satisfy a tailable cursor query.">
163+
<Input id="query-editor-max-await-time-ms" onChange={onMaxAwaitTimeMSChange} value={maxAwaitTimeMSText} />
164+
</InlineField>
165+
</InlineFieldRow>
166+
<InlineFieldRow>
167+
<InlineField label="Comment" tooltip="A string that will be included in server logs, profiling logs, and currentOp queries to help trace the operation.">
168+
<Input id="query-editor-comment" onChange={onCommentChange} value={query.aggregateComment} />
169+
</InlineField>
170+
</InlineFieldRow>
171+
<InlineFieldRow>
172+
<InlineField label="Batch Size" tooltip="The maximum number of documents to be included in each batch returned by the server."
173+
error="Invalid batch size" invalid={batchSizeText !== "" && !validatePositiveNumber(batchSizeText)}>
174+
<Input id="query-editor-batch-size" onChange={onBatchSizeChange} value={batchSizeText} />
175+
</InlineField>
176+
</InlineFieldRow>
177+
<InlineFieldRow>
178+
<InlineField label="Allow Disk Use" tooltip="If true, the operation can write to temporary files in the _tmp subdirectory of the database directory path on the server. The default value is false.">
179+
<InlineSwitch id="query-editor-allow-disk-use" onChange={onAllowDiskUseChange} value={query.aggregateAllowDiskUse} />
180+
</InlineField>
181+
</InlineFieldRow>
182+
<InlineFieldRow>
183+
<InlineField label="Bypass Document Validation" tooltip="If true, writes executed as part of the operation will opt out of document-level validation on the server. This option is valid for MongoDB versions >= 3.2 and is ignored for previous server versions. The default value is false.">
184+
<InlineSwitch id="query-editor-bypass-document-validation" onChange={onBypassDocumentValidationChange} value={query.aggregateBypassDocumentValidation} />
185+
</InlineField>
186+
</InlineFieldRow>
187+
</ControlledCollapse>
121188
<Field label="Query Text" description={`Enter the Mongo Aggregation Pipeline (${query.queryLanguage})`}
122189
error={queryTextError} invalid={queryTextError != null}>
123190
<CodeEditor onEditorDidMount={onCodeEditorDidMount} width="100%" height={300} language={query.queryLanguage === QueryLanguage.JAVASCRIPT || query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW ? "javascript" : "json"}

src/types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ export interface MongoQuery extends DataQuery {
66
collection?: string;
77
queryType?: string;
88
queryLanguage?: string;
9-
timeout?: number;
9+
10+
// Aggregate options
11+
aggregateMaxTimeMS?: number;
12+
aggregateComment?: string;
13+
aggregateBatchSize?: number;
14+
aggregateAllowDiskUse?: boolean;
15+
aggregateMaxAwaitTime?: number;
16+
aggregateBypassDocumentValidation?: boolean;
17+
1018
}
1119

1220
export const QueryType = {

0 commit comments

Comments
 (0)