diff --git a/go.mod b/go.mod index 51368744..ded10542 100644 --- a/go.mod +++ b/go.mod @@ -106,4 +106,4 @@ require ( google.golang.org/protobuf v1.35.2 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index 0796b9df..992f3a5b 100644 --- a/go.sum +++ b/go.sum @@ -329,4 +329,4 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/pkg/azuredx/client/client.go b/pkg/azuredx/client/client.go index a7a455f9..4c2a87f8 100644 --- a/pkg/azuredx/client/client.go +++ b/pkg/azuredx/client/client.go @@ -26,7 +26,7 @@ import ( type AdxClient interface { TestKustoRequest(ctx context.Context, datasourceSettings *models.DatasourceSettings, properties *models.Properties, additionalHeaders map[string]string) error TestARGsRequest(ctx context.Context, datasourceSettings *models.DatasourceSettings, properties *models.Properties, additionalHeaders map[string]string) error - KustoRequest(ctx context.Context, cluster string, url string, payload models.RequestPayload, userTrackingEnabled bool) (*models.TableResponse, error) + KustoRequest(ctx context.Context, cluster string, url string, payload models.RequestPayload, userTrackingEnabled bool, application string) (*models.TableResponse, error) ARGClusterRequest(ctx context.Context, payload models.ARGRequestPayload, additionalHeaders map[string]string) ([]models.ClusterOption, error) } @@ -196,7 +196,7 @@ func (c *Client) testManagementClient(ctx context.Context, _ *models.DatasourceS // KustoRequest executes a Kusto Query language request to Azure's Data Explorer V1 REST API // and returns a TableResponse. If there is a query syntax error, the error message inside // the API's JSON error response is returned as well (if available). -func (c *Client) KustoRequest(ctx context.Context, clusterUrl string, path string, payload models.RequestPayload, userTrackingEnabled bool) (*models.TableResponse, error) { +func (c *Client) KustoRequest(ctx context.Context, clusterUrl string, path string, payload models.RequestPayload, userTrackingEnabled bool, application string) (*models.TableResponse, error) { buf, err := json.Marshal(payload) if err != nil { return nil, errorsource.DownstreamError(fmt.Errorf("no Azure request serial: %w", err), false) @@ -214,7 +214,11 @@ func (c *Client) KustoRequest(ctx context.Context, clusterUrl string, path strin req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") - req.Header.Set("x-ms-app", "Grafana-ADX") + if application == "" { + application = "Grafana-ADX" + } + req.Header.Set("x-ms-app", application) + // req.Header.Set("x-ms-app", "Grafana-ADX") if payload.QuerySource == "" { payload.QuerySource = "unspecified" } diff --git a/pkg/azuredx/client/client_test.go b/pkg/azuredx/client/client_test.go index 56fa1d45..dbca9882 100644 --- a/pkg/azuredx/client/client_test.go +++ b/pkg/azuredx/client/client_test.go @@ -37,7 +37,7 @@ func TestClient(t *testing.T) { } client := &Client{httpClientKusto: server.Client()} - table, err := client.KustoRequest(context.Background(), server.URL, "", payload, false) + table, err := client.KustoRequest(context.Background(), server.URL, "", payload, false, "Grafana-ADX") require.NoError(t, err) require.NotNil(t, table) }) @@ -64,7 +64,7 @@ func TestClient(t *testing.T) { } client := &Client{httpClientKusto: server.Client()} - table, err := client.KustoRequest(context.Background(), server.URL, "", payload, false) + table, err := client.KustoRequest(context.Background(), server.URL, "", payload, false, "Grafana-ADX") require.Nil(t, table) require.NotNil(t, err) require.Contains(t, err.Error(), "Request is invalid and cannot be processed: Syntax error: SYN0002: A recognition error occurred. [line:position=1:9]. Query: 'PerfTest take 5'") @@ -74,7 +74,7 @@ func TestClient(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { require.Equal(t, "application/json", req.Header.Get("Accept")) require.Equal(t, "application/json", req.Header.Get("Content-Type")) - require.Equal(t, "Grafana-ADX", req.Header.Get("x-ms-app")) + require.NotEmpty(t, req.Header.Get("x-ms-app"), "Header 'x-ms-app' should not be empty") })) defer server.Close() @@ -85,7 +85,7 @@ func TestClient(t *testing.T) { } client := &Client{httpClientKusto: server.Client()} - table, err := client.KustoRequest(context.Background(), server.URL, "", payload, false) + table, err := client.KustoRequest(context.Background(), server.URL, "", payload, false, "Grafana-ADX") require.Nil(t, table) require.NotNil(t, err) }) @@ -113,7 +113,7 @@ func TestClient(t *testing.T) { Login: "test-user", }, }) - table, err := client.KustoRequest(ctxWithUser, server.URL, "", payload, true) + table, err := client.KustoRequest(ctxWithUser, server.URL, "", payload, true, "Grafana-ADX") require.Nil(t, table) require.NotNil(t, err) }) diff --git a/pkg/azuredx/datasource.go b/pkg/azuredx/datasource.go index f7f2faa4..a2ae319c 100644 --- a/pkg/azuredx/datasource.go +++ b/pkg/azuredx/datasource.go @@ -178,13 +178,13 @@ func (adx *AzureDataExplorer) modelQuery(ctx context.Context, q models.QueryMode // errorsource set in SanitizeClusterUri return backend.DataResponse{}, err } - + application := adx.settings.Application tableRes, err := adx.client.KustoRequest(ctx, sanitized, "/v1/rest/query", models.RequestPayload{ CSL: q.Query, DB: database, Properties: props, QuerySource: q.QuerySource, - }, adx.settings.EnableUserTracking) + }, adx.settings.EnableUserTracking, application) if err != nil { backend.Logger.Debug("error building kusto request", "error", err.Error()) // errorsource set in KustoRequest diff --git a/pkg/azuredx/datasource_test.go b/pkg/azuredx/datasource_test.go index ce9f7c21..90e1928e 100644 --- a/pkg/azuredx/datasource_test.go +++ b/pkg/azuredx/datasource_test.go @@ -10,7 +10,7 @@ import ( ) var ( - kustoRequestMock func(url string, cluster string, payload models.RequestPayload, enableUserTracking bool) (*models.TableResponse, error) + kustoRequestMock func(url string, cluster string, payload models.RequestPayload, enableUserTracking bool, application string) (*models.TableResponse, error) ARGClusterRequestMock func(payload models.ARGRequestPayload, additionalHeaders map[string]string) ([]models.ClusterOption, error) table = &models.TableResponse{ Tables: []models.Table{ @@ -27,11 +27,12 @@ func TestDatasource(t *testing.T) { var adx AzureDataExplorer const UserLogin string = "user-login" const ClusterURL string = "base-url" + const Application string = "Grafana-ADX" t.Run("When running a query the right args should be passed to KustoRequest", func(t *testing.T) { adx = AzureDataExplorer{} adx.client = &fakeClient{} - adx.settings = &models.DatasourceSettings{EnableUserTracking: true, ClusterURL: ClusterURL} + adx.settings = &models.DatasourceSettings{EnableUserTracking: true, ClusterURL: ClusterURL, Application: Application} query := backend.DataQuery{ RefID: "", QueryType: "", @@ -40,7 +41,7 @@ func TestDatasource(t *testing.T) { TimeRange: backend.TimeRange{}, JSON: []byte(`{"resultFormat": "table","querySource": "schema","database":"test-database"}`), } - kustoRequestMock = func(url string, cluster string, payload models.RequestPayload, enableUserTracking bool) (*models.TableResponse, error) { + kustoRequestMock = func(url string, cluster string, payload models.RequestPayload, enableUserTracking bool, application string) (*models.TableResponse, error) { require.Equal(t, "/v1/rest/query", url) require.Equal(t, ClusterURL, cluster) require.Equal(t, payload.DB, "test-database") @@ -62,7 +63,7 @@ func TestDatasource(t *testing.T) { TimeRange: backend.TimeRange{}, JSON: []byte(`{"resultFormat": "table","querySource": "schema"}`), } - kustoRequestMock = func(_ string, _ string, payload models.RequestPayload, _ bool) (*models.TableResponse, error) { + kustoRequestMock = func(_ string, _ string, payload models.RequestPayload, _ bool, _ string) (*models.TableResponse, error) { require.Equal(t, payload.DB, "test-default-database") return table, nil } @@ -100,8 +101,8 @@ func (c *fakeClient) TestARGsRequest(_ context.Context, _ *models.DatasourceSett panic("not implemented") } -func (c *fakeClient) KustoRequest(_ context.Context, cluster string, url string, payload models.RequestPayload, enableUserTracking bool) (*models.TableResponse, error) { - return kustoRequestMock(url, cluster, payload, enableUserTracking) +func (c *fakeClient) KustoRequest(_ context.Context, cluster string, url string, payload models.RequestPayload, enableUserTracking bool, application string) (*models.TableResponse, error) { + return kustoRequestMock(url, cluster, payload, enableUserTracking, application) } func (c *fakeClient) ARGClusterRequest(_ context.Context, payload models.ARGRequestPayload, additionalHeaders map[string]string) ([]models.ClusterOption, error) { diff --git a/pkg/azuredx/models/settings.go b/pkg/azuredx/models/settings.go index 9359b2be..44e5b7e0 100644 --- a/pkg/azuredx/models/settings.go +++ b/pkg/azuredx/models/settings.go @@ -20,6 +20,8 @@ type DatasourceSettings struct { CacheMaxAge string `json:"cacheMaxAge"` DynamicCaching bool `json:"dynamicCaching"` EnableUserTracking bool `json:"enableUserTracking"` + Application string `json:"application"` + // QueryTimeoutRaw is a duration string set in the datasource settings and corresponds // to the server execution timeout. diff --git a/pkg/azuredx/resource_handler.go b/pkg/azuredx/resource_handler.go index f030b19f..9c306c55 100644 --- a/pkg/azuredx/resource_handler.go +++ b/pkg/azuredx/resource_handler.go @@ -147,9 +147,9 @@ func (adx *AzureDataExplorer) getSchema(rw http.ResponseWriter, req *http.Reques respondWithError(rw, http.StatusBadRequest, "Invalid clusterUri", err) return } - + application := adx.settings.Application // Default to not sending the user request headers for schema requests - response, err := adx.client.KustoRequest(req.Context(), sanitized, ManagementApiPath, payload, false) + response, err := adx.client.KustoRequest(req.Context(), sanitized, ManagementApiPath, payload, false, application) if err != nil { respondWithError(rw, http.StatusInternalServerError, "Azure query unsuccessful", err) return @@ -196,9 +196,9 @@ func (adx *AzureDataExplorer) getDatabases(rw http.ResponseWriter, req *http.Req respondWithError(rw, http.StatusBadRequest, "Invalid clusterUri", err) return } - + application := adx.settings.Application // Default to not sending the user request headers for schema requests - response, err := adx.client.KustoRequest(req.Context(), sanitized, ManagementApiPath, payload, false) + response, err := adx.client.KustoRequest(req.Context(), sanitized, ManagementApiPath, payload, false, application) if err != nil { respondWithError(rw, http.StatusInternalServerError, "Azure query unsuccessful", err) return diff --git a/pkg/azuredx/resource_handler_test.go b/pkg/azuredx/resource_handler_test.go index 638160ff..cc2b400e 100644 --- a/pkg/azuredx/resource_handler_test.go +++ b/pkg/azuredx/resource_handler_test.go @@ -82,7 +82,7 @@ func (c *failingClient) TestARGsRequest(_ context.Context, _ *models.DatasourceS panic("not implemented") } -func (c *failingClient) KustoRequest(_ context.Context, _ string, _ string, _ models.RequestPayload, _ bool) (*models.TableResponse, error) { +func (c *failingClient) KustoRequest(_ context.Context, _ string, _ string, _ models.RequestPayload, _ bool, _ string) (*models.TableResponse, error) { return nil, fmt.Errorf("HTTP error: %v - %v", http.StatusBadRequest, "") } @@ -100,7 +100,7 @@ func (c *workingClient) TestARGsRequest(_ context.Context, _ *models.DatasourceS panic("not implemented") } -func (c *workingClient) KustoRequest(_ context.Context, _ string, _ string, _ models.RequestPayload, _ bool) (*models.TableResponse, error) { +func (c *workingClient) KustoRequest(_ context.Context, _ string, _ string, _ models.RequestPayload, _ bool, _ string) (*models.TableResponse, error) { return &models.TableResponse{ Tables: []models.Table{ { diff --git a/src/components/ConfigEditor/ApplicationConfig.tsx b/src/components/ConfigEditor/ApplicationConfig.tsx new file mode 100644 index 00000000..361098e6 --- /dev/null +++ b/src/components/ConfigEditor/ApplicationConfig.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { Field, Input } from '@grafana/ui'; +import { AdxDataSourceOptions, AdxDataSourceSecureOptions } from 'types'; +import { selectors } from 'test/selectors'; + +interface ApplicationConfigProps + extends DataSourcePluginOptionsEditorProps { + updateJsonData: (fieldName: T, value: AdxDataSourceOptions[T]) => void; +} + +const ApplicationConfig: React.FC = ({ options, updateJsonData }) => { + const { jsonData } = options; + + return ( + + ) => updateJsonData('application', ev.target.value)} + /> + + ); +}; + +export default ApplicationConfig; diff --git a/src/components/ConfigEditor/index.tsx b/src/components/ConfigEditor/index.tsx index 3a41a241..b7c6800d 100644 --- a/src/components/ConfigEditor/index.tsx +++ b/src/components/ConfigEditor/index.tsx @@ -10,6 +10,7 @@ import ConnectionConfig from './ConnectionConfig'; import DatabaseConfig from './DatabaseConfig'; import QueryConfig from './QueryConfig'; import TrackingConfig from './TrackingConfig'; +import ApplicationConfig from './ApplicationConfig'; import { getCredentials, getDefaultCredentials, @@ -52,6 +53,7 @@ const ConfigEditor: React.FC = (props) => { options.jsonData.cacheMaxAge || options.jsonData.useSchemaMapping || options.jsonData.enableUserTracking || + options.jsonData.application || options.secureJsonFields['OpenAIAPIKey'] ), [options] @@ -118,6 +120,7 @@ const ConfigEditor: React.FC = (props) => { > + diff --git a/src/components/__fixtures__/ConfigEditor.fixtures.ts b/src/components/__fixtures__/ConfigEditor.fixtures.ts index 9b216654..00244d48 100644 --- a/src/components/__fixtures__/ConfigEditor.fixtures.ts +++ b/src/components/__fixtures__/ConfigEditor.fixtures.ts @@ -29,6 +29,7 @@ export const mockConfigEditorProps = (optionsOverrides?: Partial) { super(instanceSettings); const useSchemaMapping = instanceSettings.jsonData.useSchemaMapping ?? false; const schemaMapping = instanceSettings.jsonData.schemaMappings ?? []; - + const application = instanceSettings.jsonData.application ?? 'Grafana-ADX'; this.backendSrv = getBackendSrv(); this.templateSrv = getTemplateSrv(); this.defaultOrFirstDatabase = instanceSettings.jsonData.defaultDatabase; @@ -63,6 +64,7 @@ export class AdxDataSource extends DataSourceWithBackend { const autoQuery = this.expressionParser.toAutoCompleteQuery(query, columns); @@ -329,7 +334,7 @@ export class AdxDataSource extends DataSourceWithBackend>; enableUserTracking: boolean; clusterUrl: string; + application: string; enableSecureSocksProxy?: boolean; // legacy options azureCloud?: string; diff --git a/yarn.lock b/yarn.lock index 7d1803fd..dce9c167 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12663,4 +12663,4 @@ yocto-queue@^0.1.0: yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== \ No newline at end of file