pkg/plugin/cloudlogging/cloudlogging.go (137 lines of code) (raw):
// Copyright 2022 Google LLC
//
// 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 cloudlogging
import (
"encoding/json"
"fmt"
"strings"
"cloud.google.com/go/logging/apiv2/loggingpb"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data"
rlpb "google.golang.org/genproto/googleapis/appengine/logging/v1"
alpb "google.golang.org/genproto/googleapis/cloud/audit"
ltype "google.golang.org/genproto/googleapis/logging/type"
"google.golang.org/protobuf/types/known/structpb"
)
// GetLogEntryMessage gets the message body of a LogEntry based on what kind of payload it is
// If it's JSON, we look for the `message` field since the other fields will be added as labels
func GetLogEntryMessage(entry *loggingpb.LogEntry) (string, error) {
switch t := entry.GetPayload().(type) {
case *loggingpb.LogEntry_JsonPayload:
if msg, ok := t.JsonPayload.Fields["message"]; ok {
return msg.GetStringValue(), nil
}
byteArr, err := t.JsonPayload.MarshalJSON()
if err != nil {
return "", fmt.Errorf("failed to marshal JSON payload: %v", err)
}
return string(byteArr), nil
case *loggingpb.LogEntry_TextPayload:
return t.TextPayload, nil
case *loggingpb.LogEntry_ProtoPayload:
return t.ProtoPayload.String(), nil
case nil:
return "", fmt.Errorf("empty payload %T", t)
default:
return "", fmt.Errorf("unknown payload type %T", t)
}
}
// GetLogLabels flattens a log entry's labels + resource labels into a map
func GetLogLabels(entry *loggingpb.LogEntry) data.Labels {
labels := make(data.Labels)
for k, v := range entry.GetLabels() {
labels[fmt.Sprintf("labels.\"%s\"", k)] = v
}
labels["id"] = entry.GetInsertId()
// This is how severity is set
labels["level"] = GetLogLevel(entry.GetSeverity())
resource := entry.GetResource()
if resourceType := resource.GetType(); resourceType != "" {
labels["resource.type"] = resourceType
}
// Add resource labels nested under `resource.labels.`
for k, v := range resource.GetLabels() {
labels[fmt.Sprintf("resource.labels.%s", k)] = v
}
switch t := entry.GetPayload().(type) {
case *loggingpb.LogEntry_JsonPayload:
fields := t.JsonPayload.GetFields()
for k, v := range fields {
if strings.ToLower(k) != "message" {
fieldToLabels(labels, fmt.Sprintf("jsonPayload.%s", k), v)
}
}
case *loggingpb.LogEntry_TextPayload:
labels["textPayload"] = t.TextPayload
case *loggingpb.LogEntry_ProtoPayload:
typeUrl := t.ProtoPayload.TypeUrl
if strings.HasSuffix(typeUrl, "AuditLog") {
var a alpb.AuditLog
if err := t.ProtoPayload.UnmarshalTo(&a); err != nil {
log.DefaultLogger.Error("Could not get AuditLog payload out of LogEntry: %v", err)
} else {
byteArr, _ := json.Marshal(a)
var inInterface map[string]*structpb.Value
json.Unmarshal(byteArr, &inInterface)
for k, v := range inInterface {
fieldToLabels(labels, fmt.Sprintf("protoPayload.%s", k), v)
}
}
} else if strings.HasSuffix(typeUrl, "RequestLog") {
var r rlpb.RequestLog
if err := t.ProtoPayload.UnmarshalTo(&r); err != nil {
log.DefaultLogger.Error("Could not get RequestLog payload out of LogEntry: %v", err)
} else {
byteArr, _ := json.Marshal(r)
var inInterface map[string]*structpb.Value
json.Unmarshal(byteArr, &inInterface)
for k, v := range inInterface {
fieldToLabels(labels, fmt.Sprintf("protoPayload.%s", k), v)
}
}
}
}
// If httpRequest exists in the log entry, include it too
httpRequest := entry.GetHttpRequest()
if httpRequest != nil {
byteArr, _ := json.Marshal(httpRequest)
var inInterface map[string]interface{}
json.Unmarshal(byteArr, &inInterface)
for k, v := range inInterface {
if k == "latency" {
labels["httpRequest.latency"] = httpRequest.Latency.AsDuration().String()
} else {
labels[fmt.Sprintf("httpRequest.%s", k)] = fmt.Sprintf("%v", v)
}
}
}
// Add trace data
traceId := entry.GetTrace()
spanId := entry.GetSpanId()
if traceId != "" {
trace := entry.GetTrace()
labels["trace"] = trace
labels["traceId"] = strings.Split(trace, "/")[len(strings.Split(trace, "/"))-1]
}
if spanId != "" {
labels["spanId"] = entry.GetSpanId()
}
return labels
}
// GetLogLevel maps the string value of a LogSeverity to one supported by Grafana
func GetLogLevel(severity ltype.LogSeverity) string {
switch severity {
case ltype.LogSeverity_EMERGENCY:
return "critical"
case ltype.LogSeverity_DEFAULT:
return "info"
// Other levels already map to supported values
default:
return strings.ToLower(severity.String())
}
}
// fieldToLabels converts a LogEntry Field value to a stringified version,
// recursively converting nested structs.
func fieldToLabels(labels data.Labels, fieldName string, field *structpb.Value) {
switch t := field.GetKind().(type) {
case *structpb.Value_NumberValue:
labels[fieldName] = fmt.Sprintf("%v", t.NumberValue)
case *structpb.Value_BoolValue:
labels[fieldName] = fmt.Sprintf("%t", t.BoolValue)
case *structpb.Value_StringValue:
labels[fieldName] = t.StringValue
case *structpb.Value_StructValue:
for key, value := range t.StructValue.GetFields() {
fieldToLabels(labels, fmt.Sprintf("%s.%s", fieldName, key), value)
}
default:
labels[fieldName] = field.String()
}
}