|
| 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 | +} |
0 commit comments