Skip to content

Commit 106688a

Browse files
authored
Adds Cloud Logging Service Endpoint option to configuration (#6)
* Adds Cloud Logging Service Endpoint option to configuration * Adds screenshots to plugin.json and README
1 parent 8a70b15 commit 106688a

11 files changed

Lines changed: 78 additions & 39 deletions

CHANGELOG.md

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

3+
## 1.1.0 (Unreleased)
4+
5+
* Adds Cloud Logging Service Endpoint to configuration
6+
* Fixes hide not working for Cloud Logging query
7+
* Fixes inability to retrieve projects displaying an error
8+
39
## 1.0.0 (2023-01-17)
410

511
Initial release.

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Google Cloud Logging Data Source
22

33
## Overview
4-
4+
![image info](./src/img/cloud_logging_explore_view.png)
55
The Google Cloud Logging Data Source is a backend data source plugin for Grafana,
66
which allows users to query and visualize their Google Cloud logs in Grafana.
77

@@ -32,12 +32,13 @@ You can following the those steps to enable it:
3232
8. Choose key type `JSON` and click `Create`. A JSON key file will be created and downloaded to your computer
3333

3434
### Grafana Configuration
35-
35+
![image info](./src/img/cloud_logging_config.png)
3636
1. With Grafana restarted, navigate to `Configuration -> Data sources` (or the route `/datasources`)
3737
2. Click "Add data source"
3838
3. Select "Google Cloud Logging"
3939
4. Provide credentials in a JWT file, either by using the file selector or pasting the contents of the file.
40-
5. Click "Save & test" to test that logs can be queried from Cloud Logging.
40+
5. If desired, provide a regional [Cloud Logging service endpoint](https://cloud.google.com/vpc/docs/regional-service-endpoints#cloud-logging) in order to only collect logs from a specific log bucket region
41+
6. Click "Save & test" to test that logs can be queried from Cloud Logging.
4142

4243
## Licenses
4344

pkg/plugin/cloudlogging/client.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
loggingpb "google.golang.org/genproto/googleapis/logging/v2"
3131
)
3232

33+
const testConnectionTimeout = time.Minute * 1
34+
3335
// API implements the methods we need to query logs and list projects from GCP
3436
type API interface {
3537
// ListLogs retrieves all logs matching some query filter up to the given limit
@@ -50,9 +52,9 @@ type Client struct {
5052
}
5153

5254
// NewClient creates a new Client using jsonCreds for authentication
53-
func NewClient(ctx context.Context, jsonCreds []byte) (*Client, error) {
55+
func NewClient(ctx context.Context, jsonCreds []byte, endpoint string) (*Client, error) {
5456
client, err := logging.NewClient(ctx, option.WithCredentialsJSON(jsonCreds),
55-
option.WithUserAgent("googlecloud-logging-datasource"))
57+
option.WithUserAgent("googlecloud-logging-datasource"), option.WithEndpoint(endpoint))
5658
if err != nil {
5759
return nil, err
5860
}
@@ -112,19 +114,30 @@ func (c *Client) ListProjects(ctx context.Context) ([]string, error) {
112114
// TestConnection queries for any log from the given project
113115
func (c *Client) TestConnection(ctx context.Context, projectID string) error {
114116
start := time.Now()
117+
118+
listCtx, cancel := context.WithTimeout(ctx, time.Duration(testConnectionTimeout))
119+
115120
defer func() {
121+
cancel()
116122
log.DefaultLogger.Debug("Finished testConnection", "duration", time.Since(start).String())
117123
}()
118124

119-
it := c.lClient.ListLogEntries(ctx, &loggingpb.ListLogEntriesRequest{
125+
it := c.lClient.ListLogEntries(listCtx, &loggingpb.ListLogEntriesRequest{
120126
ResourceNames: []string{projectResourceName(projectID)},
121127
PageSize: 1,
122128
})
123129

130+
if listCtx.Err() != nil {
131+
return errors.New("list entries: timeout")
132+
}
133+
124134
entry, err := it.Next()
125135
if err == iterator.Done {
126136
return errors.New("no entries")
127137
}
138+
if err == context.DeadlineExceeded {
139+
return errors.New("list entries: timeout")
140+
}
128141
if err != nil {
129142
return fmt.Errorf("list entries: %w", err)
130143
}

pkg/plugin/plugin.go

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,17 @@ const (
4242
privateKeyKey = "privateKey"
4343
)
4444

45-
// jwtToken is the fields parsed from a JWT token and sent from the front end
46-
type jwtToken struct {
45+
// config is the fields parsed from the front end
46+
type config struct {
4747
AuthType string `json:"authenticationType"`
4848
ClientEmail string `json:"clientEmail"`
4949
DefaultProject string `json:"defaultProject"`
5050
TokenURI string `json:"tokenUri"`
51+
Endpoint string `json:"endpoint"`
5152
}
5253

53-
// toServiceAccountJSON creates the serviceAccountJSON bytes from the JWT token fields
54-
// in gcpCreds
55-
func (c jwtToken) toServiceAccountJSON(privateKey string) ([]byte, error) {
54+
// toServiceAccountJSON creates the serviceAccountJSON bytes from the config fields
55+
func (c config) toServiceAccountJSON(privateKey string) ([]byte, error) {
5656
return json.Marshal(serviceAccountJSON{
5757
Type: "service_account",
5858
ProjectID: c.DefaultProject,
@@ -74,8 +74,8 @@ type serviceAccountJSON struct {
7474

7575
// NewCloudLoggingDatasource creates a new datasource instance.
7676
func NewCloudLoggingDatasource(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
77-
var creds jwtToken
78-
if err := json.Unmarshal(settings.JSONData, &creds); err != nil {
77+
var conf config
78+
if err := json.Unmarshal(settings.JSONData, &conf); err != nil {
7979
return nil, fmt.Errorf("unmarshal: %w", err)
8080
}
8181

@@ -84,12 +84,12 @@ func NewCloudLoggingDatasource(settings backend.DataSourceInstanceSettings) (ins
8484
return nil, errMissingCredentials
8585
}
8686

87-
serviceAccount, err := creds.toServiceAccountJSON(privateKey)
87+
serviceAccount, err := conf.toServiceAccountJSON(privateKey)
8888
if err != nil {
8989
return nil, fmt.Errorf("create credentials: %w", err)
9090
}
9191

92-
client, err := cloudlogging.NewClient(context.TODO(), serviceAccount)
92+
client, err := cloudlogging.NewClient(context.TODO(), serviceAccount, conf.Endpoint)
9393
if err != nil {
9494
return nil, err
9595
}
@@ -136,11 +136,7 @@ func (d *CloudLoggingDatasource) CallResource(ctx context.Context, req *backend.
136136

137137
projects, err := d.client.ListProjects(ctx)
138138
if err != nil {
139-
log.DefaultLogger.Error("error listing", "error", err)
140-
return sender.Send(&backend.CallResourceResponse{
141-
Status: http.StatusInternalServerError,
142-
Body: []byte(`Unable to list projects`),
143-
})
139+
log.DefaultLogger.Warn("problem listing projects", "error", err)
144140
}
145141

146142
body, err := json.Marshal(&ListProjectsResponse{Projects: projects})
@@ -182,9 +178,9 @@ func (d *CloudLoggingDatasource) QueryData(ctx context.Context, req *backend.Que
182178

183179
// queryModel is the fields needed to query from Grafana
184180
type queryModel struct {
185-
QueryText string `json:"queryText"`
186-
ProjectID string `json:"projectId"`
187-
MaxDataPoints int `json:"MaxDataPoints"`
181+
QueryText string `json:"queryText"`
182+
ProjectID string `json:"projectId"`
183+
Hide bool `json:"hide"`
188184
}
189185

190186
func (d *CloudLoggingDatasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {
@@ -196,6 +192,11 @@ func (d *CloudLoggingDatasource) query(ctx context.Context, pCtx backend.PluginC
196192
return response
197193
}
198194

195+
// Don't query if query should be hidden
196+
if q.Hide {
197+
return response
198+
}
199+
199200
clientRequest := cloudlogging.Query{
200201
ProjectID: q.ProjectID,
201202
Filter: q.QueryText,
@@ -254,8 +255,8 @@ func (d *CloudLoggingDatasource) CheckHealth(ctx context.Context, req *backend.C
254255
var status = backend.HealthStatusOk
255256
settings := req.PluginContext.DataSourceInstanceSettings
256257

257-
var creds jwtToken
258-
if err := json.Unmarshal(settings.JSONData, &creds); err != nil {
258+
var conf config
259+
if err := json.Unmarshal(settings.JSONData, &conf); err != nil {
259260
return nil, fmt.Errorf("unmarshal: %w", err)
260261
}
261262

@@ -264,12 +265,12 @@ func (d *CloudLoggingDatasource) CheckHealth(ctx context.Context, req *backend.C
264265
return nil, errMissingCredentials
265266
}
266267

267-
serviceAccount, err := creds.toServiceAccountJSON(privateKey)
268+
serviceAccount, err := conf.toServiceAccountJSON(privateKey)
268269
if err != nil {
269270
return nil, fmt.Errorf("create credentials: %w", err)
270271
}
271272

272-
client, err := cloudlogging.NewClient(ctx, serviceAccount)
273+
client, err := cloudlogging.NewClient(ctx, serviceAccount, conf.Endpoint)
273274
if err != nil {
274275
return nil, err
275276
}
@@ -280,7 +281,7 @@ func (d *CloudLoggingDatasource) CheckHealth(ctx context.Context, req *backend.C
280281
}
281282
}()
282283

283-
if err := client.TestConnection(ctx, creds.DefaultProject); err != nil {
284+
if err := client.TestConnection(ctx, conf.DefaultProject); err != nil {
284285
return &backend.CheckHealthResult{
285286
Status: backend.HealthStatusError,
286287
Message: fmt.Sprintf("failed to run test query: %s", err),
@@ -289,6 +290,6 @@ func (d *CloudLoggingDatasource) CheckHealth(ctx context.Context, req *backend.C
289290

290291
return &backend.CheckHealthResult{
291292
Status: status,
292-
Message: fmt.Sprintf("Successfully queried logs from GCP project %s", creds.DefaultProject),
293+
Message: fmt.Sprintf("Successfully queried logs from GCP project %s", conf.DefaultProject),
293294
}, nil
294295
}

src/ConfigEditor.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
import React from 'react';
1818
import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOption } from '@grafana/data';
19-
import { DataSourceOptions, DataSourceSecureJsonData, GoogleAuthType } from '@grafana/google-sdk';
20-
import { FieldSet } from '@grafana/ui';
19+
import { DataSourceSecureJsonData, GoogleAuthType } from '@grafana/google-sdk';
20+
import { Field, FieldSet, Input } from '@grafana/ui';
2121
import { JWTConfigEditor } from './components/JWTConfigEditor';
2222
import { JWTForm } from './components/JWTForm';
23+
import { CloudLoggingOptions } from 'types';
2324

24-
type Props = DataSourcePluginOptionsEditorProps<DataSourceOptions, DataSourceSecureJsonData>;
25+
type Props = DataSourcePluginOptionsEditorProps<CloudLoggingOptions, DataSourceSecureJsonData>;
2526

2627
/**
2728
* Config page that accepts a JWT token either through upload or pasting
@@ -47,7 +48,7 @@ export const ConfigEditor: React.FC<Props> = (props: Props) => {
4748
*
4849
* @param jsonData Remaining data source options
4950
*/
50-
const onResetJWTToken = (jsonData?: Partial<DataSourceOptions> | null) => {
51+
const onResetJWTToken = (jsonData?: Partial<CloudLoggingOptions> | null) => {
5152
const nextSecureJsonData = { ...secureJsonData };
5253
const nextJsonData = !jsonData ? { ...options.jsonData } : { ...options.jsonData, ...jsonData };
5354

@@ -63,7 +64,7 @@ export const ConfigEditor: React.FC<Props> = (props: Props) => {
6364
});
6465
};
6566

66-
const onJWTFormChange = (key: keyof DataSourceOptions) => onUpdateDatasourceJsonDataOption(props, key);
67+
const onJWTFormChange = (key: keyof CloudLoggingOptions) => onUpdateDatasourceJsonDataOption(props, key);
6768

6869
return (
6970
<>
@@ -91,6 +92,13 @@ export const ConfigEditor: React.FC<Props> = (props: Props) => {
9192
/>
9293
)}{' '}
9394
</FieldSet>
95+
<Field label="Cloud Logging Service Endpoint" description="Optional">
96+
<Input
97+
id="endpoint"
98+
value={options.jsonData.endpoint || ''}
99+
onChange={onJWTFormChange('endpoint')}
100+
/>
101+
</Field>
94102
</>
95103
);
96104
};

src/QueryEditor.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616

1717
import React, { KeyboardEvent, useEffect, useMemo, useState } from 'react';
1818
import { QueryEditorProps, SelectableValue } from '@grafana/data';
19-
import { DataSourceOptions } from '@grafana/google-sdk';
2019
import { InlineField, InlineFieldRow, LinkButton, Select, TextArea, Tooltip } from '@grafana/ui';
2120
import { DataSource } from './datasource';
22-
import { defaultQuery, Query } from './types';
21+
import { CloudLoggingOptions, defaultQuery, Query } from './types';
2322

24-
type Props = QueryEditorProps<DataSource, Query, DataSourceOptions>;
23+
type Props = QueryEditorProps<DataSource, Query, CloudLoggingOptions>;
2524

2625
/**
2726
* This is basically copied from {MQLQueryEditor} from the cloud-monitoring data source

src/img/cloud_logging_config.png

227 KB
Loading
1.57 MB
Loading
2.15 MB
Loading

src/plugin.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
"url": "https://github.com/GoogleCloudPlatform/cloud-logging-data-source-plugin/blob/master/LICENSE"
3232
}
3333
],
34-
"screenshots": [],
34+
"screenshots": [
35+
{"name": "cloud_logging_config", "path": "img/cloud_logging_config.png"},
36+
{"name": "cloud_logging_explore_view", "path": "img/cloud_logging_explore_view.png"},
37+
{"name": "cloud_logging_dashboard_view", "path": "img/cloud_logging_dashboard_view.png"}
38+
],
3539
"version": "%VERSION%",
3640
"updated": "%TODAY%"
3741
},

0 commit comments

Comments
 (0)