/* * * Copyright 2022 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package observability import ( "context" "encoding/json" "fmt" "os" gcplogging "cloud.google.com/go/logging" grpclogrecordpb "google.golang.org/grpc/observability/internal/logging" "google.golang.org/protobuf/encoding/protojson" ) // loggingExporter is the interface of logging exporter for gRPC Observability. // In future, we might expose this to allow users provide custom exporters. But // now, it exists for testing purposes. type loggingExporter interface { // EmitGrpcLogRecord writes a gRPC LogRecord to cache without blocking. EmitGrpcLogRecord(*grpclogrecordpb.GrpcLogRecord) // Close flushes all pending data and closes the exporter. Close() error } type cloudLoggingExporter struct { projectID string client *gcplogging.Client logger *gcplogging.Logger } func newCloudLoggingExporter(ctx context.Context, projectID string) (*cloudLoggingExporter, error) { c, err := gcplogging.NewClient(ctx, fmt.Sprintf("projects/%v", projectID)) if err != nil { return nil, fmt.Errorf("failed to create cloudLoggingExporter: %v", err) } defer logger.Infof("Successfully created cloudLoggingExporter") customTags := getCustomTags(os.Environ()) if len(customTags) != 0 { logger.Infof("Adding custom tags: %+v", customTags) } return &cloudLoggingExporter{ projectID: projectID, client: c, logger: c.Logger("grpc", gcplogging.CommonLabels(customTags)), }, nil } // mapLogLevelToSeverity maps the gRPC defined log level to Cloud Logging's // Severity. The canonical definition can be found at // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity. var logLevelToSeverity = map[grpclogrecordpb.GrpcLogRecord_LogLevel]gcplogging.Severity{ grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_UNKNOWN: 0, grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_TRACE: 100, // Cloud Logging doesn't have a trace level, treated as DEBUG. grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_DEBUG: 100, grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_INFO: 200, grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_WARN: 400, grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_ERROR: 500, grpclogrecordpb.GrpcLogRecord_LOG_LEVEL_CRITICAL: 600, } var protoToJSONOptions = &protojson.MarshalOptions{ UseProtoNames: true, UseEnumNumbers: false, } func (cle *cloudLoggingExporter) EmitGrpcLogRecord(l *grpclogrecordpb.GrpcLogRecord) { // Converts the log record content to a more readable format via protojson. jsonBytes, err := protoToJSONOptions.Marshal(l) if err != nil { logger.Infof("Unable to marshal log record: %v", l) return } var payload map[string]interface{} err = json.Unmarshal(jsonBytes, &payload) if err != nil { logger.Infof("Unable to unmarshal bytes to JSON: %v", jsonBytes) return } entry := gcplogging.Entry{ Timestamp: l.Timestamp.AsTime(), Severity: logLevelToSeverity[l.LogLevel], Payload: payload, } cle.logger.Log(entry) if logger.V(2) { logger.Infof("Uploading event to CloudLogging: %+v", entry) } } func (cle *cloudLoggingExporter) Close() error { var errFlush, errClose error if cle.logger != nil { errFlush = cle.logger.Flush() } if cle.client != nil { errClose = cle.client.Close() } if errFlush != nil && errClose != nil { return fmt.Errorf("failed to close exporter. Flush failed: %v; Close failed: %v", errFlush, errClose) } if errFlush != nil { return errFlush } if errClose != nil { return errClose } cle.logger = nil cle.client = nil logger.Infof("Closed CloudLogging exporter") return nil }