Skip to content

Commit ccc4cea

Browse files
authored
feat: add PDC proxy support (#60)
1 parent 70f22cc commit ccc4cea

11 files changed

Lines changed: 323 additions & 52 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [v2.1.0](https://github.com/parseablehq/parseable-datasource/tree/v2.1.0) (2026-09-12)
4+
5+
[Full Changelog](https://github.com/parseablehq/parseable-datasource/compare/v2.0.0...v2.1.0)
6+
7+
- Add Grafana Cloud Private Data Source Connect (PDC) support.
8+
39
## [v2.0.0](https://github.com/parseablehq/parseable-datasource/tree/v2.0.0) (2026-06-05)
410

511
[Full Changelog](https://github.com/parseablehq/parseable-datasource/compare/v1.2.1...v2.0.0)
@@ -12,10 +18,10 @@
1218
- release 2.0.0 [\#56](https://github.com/parseablehq/parseable-datasource/pull/56) ([nitisht](https://github.com/nitisht))
1319
- chore: upgrade deps + fix Grafana plugin validation issues [\#55](https://github.com/parseablehq/parseable-datasource/pull/55) ([praveen5959](https://github.com/praveen5959))
1420
- prepare for release 2.0.0 [\#54](https://github.com/parseablehq/parseable-datasource/pull/54) ([nitisht](https://github.com/nitisht))
15-
- feat: paginated value suggestions via dataset\_stats with top-5 + show more [\#53](https://github.com/parseablehq/parseable-datasource/pull/53) ([praveen5959](https://github.com/praveen5959))
21+
- feat: paginated value suggestions via dataset_stats with top-5 + show more [\#53](https://github.com/parseablehq/parseable-datasource/pull/53) ([praveen5959](https://github.com/praveen5959))
1622
- Prom ql builder [\#52](https://github.com/parseablehq/parseable-datasource/pull/52) ([praveen5959](https://github.com/praveen5959))
17-
- feat: PromQL-first with full Code-mode feature parity [\#51](https://github.com/parseablehq/parseable-datasource/pull/51) ([praveen5959](https://github.com/praveen5959))
18-
- Upgrade Grafana SDK to 12.x, modernize build and CI [\#50](https://github.com/parseablehq/parseable-datasource/pull/50) ([praveen5959](https://github.com/praveen5959))
23+
- feat: PromQL-first with full Code-mode feature parity [\#51](https://github.com/parseablehq/parseable-datasource/pull/51) ([praveen5959](https://github.com/praveen5959))
24+
- Upgrade Grafana SDK to 12.x, modernize build and CI [\#50](https://github.com/parseablehq/parseable-datasource/pull/50) ([praveen5959](https://github.com/praveen5959))
1925
- Update branding: replace logo and update terminology from logs to telemetry data [\#49](https://github.com/parseablehq/parseable-datasource/pull/49) ([Debanitrkl](https://github.com/Debanitrkl))
2026
- add Parseable Meta Dashboard [\#48](https://github.com/parseablehq/parseable-datasource/pull/48) ([nitisht](https://github.com/nitisht))
2127
- fixed plugin validation error: renamed root dir - from 'dist' to plugin-id [\#47](https://github.com/parseablehq/parseable-datasource/pull/47) ([balaji-jr](https://github.com/balaji-jr))
@@ -58,7 +64,7 @@
5864
- Remove commented code and update min reqd version [\#22](https://github.com/parseablehq/parseable-datasource/pull/22) ([nitisht](https://github.com/nitisht))
5965
- Fix listing in dashboard panels [\#21](https://github.com/parseablehq/parseable-datasource/pull/21) ([nitisht](https://github.com/nitisht))
6066
- Fix plugin version [\#20](https://github.com/parseablehq/parseable-datasource/pull/20) ([nitisht](https://github.com/nitisht))
61-
- Use send\_null flag while sending queries [\#19](https://github.com/parseablehq/parseable-datasource/pull/19) ([nitisht](https://github.com/nitisht))
67+
- Use send_null flag while sending queries [\#19](https://github.com/parseablehq/parseable-datasource/pull/19) ([nitisht](https://github.com/nitisht))
6268
- Add credits [\#18](https://github.com/parseablehq/parseable-datasource/pull/18) ([nitisht](https://github.com/nitisht))
6369
- Fix comments and sign the plugin [\#17](https://github.com/parseablehq/parseable-datasource/pull/17) ([nitisht](https://github.com/nitisht))
6470
- Fix plugin ID as per Grafana spec [\#16](https://github.com/parseablehq/parseable-datasource/pull/16) ([nitisht](https://github.com/nitisht))
@@ -74,9 +80,7 @@
7480
- Fix links in readme and add more info in plugin.json [\#5](https://github.com/parseablehq/parseable-datasource/pull/5) ([nitisht](https://github.com/nitisht))
7581
- Add CI and documentation [\#4](https://github.com/parseablehq/parseable-datasource/pull/4) ([nitisht](https://github.com/nitisht))
7682
- Prepare for plugin release [\#3](https://github.com/parseablehq/parseable-datasource/pull/3) ([nitisht](https://github.com/nitisht))
77-
- Use p\_timestamp as the field to be used as time [\#2](https://github.com/parseablehq/parseable-datasource/pull/2) ([nitisht](https://github.com/nitisht))
83+
- Use p_timestamp as the field to be used as time [\#2](https://github.com/parseablehq/parseable-datasource/pull/2) ([nitisht](https://github.com/nitisht))
7884
- Cleanup tooltip and expand the size of query editor [\#1](https://github.com/parseablehq/parseable-datasource/pull/1) ([nitisht](https://github.com/nitisht))
7985

80-
81-
82-
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
86+
\* _This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ This data source plugin allows you to query and visualize log data stored in Par
1111

1212
Refer the Parseable Grafana documentation page: [https://www.parseable.com/docs/integrations/visualization/grafana](https://www.parseable.com/docs/integrations/visualization/grafana).
1313

14+
## Private Data Source Connect (PDC)
15+
16+
The plugin supports querying a Parseable server in a private network from Grafana Cloud using [Private Data Source Connect](https://grafana.com/docs/grafana-cloud/observe-and-act/connect-externally-hosted/private-data-source-connect/).
17+
18+
After configuring a PDC network and agent in Grafana Cloud, edit the Parseable data source, use the private Parseable URL, enable **Secure SOCKS Proxy**, and select the appropriate PDC network. Save and test the data source to verify connectivity.
19+
20+
PDC is a Grafana Cloud feature. The option is shown only when Grafana enables the `secureSocksDSProxyEnabled` feature toggle.
21+
1422
## Screenshots
1523

1624
![query editor](https://github.com/parseablehq/parseable-datasource/blob/main/src/img/query-editor.png?raw=true)
@@ -34,6 +42,7 @@ allow_loading_unsigned_plugins = parseable-parseable-datasource
3442
```
3543
unzip parseable-datasource-*.*.*.zip -d ./parseable-parseable-datasource
3644
```
45+
3746
11. Restart the Grafana server to load the manually installed plugin.
3847

3948
## Dashboards

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parseable",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "Parseable is a purpose built observability datalake. This plugin allows you to visualize data in Parseable, via Grafana.",
55
"scripts": {
66
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
@@ -30,6 +30,7 @@
3030
"@types/lodash": "^4.17.24",
3131
"@types/node": "^20.19.0",
3232
"@types/react-router-dom": "^5.2.0",
33+
"@types/semver": "^7.7.1",
3334
"@types/testing-library__jest-dom": "5.14.8",
3435
"@typescript-eslint/eslint-plugin": "^7.18.0",
3536
"@typescript-eslint/parser": "^7.18.0",
@@ -79,6 +80,7 @@
7980
"@prometheus-io/lezer-promql": "^0.311.2",
8081
"react": "18.2.0",
8182
"react-dom": "18.2.0",
83+
"semver": "^7.7.3",
8284
"tslib": "2.8.1"
8385
}
8486
}

‎pkg/plugin/datasource.go‎

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
var (
3030
_ backend.QueryDataHandler = (*Datasource)(nil)
3131
_ backend.CheckHealthHandler = (*Datasource)(nil)
32+
_ backend.CallResourceHandler = (*Datasource)(nil)
3233
_ instancemgmt.InstanceDisposer = (*Datasource)(nil)
3334
)
3435

@@ -69,6 +70,65 @@ func (d *Datasource) Dispose() {
6970
d.httpClient.CloseIdleConnections()
7071
}
7172

73+
// CallResource proxies UI support requests through the same SDK HTTP client
74+
// used by queries and health checks. The SDK applies PDC's secure SOCKS
75+
// transport when enableSecureSocksProxy is set on the data source instance.
76+
func (d *Datasource) CallResource(
77+
ctx context.Context,
78+
req *backend.CallResourceRequest,
79+
sender backend.CallResourceResponseSender,
80+
) error {
81+
if strings.Trim(req.Path, "/") != "proxy" {
82+
return sender.Send(&backend.CallResourceResponse{Status: http.StatusNotFound})
83+
}
84+
85+
resourceURL, err := url2.Parse(req.URL)
86+
if err != nil {
87+
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadRequest})
88+
}
89+
target := resourceURL.Query().Get("target")
90+
targetURL, err := url2.Parse(target)
91+
if err != nil || targetURL.IsAbs() || targetURL.Host != "" || !strings.HasPrefix(targetURL.Path, "/") {
92+
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadRequest})
93+
}
94+
95+
baseURL, err := url2.Parse(d.settings.URL)
96+
if err != nil {
97+
return sender.Send(&backend.CallResourceResponse{Status: http.StatusInternalServerError})
98+
}
99+
baseURL.Path = strings.TrimRight(baseURL.Path, "/") + targetURL.Path
100+
baseURL.RawQuery = targetURL.RawQuery
101+
102+
proxyReq, err := http.NewRequestWithContext(ctx, req.Method, baseURL.String(), bytes.NewReader(req.Body))
103+
if err != nil {
104+
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadRequest})
105+
}
106+
for _, header := range []string{"Accept", "Content-Type"} {
107+
if value := req.GetHTTPHeader(header); value != "" {
108+
proxyReq.Header.Set(header, value)
109+
}
110+
}
111+
112+
resp, err := d.httpClient.Do(proxyReq)
113+
if err != nil {
114+
return sender.Send(&backend.CallResourceResponse{
115+
Status: http.StatusBadGateway,
116+
Body: []byte(err.Error()),
117+
})
118+
}
119+
defer resp.Body.Close()
120+
121+
body, err := io.ReadAll(resp.Body)
122+
if err != nil {
123+
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadGateway})
124+
}
125+
return sender.Send(&backend.CallResourceResponse{
126+
Status: resp.StatusCode,
127+
Headers: resp.Header,
128+
Body: body,
129+
})
130+
}
131+
72132
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
73133
response := backend.NewQueryDataResponse()
74134
for _, q := range req.Queries {
@@ -248,7 +308,12 @@ func getGrafanaFieldType(value interface{}) data.FieldType {
248308
func (d *Datasource) CheckHealth(ctx context.Context, _ *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
249309
ctxLogger := log.DefaultLogger.FromContext(ctx)
250310

251-
r, err := http.NewRequestWithContext(ctx, http.MethodGet, d.settings.URL, nil)
311+
healthURL, err := url2.JoinPath(d.settings.URL, "/api/v1/about")
312+
if err != nil {
313+
return newHealthCheckErrorf("could not construct health check URL"), nil
314+
}
315+
316+
r, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
252317
if err != nil {
253318
return newHealthCheckErrorf("could not create request"), nil
254319
}

‎pkg/plugin/resource_test.go‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/grafana/grafana-plugin-sdk-go/backend"
11+
)
12+
13+
type resourceResponseSender struct {
14+
response *backend.CallResourceResponse
15+
}
16+
17+
func (s *resourceResponseSender) Send(response *backend.CallResourceResponse) error {
18+
s.response = response
19+
return nil
20+
}
21+
22+
func TestCallResourceProxiesRelativeParseableRequest(t *testing.T) {
23+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
if r.URL.Path != "/base/api/v1/about" {
25+
t.Fatalf("unexpected path: %s", r.URL.Path)
26+
}
27+
if r.URL.Query().Get("stream") != "logs" {
28+
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
29+
}
30+
if r.Method != http.MethodPost {
31+
t.Fatalf("unexpected method: %s", r.Method)
32+
}
33+
body, err := io.ReadAll(r.Body)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
if string(body) != `{"check":true}` {
38+
t.Fatalf("unexpected body: %s", body)
39+
}
40+
w.Header().Set("Content-Type", "application/json")
41+
w.WriteHeader(http.StatusCreated)
42+
_, _ = w.Write([]byte(`{"ok":true}`))
43+
}))
44+
defer server.Close()
45+
46+
datasource := &Datasource{
47+
settings: backend.DataSourceInstanceSettings{URL: server.URL + "/base"},
48+
httpClient: server.Client(),
49+
}
50+
sender := &resourceResponseSender{}
51+
err := datasource.CallResource(context.Background(), &backend.CallResourceRequest{
52+
Path: "proxy",
53+
Method: http.MethodPost,
54+
URL: "/proxy?target=%2Fapi%2Fv1%2Fabout%3Fstream%3Dlogs",
55+
Headers: map[string][]string{
56+
"Content-Type": {"application/json"},
57+
},
58+
Body: []byte(`{"check":true}`),
59+
}, sender)
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
if sender.response.Status != http.StatusCreated {
64+
t.Fatalf("unexpected status: %d", sender.response.Status)
65+
}
66+
if string(sender.response.Body) != `{"ok":true}` {
67+
t.Fatalf("unexpected response: %s", sender.response.Body)
68+
}
69+
}
70+
71+
func TestCallResourceRejectsAbsoluteTarget(t *testing.T) {
72+
datasource := &Datasource{httpClient: http.DefaultClient}
73+
sender := &resourceResponseSender{}
74+
err := datasource.CallResource(context.Background(), &backend.CallResourceRequest{
75+
Path: "proxy",
76+
Method: http.MethodGet,
77+
URL: "/proxy?target=https%3A%2F%2Fexample.com%2Fsecret",
78+
}, sender)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
if sender.response.Status != http.StatusBadRequest {
83+
t.Fatalf("unexpected status: %d", sender.response.Status)
84+
}
85+
}

‎src/components/ConfigEditor.tsx‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,54 @@
11
import React from 'react';
2-
import { DataSourceHttpSettings } from '@grafana/ui';
2+
import { css } from '@emotion/css';
3+
import { DataSourceHttpSettings, InlineField, Switch, useStyles2 } from '@grafana/ui';
34
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
5+
import { config } from '@grafana/runtime';
46
import { MyDataSourceOptions, MySecureJsonData } from '../types';
7+
import { supportsSecureSocksProxy } from '../utils/pdc';
58

69
interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions, MySecureJsonData> {}
710

811
export const ConfigEditor = ({ onOptionsChange, options }: Props) => {
12+
const styles = useStyles2(getStyles);
13+
const pdcSupported = supportsSecureSocksProxy(config.featureToggles, config.buildInfo.version);
14+
915
return (
1016
<div className="gf-form-group">
1117
<DataSourceHttpSettings
1218
defaultUrl={'https://demo.parseable.com'}
1319
dataSourceConfig={options}
1420
onChange={onOptionsChange}
1521
/>
22+
{pdcSupported && (
23+
<InlineField
24+
label="Secure SOCKS Proxy"
25+
labelWidth={26}
26+
tooltip="Route requests to Parseable through Grafana Cloud Private Data Source Connect (PDC)."
27+
>
28+
<div className={styles.toggleContainer}>
29+
<Switch
30+
value={Boolean(options.jsonData.enableSecureSocksProxy)}
31+
onChange={(event) =>
32+
onOptionsChange({
33+
...options,
34+
jsonData: {
35+
...options.jsonData,
36+
enableSecureSocksProxy: event.currentTarget.checked,
37+
},
38+
})
39+
}
40+
/>
41+
</div>
42+
</InlineField>
43+
)}
1644
</div>
1745
);
1846
};
47+
48+
const getStyles = () => ({
49+
toggleContainer: css({
50+
display: 'flex',
51+
alignItems: 'center',
52+
minHeight: 32,
53+
}),
54+
});

0 commit comments

Comments
 (0)