Skip to content

Commit 4467a29

Browse files
authored
gcp/observability: implement logging via binarylog (#5196)
1 parent 18fdf54 commit 4467a29

File tree

16 files changed

+3425
-58
lines changed

16 files changed

+3425
-58
lines changed

gcp/observability/config.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
*
3+
* Copyright 2022 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package observability
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"os"
25+
"regexp"
26+
27+
gcplogging "cloud.google.com/go/logging"
28+
"golang.org/x/oauth2/google"
29+
configpb "google.golang.org/grpc/observability/internal/config"
30+
"google.golang.org/protobuf/encoding/protojson"
31+
)
32+
33+
const (
34+
envObservabilityConfig = "GRPC_CONFIG_OBSERVABILITY"
35+
envProjectID = "GOOGLE_CLOUD_PROJECT"
36+
logFilterPatternRegexpStr = `^([\w./]+)/((?:\w+)|[*])$`
37+
)
38+
39+
var logFilterPatternRegexp = regexp.MustCompile(logFilterPatternRegexpStr)
40+
41+
// fetchDefaultProjectID fetches the default GCP project id from environment.
42+
func fetchDefaultProjectID(ctx context.Context) string {
43+
// Step 1: Check ENV var
44+
if s := os.Getenv(envProjectID); s != "" {
45+
logger.Infof("Found project ID from env %v: %v", envProjectID, s)
46+
return s
47+
}
48+
// Step 2: Check default credential
49+
credentials, err := google.FindDefaultCredentials(ctx, gcplogging.WriteScope)
50+
if err != nil {
51+
logger.Infof("Failed to locate Google Default Credential: %v", err)
52+
return ""
53+
}
54+
if credentials.ProjectID == "" {
55+
logger.Infof("Failed to find project ID in default credential: %v", err)
56+
return ""
57+
}
58+
logger.Infof("Found project ID from Google Default Credential: %v", credentials.ProjectID)
59+
return credentials.ProjectID
60+
}
61+
62+
func validateFilters(config *configpb.ObservabilityConfig) error {
63+
for _, filter := range config.GetLogFilters() {
64+
if filter.Pattern == "*" {
65+
continue
66+
}
67+
match := logFilterPatternRegexp.FindStringSubmatch(filter.Pattern)
68+
if match == nil {
69+
return fmt.Errorf("invalid log filter pattern: %v", filter.Pattern)
70+
}
71+
}
72+
return nil
73+
}
74+
75+
func parseObservabilityConfig() (*configpb.ObservabilityConfig, error) {
76+
// Parse the config from ENV var
77+
if content := os.Getenv(envObservabilityConfig); content != "" {
78+
var config configpb.ObservabilityConfig
79+
if err := protojson.Unmarshal([]byte(content), &config); err != nil {
80+
return nil, fmt.Errorf("error parsing observability config from env %v: %v", envObservabilityConfig, err)
81+
}
82+
if err := validateFilters(&config); err != nil {
83+
return nil, fmt.Errorf("error parsing observability config: %v", err)
84+
}
85+
logger.Infof("Parsed ObservabilityConfig: %+v", &config)
86+
return &config, nil
87+
}
88+
// If the ENV var doesn't exist, do nothing
89+
return nil, nil
90+
}
91+
92+
func ensureProjectIDInObservabilityConfig(ctx context.Context, config *configpb.ObservabilityConfig) error {
93+
if config.GetDestinationProjectId() == "" {
94+
// Try to fetch the GCP project id
95+
projectID := fetchDefaultProjectID(ctx)
96+
if projectID == "" {
97+
return fmt.Errorf("empty destination project ID")
98+
}
99+
config.DestinationProjectId = projectID
100+
}
101+
return nil
102+
}

gcp/observability/exporting.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
*
3+
* Copyright 2022 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package observability
20+
21+
import (
22+
"context"
23+
"encoding/json"
24+
"fmt"
25+
"os"
26+
27+
gcplogging "cloud.google.com/go/logging"
28+
grpclogrecordpb "google.golang.org/grpc/observability/internal/logging"
29+
"google.golang.org/protobuf/encoding/protojson"
30+
)
31+
32+
// loggingExporter is the interface of logging exporter for gRPC Observability.
33+
// In future, we might expose this to allow users provide custom exporters. But
34+
// now, it exists for testing purposes.
35+
type loggingExporter interface {
36+
// EmitGrpcLogRecord writes a gRPC LogRecord to cache without blocking.
37+
EmitGrpcLogRecord(*grpclogrecordpb.GrpcLogRecord)
38+
// Close flushes all pending data and closes the exporter.
39+
Close() error
40+
}
41+
42+
type cloudLoggingExporter struct {
43+
projectID string
44+
client *gcplogging.Client
45+
logger *gcplogging.Logger
46+
}
47+
48+
func newCloudLoggingExporter(ctx context.Context, projectID string) (*cloudLoggingExporter, error) {
49+
c, err := gcplogging.NewClient(ctx, fmt.Sprintf("projects/%v", projectID))
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to create cloudLoggingExporter: %v", err)
52+
}
53+
defer logger.Infof("Successfully created cloudLoggingExporter")
54+
customTags := getCustomTags(os.Environ())
55+
if len(customTags) != 0 {
56+
logger.Infof("Adding custom tags: %+v", customTags)
57+
}
58+
return &cloudLoggingExporter{
59+
projectID: projectID,
60+
client: c,
61+
logger: c.Logger("grpc", gcplogging.CommonLabels(customTags)),
62+
}, nil
63+
}
64+
65+
// mapLogLevelToSeverity maps the gRPC defined log level to Cloud Logging's
66+
// Severity. The canonical definition can be found at
67+
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity.
68+
var logLevelToSeverity = map[grpclogrecordpb.GrpcLogRecord_LogLevel]gcplogging.Severity{
69+
grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_UNKNOWN: 0,
70+
grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_TRACE: 100, // Cloud Logging doesn't have a trace level, treated as DEBUG.
71+
grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_DEBUG: 100,
72+
grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_INFO: 200,
73+
grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_WARN: 400,
74+
grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_ERROR: 500,
75+
grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_CRITICAL: 600,
76+
}
77+
78+
var protoToJSONOptions = &protojson.MarshalOptions{
79+
UseProtoNames: true,
80+
UseEnumNumbers: false,
81+
}
82+
83+
func (cle *cloudLoggingExporter) EmitGrpcLogRecord(l *grpclogrecordpb.GrpcLogRecord) {
84+
// Converts the log record content to a more readable format via protojson.
85+
jsonBytes, err := protoToJSONOptions.Marshal(l)
86+
if err != nil {
87+
logger.Infof("Unable to marshal log record: %v", l)
88+
return
89+
}
90+
var payload map[string]interface{}
91+
err = json.Unmarshal(jsonBytes, &payload)
92+
if err != nil {
93+
logger.Infof("Unable to unmarshal bytes to JSON: %v", jsonBytes)
94+
return
95+
}
96+
entry := gcplogging.Entry{
97+
Timestamp: l.Timestamp.AsTime(),
98+
Severity: logLevelToSeverity[l.LogLevel],
99+
Payload: payload,
100+
}
101+
cle.logger.Log(entry)
102+
if logger.V(2) {
103+
logger.Infof("Uploading event to CloudLogging: %+v", entry)
104+
}
105+
}
106+
107+
func (cle *cloudLoggingExporter) Close() error {
108+
var errFlush, errClose error
109+
if cle.logger != nil {
110+
errFlush = cle.logger.Flush()
111+
}
112+
if cle.client != nil {
113+
errClose = cle.client.Close()
114+
}
115+
if errFlush != nil && errClose != nil {
116+
return fmt.Errorf("failed to close exporter. Flush failed: %v; Close failed: %v", errFlush, errClose)
117+
}
118+
if errFlush != nil {
119+
return errFlush
120+
}
121+
if errClose != nil {
122+
return errClose
123+
}
124+
cle.logger = nil
125+
cle.client = nil
126+
logger.Infof("Closed CloudLogging exporter")
127+
return nil
128+
}

gcp/observability/go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module google.golang.org/grpc/observability
2+
3+
go 1.14
4+
5+
require (
6+
cloud.google.com/go/logging v1.4.2
7+
github.com/golang/protobuf v1.5.2
8+
github.com/google/uuid v1.3.0
9+
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
10+
google.golang.org/grpc v1.43.0
11+
google.golang.org/protobuf v1.27.1
12+
)
13+
14+
replace google.golang.org/grpc => ../../

0 commit comments

Comments
 (0)