Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
c00428a
expanded the ConvertToOtlpExporter
XuechunHou Jan 16, 2026
90e9358
Verified the following:
XuechunHou Jan 16, 2026
cafb5d8
instrumentation_source and instrumentation_version
XuechunHou Jan 16, 2026
c41b129
flatten gcp.source_location logRecord attributes
XuechunHou Jan 19, 2026
f99c798
created a temp otlp exporter config for connecting to UTR logging sta…
XuechunHou Jan 21, 2026
1bdd9b8
removed changes to otlpexpoter for UTR metrics to avoid goldens diffe…
XuechunHou Jan 21, 2026
e9e08f3
resolved conflicts
XuechunHou Jan 22, 2026
29f8575
fixed conflicts
XuechunHou Jan 22, 2026
f166eaf
extend integration testsuite to support experiment combo testing
XuechunHou Jan 22, 2026
a8ca860
added temp processors to work around bugs in UTR
XuechunHou Jan 23, 2026
e021bb5
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Jan 26, 2026
15a1239
updated confgenerator test goldens
XuechunHou Jan 26, 2026
4d3c363
fixed merge conflict
XuechunHou Feb 11, 2026
1e457b0
removed temp code
XuechunHou Feb 11, 2026
bf0894a
updated integration tests to run against UTR logging
XuechunHou Feb 11, 2026
b096906
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Feb 20, 2026
df6a461
updated confgenerator test
XuechunHou Feb 20, 2026
d22ca1a
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Feb 20, 2026
1a6dff4
updated confgenerator test
XuechunHou Feb 20, 2026
22ca578
updated transformation tests
XuechunHou Feb 20, 2026
24e3538
removed temp processors, enabled utr logging for windows
XuechunHou Feb 20, 2026
2296809
explicitly zero out severity number when severity text is set
XuechunHou Feb 20, 2026
a0152a1
zero out severity number when severity text is set. Do this in filelo…
XuechunHou Feb 23, 2026
d148829
zero out severity text in syslog receiver not filelog
XuechunHou Feb 23, 2026
389b201
update transformation tests
XuechunHou Feb 23, 2026
93b4c02
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 5, 2026
2e0c055
resolved conflicts
XuechunHou Mar 5, 2026
2d231df
updated confgenerator test
XuechunHou Mar 5, 2026
3bd43cd
reran confgenerator update
XuechunHou Mar 5, 2026
9f1fce4
renamed utr confgenerator test
XuechunHou Mar 6, 2026
0ba8be2
regen confgenerator test goldens
XuechunHou Mar 6, 2026
93a46b5
update confgenerator test golden
XuechunHou Mar 6, 2026
1a78924
sort extension list to avoid confgenerator test flakiness
XuechunHou Mar 6, 2026
e375fd0
consider combined section when determines whether otel logging is sup…
XuechunHou Mar 6, 2026
0410b05
removed print statements
XuechunHou Mar 6, 2026
6fd9d2f
removed extra prints
XuechunHou Mar 6, 2026
f8e3d7b
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 6, 2026
2fd132f
drop gcp.internal.omit_otlp attribute for now until bug is fixed on t…
XuechunHou Mar 9, 2026
ef91d2f
removed unnecessary processor
XuechunHou Mar 9, 2026
b3a7e38
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 9, 2026
026026d
revert changes in modular.go
XuechunHou Mar 9, 2026
38613e5
added an integration test
XuechunHou Mar 9, 2026
32560fc
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 9, 2026
ca4b7c1
added empty trace and metrics section
XuechunHou Mar 9, 2026
2146d33
fixed otlp logs test
XuechunHou Mar 10, 2026
0f5ba5f
extend existing transformation tests to run for otel+otlpExporter
XuechunHou Mar 10, 2026
4f161f6
fixed integration test
XuechunHou Mar 10, 2026
f3ac67c
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 10, 2026
efe4909
Merge master into utr-logging
XuechunHou Mar 10, 2026
c4cdb82
merge master
XuechunHou Mar 10, 2026
809d05d
updated transformation tests
XuechunHou Mar 11, 2026
a2bf5a2
added comparison test
XuechunHou Mar 12, 2026
4d79afd
compare LogEntries in proto format
XuechunHou Mar 13, 2026
51e31eb
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 13, 2026
2de3c8b
revert changes
XuechunHou Mar 13, 2026
a708d7c
ignore project id in LogEntry.Trace
XuechunHou Mar 16, 2026
3fb7a60
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 17, 2026
fe07bbd
enabled TestModifyFields integration test for otel_logging+otlp_exporter
XuechunHou Mar 17, 2026
21e4878
revert changes
XuechunHou Mar 17, 2026
5ed8181
removed unused imports
XuechunHou Mar 17, 2026
0732c13
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 25, 2026
110556b
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Mar 30, 2026
1be54e7
fixed build error
XuechunHou Mar 30, 2026
915d375
Merge branch 'master' of github.com:GoogleCloudPlatform/ops-agent int…
XuechunHou Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions integration_test/ops_agent_test/compare_log_entries_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
// Copyright 2026 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.

//go:build integration_test

package ops_agent_test

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

loggingapiv2 "cloud.google.com/go/logging/apiv2"
"cloud.google.com/go/logging/apiv2/loggingpb"
"github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/plog"
"go.opentelemetry.io/collector/pdata/plog/plogotlp"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
"google.golang.org/api/transport/grpc"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/testing/protocmp"
)

func createOtlpClient(ctx context.Context) (plogotlp.GRPCClient, error) {
conn, err := grpc.Dial(ctx,
option.WithEndpoint("telemetry.googleapis.com:443"),
option.WithScopes("https://www.googleapis.com/auth/logging.write"),
)
if err != nil {
return nil, err
}
return plogotlp.NewGRPCClient(conn), nil
}

func readAndPrepareOtlpLogs(path string, projectID string, logName string) (plog.Logs, int, error) {
yamlBytes, err := os.ReadFile(path)
if err != nil {
return plog.NewLogs(), 0, err
}

var reqList []map[string]any
if err := yaml.Unmarshal(yamlBytes, &reqList); err != nil {
return plog.NewLogs(), 0, err
}

var allResourceLogs []any
for _, item := range reqList {
if rl, ok := item["resourceLogs"].([]any); ok {
allResourceLogs = append(allResourceLogs, rl...)
}
}
standardMap := map[string]any{
"resourceLogs": allResourceLogs,
}
jsonBytes, err := json.Marshal(standardMap)
if err != nil {
return plog.NewLogs(), 0, err
}

jsonBytes = []byte(strings.ReplaceAll(string(jsonBytes), `"now"`, `"0"`))

unmarshaler := &plog.JSONUnmarshaler{}
logs, err := unmarshaler.UnmarshalLogs(jsonBytes)
if err != nil {
return plog.NewLogs(), 0, err
}

totalCount := 0
for i := 0; i < logs.ResourceLogs().Len(); i++ {
rl := logs.ResourceLogs().At(i)
attrs := rl.Resource().Attributes()
attrs.PutStr("gcp.project_id", projectID)
attrs.PutStr("host.id", "1234567890123456789")
attrs.PutStr("cloud.project", projectID)
attrs.PutStr("cloud.availability_zone", "us-central1-a")
attrs.PutStr("cloud.region", "us-central1")
for j := 0; j < rl.ScopeLogs().Len(); j++ {
sl := rl.ScopeLogs().At(j)
for k := 0; k < sl.LogRecords().Len(); k++ {
lr := sl.LogRecords().At(k)
now := time.Now()
lr.SetTimestamp(pcommon.NewTimestampFromTime(now))
lr.SetObservedTimestamp(pcommon.NewTimestampFromTime(now))
lr.Attributes().PutStr("gcp.log_name", logName)
totalCount++
}
}
}

return logs, totalCount, nil
}

func findMatchingLogs(ctx context.Context, projectID string, logNameRegex string, window time.Duration, query string) ([]*loggingpb.LogEntry, error) {
client, err := loggingapiv2.NewClient(ctx)
if err != nil {
return nil, err
}
defer client.Close()

start := time.Now().Add(-window)
t := start.Format(time.RFC3339)
filter := fmt.Sprintf(`logName=~"projects/%s/logs/%s" AND timestamp > "%s"`, projectID, logNameRegex, t)
if query != "" {
filter += fmt.Sprintf(` AND %s`, query)
}

req := &loggingpb.ListLogEntriesRequest{
ResourceNames: []string{fmt.Sprintf("projects/%s", projectID)},
Filter: filter,
}
it := client.ListLogEntries(ctx, req)
var entries []*loggingpb.LogEntry
for {
entry, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
entries = append(entries, entry)
}
return entries, nil
}

func waitForLogs(ctx context.Context, projectID string, logName string, expectedCount int) ([]*loggingpb.LogEntry, error) {
var actualEntries []*loggingpb.LogEntry
var err error
delay := 2 * time.Second
for attempt := 0; attempt < 20; attempt++ {
actualEntries, err = findMatchingLogs(ctx, projectID, logName, 10*time.Minute, "")
if err == nil && len(actualEntries) == expectedCount {
return actualEntries, nil
}
time.Sleep(delay)
delay *= 2
if delay > 20*time.Second {
delay = 20 * time.Second
}
}
if err != nil {
return actualEntries, err
}
return actualEntries, fmt.Errorf("timeout waiting for %d logs, found %d", expectedCount, len(actualEntries))
}

func readExpectedEntries(path string) ([]*loggingpb.LogEntry, error) {
goldenBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var goldenData []map[string]any
if err := yaml.Unmarshal(goldenBytes, &goldenData); err != nil {
return nil, err
}

var expectedEntries []*loggingpb.LogEntry
for _, item := range goldenData {
if es, ok := item["entries"].([]any); ok {
for _, e := range es {
if entryMap, ok := e.(map[string]any); ok {
if ts, ok := entryMap["timestamp"]; ok && ts == "now" {
delete(entryMap, "timestamp")
}
}
}

jsonBytes, err := json.Marshal(item)
if err != nil {
return nil, err
}

req := &loggingpb.WriteLogEntriesRequest{}
opts := protojson.UnmarshalOptions{DiscardUnknown: true}
if err := opts.Unmarshal(jsonBytes, req); err != nil {
return nil, err
}

expectedEntries = append(expectedEntries, req.Entries...)
}
}
return expectedEntries, nil
}

// This test compares LogEntries generated by the telemetry.googleapis.com endpoint
// against those from the Google Cloud exporter, given the same input.
//
// The test reads logs in OTLP format from 'output_otel_otlpexporter.yaml' file under each transformation test case directory,
// constructs a request to the telemetry.googleapis.com endpoint to send these logs,
// and then queries Cloud Logging for the resulting LogEntries.
//
// Finally, it compares these results with the expected LogEntries found in 'output_otel.yaml'.
func TestCompareLogEntries(t *testing.T) {
t.Parallel()
projectID := os.Getenv("PROJECT")
if projectID == "" {
t.Skip("PROJECT environment variable is not set")
}

ctx := context.Background()
client, err := createOtlpClient(ctx)
if err != nil {
t.Fatalf("Failed to create OTLP client: %v", err)
}

testdataDir := "../../transformation_test/testdata"
dirs, err := os.ReadDir(testdataDir)
if err != nil {
t.Fatalf("Failed to read testdata dir: %v", err)
}

for _, dir := range dirs {
if !dir.IsDir() {
continue
}
caseName := dir.Name()
caseDir := filepath.Join(testdataDir, caseName)

otlpExporterPath := filepath.Join(caseDir, "output_otel_otlpexporter.yaml")
outputOtelPath := filepath.Join(caseDir, "output_otel.yaml")

if _, err := os.Stat(otlpExporterPath); os.IsNotExist(err) {
continue
}
if _, err := os.Stat(outputOtelPath); os.IsNotExist(err) {
continue
}

t.Run(caseName, func(t *testing.T) {
t.Parallel()
logName := fmt.Sprintf("transform-test-%d-%s", time.Now().UnixNano(), caseName)

logs, sentCount, err := readAndPrepareOtlpLogs(otlpExporterPath, projectID, logName)
if err != nil {
t.Fatalf("Failed to read and prepare logs: %v", err)
}

// Send logs to telemetry.googleapis.com
req := plogotlp.NewExportRequestFromLogs(logs)
if _, err := client.Export(ctx, req); err != nil {
st, _ := status.FromError(err)
marshaler := &plog.JSONMarshaler{}
reqBytes, _ := marshaler.MarshalLogs(logs)
t.Fatalf("Failed to export logs: %v\nDetails: %v\nRequest: %s", err, st.Details(), string(reqBytes))
}

expectedEntries, err := readExpectedEntries(outputOtelPath)
if err != nil {
t.Fatalf("Failed to read expected entries: %v", err)
}

actualEntries, err := waitForLogs(ctx, projectID, logName, sentCount)
if err != nil {
t.Fatalf("Failed waiting for logs: %v", err)
}
// Compare slices directly
if diff := cmp.Diff(expectedEntries, actualEntries,
protocmp.Transform(),
protocmp.IgnoreFields(&loggingpb.LogEntry{}, "timestamp", "insert_id", "log_name", "receive_timestamp", "resource"),
protocmp.IgnoreUnknown(),
); diff != "" {
t.Errorf("Mismatch in entries (-want +got):\n%s", diff)
}
})
}
}
2 changes: 1 addition & 1 deletion integration_test/ops_agent_test/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1700,7 +1700,7 @@ func TestExcludeLogsModifyFieldsOrder(t *testing.T) {

func TestModifyFields(t *testing.T) {
t.Parallel()
RunForEachImageAndFeatureFlag(t, []string{agents.OtelLoggingFeatureFlag}, func(t *testing.T, imageSpec string, feature string) {
RunForEachImageAndFeatureFlag(t, []string{agents.OtelLoggingFeatureFlag, OtelLoggingOTLPExporterFeatureFlag}, func(t *testing.T, imageSpec string, feature string) {
t.Parallel()
ctx, logger, vm := setupMainLogAndVM(t, imageSpec)
file1 := fmt.Sprintf("%s_1", logPathForImage(vm.ImageSpec))
Expand Down
Loading