Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion connectors/grafana-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Click the `New Dashboard` icon on the top right, and select `Add an empty panel`

<img style="width:100%; max-width:800px; max-height:600px; margin-left:auto; margin-right:auto; display:block;" src="https://github.com/apache/iotdb-bin-resources/blob/main/docs/UserGuide/Ecosystem%20Integration/Grafana-plugin/add%20empty%20panel.png?raw=true">

Grafana plugin supports SQL: Full Customized mode and SQL: Drop-down List mode, and the default mode is SQL: Full Customized mode.
Grafana plugin supports SQL: Full Customized mode and SQL: Drop-down List mode for the tree model, and SQL: Table Model mode for the table model. The default mode is SQL: Full Customized mode.

<img style="width:100%; max-width:800px; max-height:600px; margin-left:auto; margin-right:auto; display:block;" src="https://github.com/apache/iotdb-bin-resources/blob/main/docs/UserGuide/Ecosystem%20Integration/Grafana-plugin/grafana_input_style.png?raw=true">

Expand Down Expand Up @@ -119,6 +119,37 @@ Select a time series in the TIME-SERIES selection box, select a function in the

<img style="width:100%; max-width:800px; max-height:600px; margin-left:auto; margin-right:auto; display:block;" src="https://github.com/apache/iotdb-bin-resources/blob/main/docs/UserGuide/Ecosystem%20Integration/Grafana-plugin/grafana_input2.png?raw=true">

##### SQL: Table Model

Queries IoTDB's table model (IoTDB 2.x) through the native Go client ([apache/iotdb-client-go](https://github.com/apache/iotdb-client-go)). Enter a standard SQL statement in the SQL input box, for example:

```sql
SELECT time, device_id, temperature FROM table1 WHERE $__timeFilter(time)
```

Unlike the tree-model modes, which go through the REST service, this mode connects to the IoTDB RPC port (6667 by default). The data source's `rpc address` option sets that endpoint explicitly; when it is left empty, the URL's host with port 6667 is used.

DATABASE input box: the database to run the statement against. Required — every query first runs `USE <database>` on its pooled session, so results stay deterministic regardless of which session the pool hands out. Fully-qualified `database.table` references are still allowed and take precedence over the session database.

SQL input box: a single table-model SELECT statement. The following macros are expanded by the plugin before the statement is sent:

| Macro | Expands to |
| ----- | ---------- |
| `$__timeFilter(col)` | `(col >= <panel start> AND col <= <panel end>)`; `col` defaults to `time` when omitted |
| `$__timeFrom`, `$__timeFrom()` | the panel range start as an ISO 8601 UTC timestamp literal |
| `$__timeTo`, `$__timeTo()` | the panel range end as an ISO 8601 UTC timestamp literal |

The macros expand to ISO 8601 timestamp literals (e.g. `2020-09-13T12:26:40.000+00:00`), which the server interprets in its own configured `timestamp_precision` — so time filtering works unchanged on `ms`, `us` and `ns` servers, and TIMESTAMP values are likewise converted by the client using the server-reported precision.

FORMAT option:

* `Time series` (default): rows are sorted by the first TIMESTAMP column, and a result that contains tag/string columns next to numeric columns is pivoted into one series per tag combination — so a query returning several devices renders as separate lines in a time-series panel. Note that the pivot requires timestamps without nulls; results are sorted by the plugin, so an `ORDER BY` clause is not needed.
* `Table`: rows are returned exactly as the server sent them (use this with the table panel, or when your `ORDER BY` should be preserved).

Result columns are typed from the result set's data types: TIMESTAMP columns become Grafana time fields, INT32/INT64 become integers, FLOAT/DOUBLE become floats, BOOLEAN becomes booleans, and TEXT/STRING/DATE/BLOB render as strings.

There is no server-imposed row cap on this path; add a `LIMIT` clause when querying wide time ranges over dense data to bound the result size.

#### Support for variables and template functions

Both SQL: Full Customized and SQL: Drop-down List input methods support the variable and template functions of grafana. In the following example, raw input method is used, and aggregation is similar.
Expand Down
6 changes: 5 additions & 1 deletion connectors/grafana-plugin/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ go 1.21

toolchain go1.21.0

require github.com/grafana/grafana-plugin-sdk-go v0.250.0
require (
github.com/apache/iotdb-client-go/v2 v2.0.8
github.com/grafana/grafana-plugin-sdk-go v0.250.0
)

require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/apache/arrow/go/v15 v15.0.2 // indirect
github.com/apache/thrift v0.17.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down
40 changes: 39 additions & 1 deletion connectors/grafana-plugin/pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"time"

"github.com/apache/iotdb-client-go/v2/client"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
Expand Down Expand Up @@ -70,7 +72,8 @@ func ApacheIoTDBDatasource(ctx context.Context, d backend.DataSourceInstanceSett
if password, exists := d.DecryptedSecureJSONData["password"]; exists {
authorization = "Basic " + base64.StdEncoding.EncodeToString([]byte(dm.Username+":"+password))
}
return &IoTDBDataSource{CallResourceHandler: iotdbResourceHandler(authorization, httpClient), Username: dm.Username, Ulr: dm.Url, httpClient: httpClient}, nil
password := d.DecryptedSecureJSONData["password"]
return &IoTDBDataSource{CallResourceHandler: iotdbResourceHandler(authorization, httpClient), Username: dm.Username, Ulr: dm.Url, RPCAddress: dm.RPCAddress, password: password, httpClient: httpClient}, nil
}

// SampleDatasource is an example datasource which can respond to data queries, reports
Expand All @@ -79,7 +82,14 @@ type IoTDBDataSource struct {
backend.CallResourceHandler
Username string
Ulr string
RPCAddress string
password string
httpClient *http.Client

// Native-client session pool for the table-model mode, created lazily by
// getTablePool on the first table query.
tablePoolMu sync.Mutex
tablePool *client.TableSessionPool
}

// Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance
Expand All @@ -88,6 +98,12 @@ type IoTDBDataSource struct {
func (d *IoTDBDataSource) Dispose() {
// Clean up datasource instance resources.
d.httpClient.CloseIdleConnections()
d.tablePoolMu.Lock()
defer d.tablePoolMu.Unlock()
if d.tablePool != nil {
d.tablePool.Close()
d.tablePool = nil
}
}

// QueryData handles multiple queries and returns multiple responses.
Expand All @@ -113,6 +129,10 @@ func (d *IoTDBDataSource) QueryData(ctx context.Context, req *backend.QueryDataR
type dataSourceModel struct {
Username string `json:"username"`
Url string `json:"url"`
// RPCAddress is the IoTDB RPC endpoint (host or host:port) the native
// client used by the table-model mode connects to. When empty, the URL's
// host with the default RPC port 6667 is used.
RPCAddress string `json:"rpcAddress"`
}

type groupBy struct {
Expand All @@ -134,6 +154,9 @@ type queryParam struct {
FillClauses string `json:"fillClauses"`
GroupBy groupBy `json:"groupBy"`
Hide bool `json:"hide"`
Database string `json:"database"`
Sql string `json:"sql"`
Format string `json:"format"`
}

type QueryDataReq struct {
Expand Down Expand Up @@ -200,6 +223,17 @@ func verifyQuery(query backend.DataQuery) (qp *queryParam, errMsg string) {
return nil, "Input error, FROM is required"
}
}
} else if qp.SqlType == TableModelSqlType {
if strings.TrimSpace(qp.Sql) == "" {
return nil, "Input error, SQL is required"
}
// The database is required: every query then USEs it on its pooled
// session, so a session's leftover USE state from a previous query
// can never influence the result (PooledTableSession.Close only
// resets the database when the pool has one configured).
if strings.TrimSpace(qp.Database) == "" {
return nil, "Input error, DATABASE is required"
}
} else {
return nil, "none"
}
Expand Down Expand Up @@ -231,6 +265,10 @@ func (d *IoTDBDataSource) query(cxt context.Context, pCtx backend.PluginContext,
qp.StartTime = query.TimeRange.From.UnixNano() / 1000000
qp.EndTime = query.TimeRange.To.UnixNano() / 1000000

if qp.SqlType == TableModelSqlType {
return d.queryTableModel(cxt, qp)
}

if qp.SqlType == "SQL: Drop-down List" {
qp.Control = ""
var expressions []string = qp.Paths[len(qp.Paths)-1:]
Expand Down
Loading
Loading