Skip to content
Open
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
20 changes: 12 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [v2.1.0](https://github.com/parseablehq/parseable-datasource/tree/v2.1.0) (2026-09-12)

[Full Changelog](https://github.com/parseablehq/parseable-datasource/compare/v2.0.0...v2.1.0)

- Add Grafana Cloud Private Data Source Connect (PDC) support.

## [v2.0.0](https://github.com/parseablehq/parseable-datasource/tree/v2.0.0) (2026-06-05)

[Full Changelog](https://github.com/parseablehq/parseable-datasource/compare/v1.2.1...v2.0.0)
Expand All @@ -12,10 +18,10 @@
- release 2.0.0 [\#56](https://github.com/parseablehq/parseable-datasource/pull/56) ([nitisht](https://github.com/nitisht))
- chore: upgrade deps + fix Grafana plugin validation issues [\#55](https://github.com/parseablehq/parseable-datasource/pull/55) ([praveen5959](https://github.com/praveen5959))
- prepare for release 2.0.0 [\#54](https://github.com/parseablehq/parseable-datasource/pull/54) ([nitisht](https://github.com/nitisht))
- 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))
- 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))
- Prom ql builder [\#52](https://github.com/parseablehq/parseable-datasource/pull/52) ([praveen5959](https://github.com/praveen5959))
- feat: PromQL-first with full Code-mode feature parity [\#51](https://github.com/parseablehq/parseable-datasource/pull/51) ([praveen5959](https://github.com/praveen5959))
- Upgrade Grafana SDK to 12.x, modernize build and CI [\#50](https://github.com/parseablehq/parseable-datasource/pull/50) ([praveen5959](https://github.com/praveen5959))
- feat: PromQL-first with full Code-mode feature parity [\#51](https://github.com/parseablehq/parseable-datasource/pull/51) ([praveen5959](https://github.com/praveen5959))
- Upgrade Grafana SDK to 12.x, modernize build and CI [\#50](https://github.com/parseablehq/parseable-datasource/pull/50) ([praveen5959](https://github.com/praveen5959))
- 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))
- add Parseable Meta Dashboard [\#48](https://github.com/parseablehq/parseable-datasource/pull/48) ([nitisht](https://github.com/nitisht))
- 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))
Expand Down Expand Up @@ -58,7 +64,7 @@
- Remove commented code and update min reqd version [\#22](https://github.com/parseablehq/parseable-datasource/pull/22) ([nitisht](https://github.com/nitisht))
- Fix listing in dashboard panels [\#21](https://github.com/parseablehq/parseable-datasource/pull/21) ([nitisht](https://github.com/nitisht))
- Fix plugin version [\#20](https://github.com/parseablehq/parseable-datasource/pull/20) ([nitisht](https://github.com/nitisht))
- Use send\_null flag while sending queries [\#19](https://github.com/parseablehq/parseable-datasource/pull/19) ([nitisht](https://github.com/nitisht))
- Use send_null flag while sending queries [\#19](https://github.com/parseablehq/parseable-datasource/pull/19) ([nitisht](https://github.com/nitisht))
- Add credits [\#18](https://github.com/parseablehq/parseable-datasource/pull/18) ([nitisht](https://github.com/nitisht))
- Fix comments and sign the plugin [\#17](https://github.com/parseablehq/parseable-datasource/pull/17) ([nitisht](https://github.com/nitisht))
- Fix plugin ID as per Grafana spec [\#16](https://github.com/parseablehq/parseable-datasource/pull/16) ([nitisht](https://github.com/nitisht))
Expand All @@ -74,9 +80,7 @@
- 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))
- Add CI and documentation [\#4](https://github.com/parseablehq/parseable-datasource/pull/4) ([nitisht](https://github.com/nitisht))
- Prepare for plugin release [\#3](https://github.com/parseablehq/parseable-datasource/pull/3) ([nitisht](https://github.com/nitisht))
- 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))
- 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))
- Cleanup tooltip and expand the size of query editor [\#1](https://github.com/parseablehq/parseable-datasource/pull/1) ([nitisht](https://github.com/nitisht))



\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
\* _This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ This data source plugin allows you to query and visualize log data stored in Par

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

## Private Data Source Connect (PDC)

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/).

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.

PDC is a Grafana Cloud feature. The option is shown only when Grafana enables the `secureSocksDSProxyEnabled` feature toggle.

## Screenshots

![query editor](https://github.com/parseablehq/parseable-datasource/blob/main/src/img/query-editor.png?raw=true)
Expand All @@ -34,6 +42,7 @@ allow_loading_unsigned_plugins = parseable-parseable-datasource
```
unzip parseable-datasource-*.*.*.zip -d ./parseable-parseable-datasource
```

11. Restart the Grafana server to load the manually installed plugin.

## Dashboards
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parseable",
"version": "2.0.0",
"version": "2.1.0",
"description": "Parseable is a purpose built observability datalake. This plugin allows you to visualize data in Parseable, via Grafana.",
"scripts": {
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
Expand Down Expand Up @@ -30,6 +30,7 @@
"@types/lodash": "^4.17.24",
"@types/node": "^20.19.0",
"@types/react-router-dom": "^5.2.0",
"@types/semver": "^7.7.1",
"@types/testing-library__jest-dom": "5.14.8",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
Expand Down Expand Up @@ -79,6 +80,7 @@
"@prometheus-io/lezer-promql": "^0.311.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"semver": "^7.7.3",
"tslib": "2.8.1"
}
}
67 changes: 66 additions & 1 deletion pkg/plugin/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
var (
_ backend.QueryDataHandler = (*Datasource)(nil)
_ backend.CheckHealthHandler = (*Datasource)(nil)
_ backend.CallResourceHandler = (*Datasource)(nil)
_ instancemgmt.InstanceDisposer = (*Datasource)(nil)
)

Expand Down Expand Up @@ -69,6 +70,65 @@ func (d *Datasource) Dispose() {
d.httpClient.CloseIdleConnections()
}

// CallResource proxies UI support requests through the same SDK HTTP client
// used by queries and health checks. The SDK applies PDC's secure SOCKS
// transport when enableSecureSocksProxy is set on the data source instance.
func (d *Datasource) CallResource(
ctx context.Context,
req *backend.CallResourceRequest,
sender backend.CallResourceResponseSender,
) error {
if strings.Trim(req.Path, "/") != "proxy" {
return sender.Send(&backend.CallResourceResponse{Status: http.StatusNotFound})
}

resourceURL, err := url2.Parse(req.URL)
if err != nil {
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadRequest})
}
target := resourceURL.Query().Get("target")
targetURL, err := url2.Parse(target)
if err != nil || targetURL.IsAbs() || targetURL.Host != "" || !strings.HasPrefix(targetURL.Path, "/") {
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadRequest})
}

baseURL, err := url2.Parse(d.settings.URL)
if err != nil {
return sender.Send(&backend.CallResourceResponse{Status: http.StatusInternalServerError})
}
baseURL.Path = strings.TrimRight(baseURL.Path, "/") + targetURL.Path
baseURL.RawQuery = targetURL.RawQuery

proxyReq, err := http.NewRequestWithContext(ctx, req.Method, baseURL.String(), bytes.NewReader(req.Body))
if err != nil {
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadRequest})
}
for _, header := range []string{"Accept", "Content-Type"} {
if value := req.GetHTTPHeader(header); value != "" {
proxyReq.Header.Set(header, value)
}
}

resp, err := d.httpClient.Do(proxyReq)
if err != nil {
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusBadGateway,
Body: []byte(err.Error()),
})
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return sender.Send(&backend.CallResourceResponse{Status: http.StatusBadGateway})
}
return sender.Send(&backend.CallResourceResponse{
Status: resp.StatusCode,
Headers: resp.Header,
Body: body,
})
}

func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
response := backend.NewQueryDataResponse()
for _, q := range req.Queries {
Expand Down Expand Up @@ -248,7 +308,12 @@ func getGrafanaFieldType(value interface{}) data.FieldType {
func (d *Datasource) CheckHealth(ctx context.Context, _ *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
ctxLogger := log.DefaultLogger.FromContext(ctx)

r, err := http.NewRequestWithContext(ctx, http.MethodGet, d.settings.URL, nil)
healthURL, err := url2.JoinPath(d.settings.URL, "/api/v1/about")
if err != nil {
return newHealthCheckErrorf("could not construct health check URL"), nil
}

r, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
if err != nil {
return newHealthCheckErrorf("could not create request"), nil
}
Expand Down
85 changes: 85 additions & 0 deletions pkg/plugin/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package plugin

import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/backend"
)

type resourceResponseSender struct {
response *backend.CallResourceResponse
}

func (s *resourceResponseSender) Send(response *backend.CallResourceResponse) error {
s.response = response
return nil
}

func TestCallResourceProxiesRelativeParseableRequest(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/base/api/v1/about" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("stream") != "logs" {
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
}
if r.Method != http.MethodPost {
t.Fatalf("unexpected method: %s", r.Method)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != `{"check":true}` {
t.Fatalf("unexpected body: %s", body)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer server.Close()

datasource := &Datasource{
settings: backend.DataSourceInstanceSettings{URL: server.URL + "/base"},
httpClient: server.Client(),
}
sender := &resourceResponseSender{}
err := datasource.CallResource(context.Background(), &backend.CallResourceRequest{
Path: "proxy",
Method: http.MethodPost,
URL: "/proxy?target=%2Fapi%2Fv1%2Fabout%3Fstream%3Dlogs",
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: []byte(`{"check":true}`),
}, sender)
if err != nil {
t.Fatal(err)
}
if sender.response.Status != http.StatusCreated {
t.Fatalf("unexpected status: %d", sender.response.Status)
}
if string(sender.response.Body) != `{"ok":true}` {
t.Fatalf("unexpected response: %s", sender.response.Body)
}
}

func TestCallResourceRejectsAbsoluteTarget(t *testing.T) {
datasource := &Datasource{httpClient: http.DefaultClient}
sender := &resourceResponseSender{}
err := datasource.CallResource(context.Background(), &backend.CallResourceRequest{
Path: "proxy",
Method: http.MethodGet,
URL: "/proxy?target=https%3A%2F%2Fexample.com%2Fsecret",
}, sender)
if err != nil {
t.Fatal(err)
}
if sender.response.Status != http.StatusBadRequest {
t.Fatalf("unexpected status: %d", sender.response.Status)
}
}
38 changes: 37 additions & 1 deletion src/components/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,54 @@
import React from 'react';
import { DataSourceHttpSettings } from '@grafana/ui';
import { css } from '@emotion/css';
import { DataSourceHttpSettings, InlineField, Switch, useStyles2 } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { config } from '@grafana/runtime';
import { MyDataSourceOptions, MySecureJsonData } from '../types';
import { supportsSecureSocksProxy } from '../utils/pdc';

interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions, MySecureJsonData> {}

export const ConfigEditor = ({ onOptionsChange, options }: Props) => {
const styles = useStyles2(getStyles);
const pdcSupported = supportsSecureSocksProxy(config.featureToggles, config.buildInfo.version);

return (
<div className="gf-form-group">
<DataSourceHttpSettings
defaultUrl={'https://demo.parseable.com'}
dataSourceConfig={options}
onChange={onOptionsChange}
/>
{pdcSupported && (
<InlineField
label="Secure SOCKS Proxy"
labelWidth={26}
tooltip="Route requests to Parseable through Grafana Cloud Private Data Source Connect (PDC)."
>
<div className={styles.toggleContainer}>
<Switch
value={Boolean(options.jsonData.enableSecureSocksProxy)}
onChange={(event) =>
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
enableSecureSocksProxy: event.currentTarget.checked,
},
})
}
/>
</div>
</InlineField>
)}
</div>
);
};

const getStyles = () => ({
toggleContainer: css({
display: 'flex',
alignItems: 'center',
minHeight: 32,
}),
});
Loading
Loading