Skip to content

Commit

Permalink
fix(logging): added marshalling methods for proto fields in structure…
Browse files Browse the repository at this point in the history
…dLogEntry (#8979)

* fix(logging): added marshalling methods for proto fields in
structuredLogEntry

* Renamed structuredLogEntryHttpRequest due to linter error.
  • Loading branch information
gkevinzheng committed Nov 13, 2023
1 parent 9af7e85 commit aa385f9
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 50 deletions.
100 changes: 53 additions & 47 deletions logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 54,7 @@ import (
logtypepb "google.golang.org/genproto/googleapis/logging/type"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
Expand Down Expand Up @@ -967,65 968,70 @@ func toLogEntryInternalImpl(e Entry, l *Logger, parent string, skipLevels int) (
// entry represents the fields of a logging.Entry that can be parsed by Logging agent.
// See the mappings at https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
type structuredLogEntry struct {
// JsonMessage map[string]interface{} `json:"message,omitempty"`
// TextMessage string `json:"message,omitempty"`
Message json.RawMessage `json:"message"`
Severity string `json:"severity,omitempty"`
HTTPRequest *logtypepb.HttpRequest `json:"httpRequest,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
Labels map[string]string `json:"logging.googleapis.com/labels,omitempty"`
InsertID string `json:"logging.googleapis.com/insertId,omitempty"`
Operation *logpb.LogEntryOperation `json:"logging.googleapis.com/operation,omitempty"`
SourceLocation *logpb.LogEntrySourceLocation `json:"logging.googleapis.com/sourceLocation,omitempty"`
SpanID string `json:"logging.googleapis.com/spanId,omitempty"`
Trace string `json:"logging.googleapis.com/trace,omitempty"`
TraceSampled bool `json:"logging.googleapis.com/trace_sampled,omitempty"`
Message json.RawMessage `json:"message"`
Severity string `json:"severity,omitempty"`
HTTPRequest *structuredLogEntryHTTPRequest `json:"httpRequest,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
Labels map[string]string `json:"logging.googleapis.com/labels,omitempty"`
InsertID string `json:"logging.googleapis.com/insertId,omitempty"`
Operation *structuredLogEntryOperation `json:"logging.googleapis.com/operation,omitempty"`
SourceLocation *structuredLogEntrySourceLocation `json:"logging.googleapis.com/sourceLocation,omitempty"`
SpanID string `json:"logging.googleapis.com/spanId,omitempty"`
Trace string `json:"logging.googleapis.com/trace,omitempty"`
TraceSampled bool `json:"logging.googleapis.com/trace_sampled,omitempty"`
}

func convertSnakeToMixedCase(snakeStr string) string {
words := strings.Split(snakeStr, "_")
mixedStr := words[0]
for _, word := range words[1:] {
mixedStr = strings.Title(word)
}
return mixedStr
// structuredLogEntryHTTPRequest wraps the HTTPRequest proto field in structuredLogEntry for easier JSON marshalling.
type structuredLogEntryHTTPRequest struct {
request *logtypepb.HttpRequest
}

func (s structuredLogEntry) MarshalJSON() ([]byte, error) {
// extract structuredLogEntry into json map
type Alias structuredLogEntry
var mapData map[string]interface{}
data, err := json.Marshal(Alias(s))
if err == nil {
err = json.Unmarshal(data, &mapData)
}
if err == nil {
// ensure all inner dicts use mixed case instead of snake case
innerDicts := [3]string{"httpRequest", "logging.googleapis.com/operation", "logging.googleapis.com/sourceLocation"}
for _, field := range innerDicts {
if fieldData, ok := mapData[field]; ok {
formattedFieldData := make(map[string]interface{})
for k, v := range fieldData.(map[string]interface{}) {
formattedFieldData[convertSnakeToMixedCase(k)] = v
}
mapData[field] = formattedFieldData
}
}
// serialize json map into raw bytes
return json.Marshal(mapData)
}
return data, err
func (s structuredLogEntryHTTPRequest) MarshalJSON() ([]byte, error) {
return protojson.Marshal(s.request)
}

// structuredLogEntryOperation wraps the Operation proto field in structuredLogEntry for easier JSON marshalling.
type structuredLogEntryOperation struct {
operation *logpb.LogEntryOperation
}

func (s structuredLogEntryOperation) MarshalJSON() ([]byte, error) {
return protojson.Marshal(s.operation)
}

// structuredLogEntrySourceLocation wraps the SourceLocation proto field in structuredLogEntry for easier JSON marshalling.
type structuredLogEntrySourceLocation struct {
sourceLocation *logpb.LogEntrySourceLocation
}

func (s structuredLogEntrySourceLocation) MarshalJSON() ([]byte, error) {
return protojson.Marshal(s.sourceLocation)
}

func serializeEntryToWriter(entry *logpb.LogEntry, w io.Writer) error {
var httpRequest *structuredLogEntryHTTPRequest
if entry.HttpRequest != nil {
httpRequest = &structuredLogEntryHTTPRequest{entry.HttpRequest}
}

var operation *structuredLogEntryOperation
if entry.Operation != nil {
operation = &structuredLogEntryOperation{entry.Operation}
}

var sourceLocation *structuredLogEntrySourceLocation
if entry.SourceLocation != nil {
sourceLocation = &structuredLogEntrySourceLocation{entry.SourceLocation}
}

jsonifiedEntry := structuredLogEntry{
Severity: entry.Severity.String(),
HTTPRequest: entry.HttpRequest,
HTTPRequest: httpRequest,
Timestamp: entry.Timestamp.String(),
Labels: entry.Labels,
InsertID: entry.InsertId,
Operation: entry.Operation,
SourceLocation: entry.SourceLocation,
Operation: operation,
SourceLocation: sourceLocation,
SpanID: entry.SpanId,
Trace: entry.Trace,
TraceSampled: entry.TraceSampled,
Expand Down
20 changes: 17 additions & 3 deletions logging/logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1438,7 1438,7 @@ func TestRedirectOutputFormats(t *testing.T) {
},
want: `{"httpRequest":{"requestMethod":"POST","requestUrl":"https://example.com/test"},"logging.googleapis.com/insertId":"0000AAA01",`
`"logging.googleapis.com/labels":{"key1":"value1","key2":"value2"},"logging.googleapis.com/operation":{"id":"0123456789","producer":"test"},`
`"logging.googleapis.com/sourceLocation":{"file":"acme.go","function":"main","line":100},"logging.googleapis.com/spanId":"000000000001",`
`"logging.googleapis.com/sourceLocation":{"file":"acme.go","function":"main","line":"100"},"logging.googleapis.com/spanId":"000000000001",`
`"logging.googleapis.com/trace":"projects/P/ABCD12345678AB12345678","logging.googleapis.com/trace_sampled":true,`
`"message":"this is text payload","severity":"DEBUG","timestamp":"seconds:1000"}`,
},
Expand Down Expand Up @@ -1474,7 1474,7 @@ func TestRedirectOutputFormats(t *testing.T) {
},
want: `{"httpRequest":{"requestMethod":"POST","requestUrl":"https://example.com/test"},"logging.googleapis.com/insertId":"0000AAA01",`
`"logging.googleapis.com/labels":{"key1":"value1","key2":"value2"},"logging.googleapis.com/operation":{"id":"0123456789","producer":"test"},`
`"logging.googleapis.com/sourceLocation":{"file":"acme.go","function":"main","line":100},"logging.googleapis.com/spanId":"000000000001",`
`"logging.googleapis.com/sourceLocation":{"file":"acme.go","function":"main","line":"100"},"logging.googleapis.com/spanId":"000000000001",`
`"logging.googleapis.com/trace":"projects/P/ABCD12345678AB12345678","logging.googleapis.com/trace_sampled":true,`
`"message":{"Latency":321,"Message":"message part of the payload"},"severity":"DEBUG","timestamp":"seconds:1000"}`,
},
Expand Down Expand Up @@ -1506,7 1506,21 @@ func TestRedirectOutputFormats(t *testing.T) {
t.Errorf("Expected error: % v, want: %v\n", tc.in, tc.wantError)
}
got := strings.TrimSpace(buffer.String())
if got != tc.want {

// Compare structure equivalence of the outputs, not string equivalence, as order doesn't matter.
var gotJson, wantJson interface{}

err = json.Unmarshal([]byte(got), &gotJson)
if err != nil {
t.Errorf("Error when serializing JSON output: %v", err)
}

err = json.Unmarshal([]byte(tc.want), &wantJson)
if err != nil {
t.Fatalf("Error unmarshalling JSON input for want: %v", err)
}

if !reflect.DeepEqual(gotJson, wantJson) {
t.Errorf("TestRedirectOutputFormats: % v: got %v, want %v", tc.in, got, tc.want)
}
}
Expand Down

0 comments on commit aa385f9

Please sign in to comment.