Repository: GoogleCloudPlatform/opentelemetry-operations-go
Pull request #373 (2022-04-29 20:05:04)

Collector logging exporter
This picks the logic commits from https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/tree/collector_logging into a new branch based off `main`

damemi: Replacing with https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/pull/373 to just manually pick the logic

Patch:
diff --git a/exporter/collector/config.go b/exporter/collector/config.go
index 19bbfc0..1a60842 100644
--- a/exporter/collector/config.go
+++ b/exporter/collector/config.go
@@ -33,6 +33,8 @@ type Config struct {
 	TraceConfig TraceConfig `mapstructure:"trace"`
 
 	MetricConfig MetricConfig `mapstructure:"metric"`
+
+	LogConfig LogConfig `mapstructure:"log"`
 }
 
 type ClientConfig struct {
@@ -111,6 +113,14 @@ type ResourceFilter struct {
 	Prefix string `mapstructure:"prefix"`
 }
 
+type LogConfig struct {
+	ClientConfig ClientConfig `mapstructure:",squash"`
+
+	// DefaultLogLanme sets the fallback log name to use when one isn't explicitly set
+	// for a log entry. If unset, logs without a log name will raise an error.
+	DefaultLogName string `mapstructure:"default_log_name"`
+}
+
 // Known metric domains. Note: This is now configurable for advanced usages.
 var domains = []string{"googleapis.com", "kubernetes.io", "istio.io", "knative.dev"}
 
diff --git a/exporter/collector/go.mod b/exporter/collector/go.mod
index d719a7d..f62cda7 100644
--- a/exporter/collector/go.mod
+++ b/exporter/collector/go.mod
@@ -3,6 +3,7 @@ module github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/colle
 go 1.17
 
 require (
+	cloud.google.com/go/logging v1.4.2
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.4.0
 	github.com/census-instrumentation/opencensus-proto v0.3.0
 	github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.49.0
@@ -22,13 +23,16 @@ require (
 	cloud.google.com/go/monitoring v1.4.0
 	cloud.google.com/go/trace v1.2.0
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.0.0-00010101000000-000000000000
+	github.com/benbjohnson/clock v1.3.0 // indirect
 	github.com/google/go-cmp v0.5.7
 	go.opentelemetry.io/collector/pdata v0.49.0
+	go.uber.org/multierr v1.8.0
 	go.uber.org/zap v1.21.0
 	golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
 )
 
 require (
+	cloud.google.com/go v0.100.2 // indirect
 	cloud.google.com/go/compute v1.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
@@ -41,8 +45,8 @@ require (
 	github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.49.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
-	go.uber.org/multierr v1.8.0 // indirect
 	golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
diff --git a/exporter/collector/go.sum b/exporter/collector/go.sum
index 8821528..87f3411 100644
--- a/exporter/collector/go.sum
+++ b/exporter/collector/go.sum
@@ -40,6 +40,8 @@ cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiL
 cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/logging v1.4.2 h1:Mu2Q75VBDQlW1HlBMjTX4X84UFR73G1TiLlRYc/b7tA=
+cloud.google.com/go/logging v1.4.2/go.mod h1:jco9QZSx8HiVVqLJReq7z7bVdj0P1Jb9PDFs63T+axo=
 cloud.google.com/go/monitoring v1.4.0 h1:05+IuNMbh40hbxcqQ4SnynbwZbLG1Wc9dysIJxnfv7U=
 cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -58,8 +60,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -342,6 +345,7 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
@@ -360,6 +364,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -399,6 +404,7 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -510,6 +516,7 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
 google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
 google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I=
 google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
 google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
 google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
@@ -573,7 +580,9 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
diff --git a/exporter/collector/logs.go b/exporter/collector/logs.go
new file mode 100644
index 0000000..4cff14c
--- /dev/null
+++ b/exporter/collector/logs.go
@@ -0,0 +1,388 @@
+// 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 collector
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+
+	"cloud.google.com/go/logging"
+	loggingv2 "cloud.google.com/go/logging/apiv2"
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/plog"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+	"google.golang.org/genproto/googleapis/api/monitoredres"
+	logpb "google.golang.org/genproto/googleapis/logging/v2"
+	"google.golang.org/protobuf/proto"
+)
+
+const (
+	HTTPRequestAttributeKey = "com.google.httpRequest"
+
+	defaultMaxEntrySize   = 256000   // 256 KB
+	defaultMaxRequestSize = 10000000 // 10 MB
+)
+
+// severityMapping maps the integer severity level values from OTel [0-24]
+// to matching Cloud Logging severity levels
+var severityMapping = []logging.Severity{
+	logging.Default,   // Default, 0
+	logging.Debug,     //
+	logging.Debug,     //
+	logging.Debug,     //
+	logging.Debug,     //
+	logging.Debug,     //
+	logging.Debug,     //
+	logging.Debug,     //
+	logging.Debug,     // 1-8 -> Debug
+	logging.Info,      //
+	logging.Info,      // 9-10 -> Info
+	logging.Notice,    //
+	logging.Notice,    // 11-12 -> Notice
+	logging.Warning,   //
+	logging.Warning,   //
+	logging.Warning,   //
+	logging.Warning,   // 13-16 -> Warning
+	logging.Error,     //
+	logging.Error,     //
+	logging.Error,     //
+	logging.Error,     // 17-20 -> Error
+	logging.Critical,  //
+	logging.Critical,  // 21-22 -> Critical
+	logging.Alert,     // 23 -> Alert
+	logging.Emergency, // 24 -> Emergency
+}
+
+type LogsExporter struct {
+	cfg    Config
+	obs    selfObservability
+	mapper logMapper
+
+	loggingClient *loggingv2.Client
+}
+
+type logMapper struct {
+	obs selfObservability
+	cfg Config
+}
+
+func NewGoogleCloudLogsExporter(
+	ctx context.Context,
+	cfg Config,
+	log *zap.Logger,
+) (*LogsExporter, error) {
+	setProjectFromADC(ctx, &cfg, loggingv2.DefaultAuthScopes())
+
+	clientOpts, err := generateClientOptions(&cfg.LogConfig.ClientConfig, cfg.UserAgent)
+	if err != nil {
+		return nil, err
+	}
+
+	loggingClient, err := loggingv2.NewClient(ctx, clientOpts...)
+	if err != nil {
+		return nil, err
+	}
+
+	obs := selfObservability{
+		log: log,
+	}
+
+	return &LogsExporter{
+		cfg: cfg,
+		obs: obs,
+		mapper: logMapper{
+			obs: obs,
+			cfg: cfg,
+		},
+
+		loggingClient: loggingClient,
+	}, nil
+}
+
+func (l *LogsExporter) Shutdown(ctx context.Context) error {
+	return l.loggingClient.Close()
+}
+
+func (l *LogsExporter) PushLogs(ctx context.Context, ld plog.Logs) error {
+	entries, err := l.mapper.createEntries(ld)
+	if err != nil {
+		return err
+	}
+
+	errors := []error{}
+	entry := 0
+	currentBatchSize := 0
+	// Send entries in WriteRequest chunks
+	// TODO(damemi): Add integration test for batch request processing
+	for len(entries) > 0 {
+		// default to max int so that when we are at index=len we skip the size check to avoid panic
+		// (index=len is the break condition when we reassign entries=entries[len:])
+		entrySize := defaultMaxRequestSize
+		if entry < len(entries) {
+			entrySize = proto.Size(entries[entry])
+		}
+
+		// this block gets skipped if we are out of entries to check
+		if currentBatchSize+entrySize < defaultMaxRequestSize {
+			// if adding the current entry to the current batch doesn't go over the request size,
+			// increase the index and account for the new request size, then continue
+			currentBatchSize += entrySize
+			entry++
+			continue
+		}
+
+		// if the current entry goes over the request size (or we have gone over every entry, i.e. index=len),
+		// write the list up to but not including the current entry's index
+		_, err := l.writeLogEntries(ctx, entries[:entry])
+		if err != nil {
+			errors = append(errors, err)
+		}
+
+		entries = entries[entry:]
+		entry = 0
+	}
+
+	if len(errors) > 0 {
+		return multierr.Combine(errors...)
+	}
+	return nil
+}
+
+func (l logMapper) createEntries(ld plog.Logs) ([]*logpb.LogEntry, error) {
+	errors := []error{}
+	mapper := &metricMapper{} // Refactor metricMapper to map MRs for logging?
+
+	entries := make([]*logpb.LogEntry, 0, 0)
+	for i := 0; i < ld.ResourceLogs().Len(); i++ {
+		rl := ld.ResourceLogs().At(i)
+		mr, _ := mapper.resourceToMonitoredResource(rl.Resource())
+
+		for j := 0; j < rl.ScopeLogs().Len(); j++ {
+			sl := rl.ScopeLogs().At(j)
+			instrumentationSource := sl.Scope().Name()
+			instrumentationVersion := sl.Scope().Version()
+
+			for k := 0; k < sl.LogRecords().Len(); k++ {
+				log := sl.LogRecords().At(k)
+
+				entry, err := l.logToEntry(
+					log,
+					mr,
+					instrumentationSource,
+					instrumentationVersion,
+					time.Now())
+				if err != nil {
+					errors = append(errors, err)
+					continue
+				}
+
+				internalLogEntry, err := logging.ToLogEntry(entry, fmt.Sprintf("projects/%s", l.cfg.ProjectID))
+				if err != nil {
+					errors = append(errors, err)
+					continue
+				}
+				logName, err := l.getLogName(log)
+				if err != nil {
+					errors = append(errors, err)
+					continue
+				}
+				internalLogEntry.LogName = fmt.Sprintf("projects/%s/logs/%s", l.cfg.ProjectID, url.PathEscape(logName))
+				internalLogEntry.Resource = mr
+
+				entries = append(entries, internalLogEntry)
+			}
+		}
+	}
+
+	return entries, multierr.Combine(errors...)
+}
+
+func (l *LogsExporter) writeLogEntries(ctx context.Context, batch []*logpb.LogEntry) (*logpb.WriteLogEntriesResponse, error) {
+	request := &logpb.WriteLogEntriesRequest{
+		PartialSuccess: true,
+		Entries:        batch,
+	}
+
+	// TODO(damemi): handle response code
+	return l.loggingClient.WriteLogEntries(ctx, request)
+}
+
+func (l logMapper) getLogName(log plog.LogRecord) (string, error) {
+	logNameAttr, exists := log.Attributes().Get("com.google.logName")
+	if exists {
+		return logNameAttr.AsString(), nil
+	}
+	if len(l.cfg.LogConfig.DefaultLogName) > 0 {
+		return l.cfg.LogConfig.DefaultLogName, nil
+	}
+	return "", fmt.Errorf("no log name provided.  Set the 'default_log_name' option, or add the 'com.google.logName' attribute to set a log name")
+}
+
+func (l logMapper) logToEntry(
+	log plog.LogRecord,
+	mr *monitoredres.MonitoredResource,
+	instrumentationSource string,
+	instrumentationVersion string,
+	processTime time.Time,
+) (logging.Entry, error) {
+	entry := logging.Entry{
+		Resource: mr,
+	}
+
+	// TODO(damemi): Make overwriting these labels (if they already exist) configurable
+	if len(instrumentationSource) > 0 {
+		entry.Labels["instrumentation_source"] = instrumentationSource
+	}
+	if len(instrumentationVersion) > 0 {
+		entry.Labels["instrumentation_version"] = instrumentationVersion
+	}
+
+	// if timestamp has not been explicitly initialized, default to current time
+	// TODO: figure out how to fall back to observed_time_unix_nano as recommended
+	//   (see https://github.com/open-telemetry/opentelemetry-proto/blob/4abbb78/opentelemetry/proto/logs/v1/logs.proto#L176-L179)
+	entry.Timestamp = log.Timestamp().AsTime()
+	if log.Timestamp() == 0 {
+		entry.Timestamp = processTime
+	}
+
+	// parse LogEntrySourceLocation struct from OTel attribute
+	sourceLocation, ok := log.Attributes().Get("com.google.sourceLocation")
+	if ok {
+		var logEntrySourceLocation logpb.LogEntrySourceLocation
+		err := json.Unmarshal(sourceLocation.BytesVal(), &logEntrySourceLocation)
+		if err != nil {
+			return entry, err
+		}
+		entry.SourceLocation = &logEntrySourceLocation
+	}
+
+	// parse TraceID and SpanID, if present
+	if !log.TraceID().IsEmpty() {
+		entry.Trace = log.TraceID().HexString()
+	}
+	if !log.SpanID().IsEmpty() {
+		entry.SpanID = log.SpanID().HexString()
+	}
+
+	httpRequestAttr, ok := log.Attributes().Get(HTTPRequestAttributeKey)
+	if ok {
+		httpRequest, err := l.parseHTTPRequest(httpRequestAttr)
+		if err != nil {
+			l.obs.log.Debug("Unable to parse httpRequest", zap.Error(err))
+		}
+		entry.HTTPRequest = httpRequest
+	}
+
+	if log.SeverityNumber() < 0 || int(log.SeverityNumber()) > len(severityMapping)-1 {
+		return entry, fmt.Errorf("Unknown SeverityNumber %v", log.SeverityNumber())
+	}
+	entry.Severity = severityMapping[log.SeverityNumber()]
+
+	payload, err := parseEntryPayload(log.Body())
+	if err != nil {
+		return entry, nil
+	}
+	entry.Payload = payload
+
+	return entry, nil
+}
+
+func parseEntryPayload(logBody pcommon.Value) (interface{}, error) {
+	if len(logBody.AsString()) == 0 {
+		return nil, nil
+	}
+	switch logBody.Type() {
+	case pcommon.ValueTypeBytes:
+		return logBody.BytesVal(), nil
+	case pcommon.ValueTypeString:
+		return logBody.AsString(), nil
+	case pcommon.ValueTypeMap:
+		return logBody.MapVal().AsRaw(), nil
+
+	default:
+		return nil, fmt.Errorf("unknown log body value %v", logBody.Type().String())
+	}
+}
+
+// JSON keys derived from:
+// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#httprequest
+type httpRequestLog struct {
+	RequestMethod                  string `json:"requestMethod"`
+	RequestURL                     string `json:"requestUrl"`
+	RequestSize                    int64  `json:"requestSize,string"`
+	Status                         int    `json:"status,string"`
+	ResponseSize                   int64  `json:"responseSize,string"`
+	UserAgent                      string `json:"userAgent"`
+	RemoteIP                       string `json:"remoteIp"`
+	ServerIP                       string `json:"serverIp"`
+	Referer                        string `json:"referer"`
+	Latency                        string `json:"latency"`
+	CacheLookup                    bool   `json:"cacheLookup"`
+	CacheHit                       bool   `json:"cacheHit"`
+	CacheValidatedWithOriginServer bool   `json:"cacheValidatedWithOriginServer"`
+	CacheFillBytes                 int64  `json:"cacheFillBytes,string"`
+	Protocol                       string `json:"protocol"`
+}
+
+func (l logMapper) parseHTTPRequest(httpRequestAttr pcommon.Value) (*logging.HTTPRequest, error) {
+	var bytes []byte
+	switch httpRequestAttr.Type() {
+	case pcommon.ValueTypeBytes:
+		bytes = httpRequestAttr.BytesVal()
+	case pcommon.ValueTypeString, pcommon.ValueTypeMap:
+		bytes = []byte(httpRequestAttr.AsString())
+	}
+
+	// TODO: Investigate doing this without the JSON unmarshal. Getting the attribute as a map
+	// instead of a slice of bytes could do, but would need a lot of type casting and checking
+	// assertions with it.
+	var parsedHTTPRequest httpRequestLog
+	if err := json.Unmarshal(bytes, &parsedHTTPRequest); err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(parsedHTTPRequest.RequestMethod, parsedHTTPRequest.RequestURL, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Referer", parsedHTTPRequest.Referer)
+	req.Header.Set("User-Agent", parsedHTTPRequest.UserAgent)
+
+	httpRequest := &logging.HTTPRequest{
+		Request:                        req,
+		RequestSize:                    parsedHTTPRequest.RequestSize,
+		Status:                         parsedHTTPRequest.Status,
+		ResponseSize:                   parsedHTTPRequest.ResponseSize,
+		LocalIP:                        parsedHTTPRequest.ServerIP,
+		RemoteIP:                       parsedHTTPRequest.RemoteIP,
+		CacheHit:                       parsedHTTPRequest.CacheHit,
+		CacheValidatedWithOriginServer: parsedHTTPRequest.CacheValidatedWithOriginServer,
+		CacheFillBytes:                 parsedHTTPRequest.CacheFillBytes,
+		CacheLookup:                    parsedHTTPRequest.CacheLookup,
+	}
+	if parsedHTTPRequest.Latency != "" {
+		latency, err := time.ParseDuration(parsedHTTPRequest.Latency)
+		if err == nil {
+			httpRequest.Latency = latency
+		}
+	}
+
+	return httpRequest, nil
+}


Test patch:
diff --git a/exporter/collector/integrationtest/cmd/recordfixtures/main.go b/exporter/collector/integrationtest/cmd/recordfixtures/main.go
index 43f4a6f..babc056 100644
--- a/exporter/collector/integrationtest/cmd/recordfixtures/main.go
+++ b/exporter/collector/integrationtest/cmd/recordfixtures/main.go
@@ -45,11 +45,45 @@ func (t *FakeTesting) Helper()      {}
 func (t *FakeTesting) Name() string { return "record fixtures" }
 
 func main() {
-	t := &FakeTesting{}
 	ctx := context.Background()
 	endTime := time.Now()
 	startTime := endTime.Add(-time.Second)
+	t := &FakeTesting{}
+
+	recordLogs(ctx, t, endTime)
+	recordMetrics(ctx, t, startTime, endTime)
+
+}
+
+func recordLogs(ctx context.Context, t *FakeTesting, timestamp time.Time) {
+	testServer, err := integrationtest.NewLoggingTestServer()
+	if err != nil {
+		panic(err)
+	}
+	go testServer.Serve()
+	defer testServer.Shutdown()
+
+	for _, test := range integrationtest.LogsTestCases {
+		if test.Skip {
+			continue
+		}
+		func() {
+			logs := test.LoadOTLPLogsInput(t, timestamp)
+			testServerExporter := testServer.NewExporter(ctx, t, test.CreateLogConfig())
+
+			require.NoError(t, testServerExporter.PushLogs(ctx, logs), "failed to export logs to local test server")
+			require.NoError(t, testServerExporter.Shutdown(ctx))
+
+			require.NoError(t, err)
+			fixture := &integrationtest.LogExpectFixture{
+				WriteLogEntriesRequests: testServer.CreateWriteLogEntriesRequests(),
+			}
+			test.SaveRecordedLogFixtures(t, fixture)
+		}()
+	}
+}
 
+func recordMetrics(ctx context.Context, t *FakeTesting, startTime, endTime time.Time) {
 	testServer, err := integrationtest.NewMetricTestServer()
 	if err != nil {
 		panic(err)
@@ -57,13 +91,13 @@ func main() {
 	go testServer.Serve()
 	defer testServer.Shutdown()
 
-	for _, test := range integrationtest.TestCases {
+	for _, test := range integrationtest.MetricsTestCases {
 		if test.Skip {
 			continue
 		}
 		func() {
 			metrics := test.LoadOTLPMetricsInput(t, startTime, endTime)
-			testServerExporter := testServer.NewExporter(ctx, t, test.CreateConfig())
+			testServerExporter := testServer.NewExporter(ctx, t, test.CreateMetricConfig())
 			inMemoryOCExporter, err := integrationtest.NewInMemoryOCViewExporter()
 			require.NoError(t, err)
 			defer inMemoryOCExporter.Shutdown(ctx)
@@ -79,7 +113,7 @@ func main() {
 				CreateServiceTimeSeriesRequests: testServer.CreateServiceTimeSeriesRequests(),
 				SelfObservabilityMetrics:        selfObsMetrics,
 			}
-			test.SaveRecordedFixtures(t, fixture)
+			test.SaveRecordedMetricFixtures(t, fixture)
 		}()
 	}
 }
diff --git a/exporter/collector/integrationtest/diff.go b/exporter/collector/integrationtest/diff.go
index d1ec723..7b140ff 100644
--- a/exporter/collector/integrationtest/diff.go
+++ b/exporter/collector/integrationtest/diff.go
@@ -31,7 +31,7 @@ var (
 )
 
 // Diff uses cmp.Diff(), protocmp, and some custom options to compare two protobuf messages.
-func DiffProtos(t testing.TB, x, y *MetricExpectFixture) string {
+func DiffMetricProtos(t testing.TB, x, y *MetricExpectFixture) string {
 	x = proto.Clone(x).(*MetricExpectFixture)
 	y = proto.Clone(y).(*MetricExpectFixture)
 	normalizeFixture(t, x)
@@ -39,3 +39,11 @@ func DiffProtos(t testing.TB, x, y *MetricExpectFixture) string {
 
 	return cmp.Diff(x, y, cmpOptions...)
 }
+
+// Diff uses cmp.Diff(), protocmp, and some custom options to compare two protobuf messages.
+func DiffLogProtos(t testing.TB, x, y *LogExpectFixture) string {
+	x = proto.Clone(x).(*LogExpectFixture)
+	y = proto.Clone(y).(*LogExpectFixture)
+
+	return cmp.Diff(x, y, cmpOptions...)
+}
diff --git a/exporter/collector/integrationtest/fixtures.pb.go b/exporter/collector/integrationtest/fixtures.pb.go
index d94604c..dbf9ddc 100644
--- a/exporter/collector/integrationtest/fixtures.pb.go
+++ b/exporter/collector/integrationtest/fixtures.pb.go
@@ -21,6 +21,7 @@
 package integrationtest
 
 import (
+	v2 "google.golang.org/genproto/googleapis/logging/v2"
 	v3 "google.golang.org/genproto/googleapis/monitoring/v3"
 	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -161,66 +162,123 @@ func (x *SelfObservabilityMetric) GetCreateMetricDescriptorRequests() []*v3.Crea
 	return nil
 }
 
+type LogExpectFixture struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	WriteLogEntriesRequests []*v2.WriteLogEntriesRequest `protobuf:"bytes,1,rep,name=write_log_entries_requests,json=writeLogEntriesRequests,proto3" json:"write_log_entries_requests,omitempty"`
+}
+
+func (x *LogExpectFixture) Reset() {
+	*x = LogExpectFixture{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_fixtures_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *LogExpectFixture) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogExpectFixture) ProtoMessage() {}
+
+func (x *LogExpectFixture) ProtoReflect() protoreflect.Message {
+	mi := &file_fixtures_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use LogExpectFixture.ProtoReflect.Descriptor instead.
+func (*LogExpectFixture) Descriptor() ([]byte, []int) {
+	return file_fixtures_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *LogExpectFixture) GetWriteLogEntriesRequests() []*v2.WriteLogEntriesRequest {
+	if x != nil {
+		return x.WriteLogEntriesRequests
+	}
+	return nil
+}
+
 var File_fixtures_proto protoreflect.FileDescriptor
 
 var file_fixtures_proto_rawDesc = []byte{
 	0x0a, 0x0e, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
 	0x12, 0x08, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x73, 0x1a, 0x14, 0x6d, 0x65, 0x74, 0x72,
 	0x69, 0x63, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x22, 0xe1, 0x03, 0x0a, 0x13, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x45, 0x78, 0x70, 0x65, 0x63,
-	0x74, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x6c, 0x0a, 0x1b, 0x63, 0x72, 0x65, 0x61,
-	0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x65, 0x73, 0x5f, 0x72,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e,
-	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e,
-	0x67, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53,
-	0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x18, 0x63, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x7e, 0x0a, 0x21, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
-	0x6f, 0x72, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
-	0x0b, 0x32, 0x33, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x6e, 0x69, 0x74,
-	0x6f, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d,
-	0x65, 0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x1e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65,
-	0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x7b, 0x0a, 0x23, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x65,
-	0x72, 0x69, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x03, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x6e,
-	0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
-	0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x52, 0x1f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
-	0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x1a, 0x73, 0x65, 0x6c, 0x66, 0x5f, 0x6f, 0x62, 0x73, 0x65,
-	0x72, 0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63,
-	0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72,
-	0x65, 0x73, 0x2e, 0x53, 0x65, 0x6c, 0x66, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x62, 0x69,
-	0x6c, 0x69, 0x74, 0x79, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x52, 0x18, 0x73, 0x65, 0x6c, 0x66,
-	0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x4d, 0x65, 0x74,
-	0x72, 0x69, 0x63, 0x73, 0x22, 0x87, 0x02, 0x0a, 0x17, 0x53, 0x65, 0x6c, 0x66, 0x4f, 0x62, 0x73,
-	0x65, 0x72, 0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63,
-	0x12, 0x6c, 0x0a, 0x1b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f,
-	0x73, 0x65, 0x72, 0x69, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18,
-	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6d,
-	0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x72, 0x65,
+	0x1a, 0x15, 0x6c, 0x6f, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe1, 0x03, 0x0a, 0x13, 0x4d, 0x65, 0x74, 0x72,
+	0x69, 0x63, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12,
+	0x6c, 0x0a, 0x1b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73,
+	0x65, 0x72, 0x69, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6d, 0x6f,
+	0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x72, 0x65, 0x61,
+	0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x52, 0x18, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53,
+	0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x7e, 0x0a,
+	0x21, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x64,
+	0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x65, 0x2e, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x33, 0x2e,
+	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63,
+	0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x1e, 0x63,
+	0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x72,
+	0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x7b, 0x0a,
+	0x23, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f,
+	0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76,
+	0x33, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x69,
+	0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x1f, 0x63, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x69,
+	0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x1a, 0x73, 0x65,
+	0x6c, 0x66, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
+	0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21,
+	0x2e, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x73, 0x2e, 0x53, 0x65, 0x6c, 0x66, 0x4f, 0x62,
+	0x73, 0x65, 0x72, 0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x4d, 0x65, 0x74, 0x72, 0x69,
+	0x63, 0x52, 0x18, 0x73, 0x65, 0x6c, 0x66, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x62, 0x69,
+	0x6c, 0x69, 0x74, 0x79, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x22, 0x87, 0x02, 0x0a, 0x17,
+	0x53, 0x65, 0x6c, 0x66, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74,
+	0x79, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x6c, 0x0a, 0x1b, 0x63, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x65, 0x73, 0x5f, 0x72, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67,
+	0x2e, 0x76, 0x33, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65,
+	0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x18, 0x63, 0x72, 0x65,
 	0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x52, 0x18, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65,
-	0x53, 0x65, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x7e,
-	0x0a, 0x21, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f,
-	0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
-	0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x33,
-	0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x73,
-	0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x1e,
-	0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63,
-	0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x42, 0x68,
-	0x5a, 0x66, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x47, 0x6f, 0x6f,
-	0x67, 0x6c, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d,
-	0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2d, 0x6f,
-	0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2d, 0x67, 0x6f, 0x2f, 0x65, 0x78, 0x70,
-	0x6f, 0x72, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f,
-	0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61,
-	0x74, 0x69, 0x6f, 0x6e, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x7e, 0x0a, 0x21, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f,
+	0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f,
+	0x72, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x33, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f,
+	0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65,
+	0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x1e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74,
+	0x72, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x73, 0x22, 0x7a, 0x0a, 0x10, 0x4c, 0x6f, 0x67, 0x45, 0x78, 0x70, 0x65,
+	0x63, 0x74, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x66, 0x0a, 0x1a, 0x77, 0x72, 0x69,
+	0x74, 0x65, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x5f, 0x72,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x6c, 0x6f, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x2e, 0x76,
+	0x32, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65,
+	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x17, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4c,
+	0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x73, 0x42, 0x68, 0x5a, 0x66, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
+	0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x50, 0x6c, 0x61, 0x74, 0x66,
+	0x6f, 0x72, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72,
+	0x79, 0x2d, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2d, 0x67, 0x6f, 0x2f,
+	0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74,
+	0x6f, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65,
+	0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
@@ -235,25 +293,28 @@ func file_fixtures_proto_rawDescGZIP() []byte {
 	return file_fixtures_proto_rawDescData
 }
 
-var file_fixtures_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_fixtures_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
 var file_fixtures_proto_goTypes = []interface{}{
 	(*MetricExpectFixture)(nil),              // 0: fixtures.MetricExpectFixture
 	(*SelfObservabilityMetric)(nil),          // 1: fixtures.SelfObservabilityMetric
-	(*v3.CreateTimeSeriesRequest)(nil),       // 2: google.monitoring.v3.CreateTimeSeriesRequest
-	(*v3.CreateMetricDescriptorRequest)(nil), // 3: google.monitoring.v3.CreateMetricDescriptorRequest
+	(*LogExpectFixture)(nil),                 // 2: fixtures.LogExpectFixture
+	(*v3.CreateTimeSeriesRequest)(nil),       // 3: google.monitoring.v3.CreateTimeSeriesRequest
+	(*v3.CreateMetricDescriptorRequest)(nil), // 4: google.monitoring.v3.CreateMetricDescriptorRequest
+	(*v2.WriteLogEntriesRequest)(nil),        // 5: google.logging.v2.WriteLogEntriesRequest
 }
 var file_fixtures_proto_depIdxs = []int32{
-	2, // 0: fixtures.MetricExpectFixture.create_time_series_requests:type_name -> google.monitoring.v3.CreateTimeSeriesRequest
-	3, // 1: fixtures.MetricExpectFixture.create_metric_descriptor_requests:type_name -> google.monitoring.v3.CreateMetricDescriptorRequest
-	2, // 2: fixtures.MetricExpectFixture.create_service_time_series_requests:type_name -> google.monitoring.v3.CreateTimeSeriesRequest
+	3, // 0: fixtures.MetricExpectFixture.create_time_series_requests:type_name -> google.monitoring.v3.CreateTimeSeriesRequest
+	4, // 1: fixtures.MetricExpectFixture.create_metric_descriptor_requests:type_name -> google.monitoring.v3.CreateMetricDescriptorRequest
+	3, // 2: fixtures.MetricExpectFixture.create_service_time_series_requests:type_name -> google.monitoring.v3.CreateTimeSeriesRequest
 	1, // 3: fixtures.MetricExpectFixture.self_observability_metrics:type_name -> fixtures.SelfObservabilityMetric
-	2, // 4: fixtures.SelfObservabilityMetric.create_time_series_requests:type_name -> google.monitoring.v3.CreateTimeSeriesRequest
-	3, // 5: fixtures.SelfObservabilityMetric.create_metric_descriptor_requests:type_name -> google.monitoring.v3.CreateMetricDescriptorRequest
-	6, // [6:6] is the sub-list for method output_type
-	6, // [6:6] is the sub-list for method input_type
-	6, // [6:6] is the sub-list for extension type_name
-	6, // [6:6] is the sub-list for extension extendee
-	0, // [0:6] is the sub-list for field type_name
+	3, // 4: fixtures.SelfObservabilityMetric.create_time_series_requests:type_name -> google.monitoring.v3.CreateTimeSeriesRequest
+	4, // 5: fixtures.SelfObservabilityMetric.create_metric_descriptor_requests:type_name -> google.monitoring.v3.CreateMetricDescriptorRequest
+	5, // 6: fixtures.LogExpectFixture.write_log_entries_requests:type_name -> google.logging.v2.WriteLogEntriesRequest
+	7, // [7:7] is the sub-list for method output_type
+	7, // [7:7] is the sub-list for method input_type
+	7, // [7:7] is the sub-list for extension type_name
+	7, // [7:7] is the sub-list for extension extendee
+	0, // [0:7] is the sub-list for field type_name
 }
 
 func init() { file_fixtures_proto_init() }
@@ -286,6 +347,18 @@ func file_fixtures_proto_init() {
 				return nil
 			}
 		}
+		file_fixtures_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*LogExpectFixture); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
@@ -293,7 +366,7 @@ func file_fixtures_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_fixtures_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   2,
+			NumMessages:   3,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
diff --git a/exporter/collector/integrationtest/fixtures.proto b/exporter/collector/integrationtest/fixtures.proto
index 9575fb5..74e6c0e 100644
--- a/exporter/collector/integrationtest/fixtures.proto
+++ b/exporter/collector/integrationtest/fixtures.proto
@@ -17,6 +17,7 @@ package fixtures;
 option go_package = "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/collector/internal/integrationtest";
 
 import "metric_service.proto";
+import "logging_service.proto";
 
 message MetricExpectFixture {
   repeated google.monitoring.v3.CreateTimeSeriesRequest create_time_series_requests = 1;
@@ -29,3 +30,7 @@ message SelfObservabilityMetric {
   repeated google.monitoring.v3.CreateTimeSeriesRequest create_time_series_requests = 1;
   repeated google.monitoring.v3.CreateMetricDescriptorRequest create_metric_descriptor_requests = 2;
 }
+
+message LogExpectFixture {
+  repeated google.logging.v2.WriteLogEntriesRequest write_log_entries_requests = 1;
+}
diff --git a/exporter/collector/integrationtest/go.mod b/exporter/collector/integrationtest/go.mod
index 5834f29..275f554 100644
--- a/exporter/collector/integrationtest/go.mod
+++ b/exporter/collector/integrationtest/go.mod
@@ -19,7 +19,9 @@ require (
 )
 
 require (
+	cloud.google.com/go v0.100.2 // indirect
 	cloud.google.com/go/compute v1.5.0 // indirect
+	cloud.google.com/go/logging v1.4.2 // indirect
 	cloud.google.com/go/monitoring v1.4.0 // indirect
 	cloud.google.com/go/trace v1.2.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.4.0 // indirect
diff --git a/exporter/collector/integrationtest/go.sum b/exporter/collector/integrationtest/go.sum
index 4c90f05..9c45d5c 100644
--- a/exporter/collector/integrationtest/go.sum
+++ b/exporter/collector/integrationtest/go.sum
@@ -40,6 +40,8 @@ cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiL
 cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/logging v1.4.2 h1:Mu2Q75VBDQlW1HlBMjTX4X84UFR73G1TiLlRYc/b7tA=
+cloud.google.com/go/logging v1.4.2/go.mod h1:jco9QZSx8HiVVqLJReq7z7bVdj0P1Jb9PDFs63T+axo=
 cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4=
 cloud.google.com/go/monitoring v1.4.0 h1:05+IuNMbh40hbxcqQ4SnynbwZbLG1Wc9dysIJxnfv7U=
 cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4=
@@ -433,6 +435,7 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
@@ -496,6 +499,7 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -610,6 +614,7 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
 google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
 google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I=
 google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
 google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
 google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
@@ -676,7 +681,9 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
diff --git a/exporter/collector/integrationtest/logging_service.proto b/exporter/collector/integrationtest/logging_service.proto
new file mode 100644
index 0000000..391e930
--- /dev/null
+++ b/exporter/collector/integrationtest/logging_service.proto
@@ -0,0 +1,23 @@
+// 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
+//
+//      https://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.
+
+// This is a stub of
+// https://github.com/googleapis/googleapis/blob/master/google/logging/v2/logging.proto
+// to avoid taking full dependency on googleapis repo
+
+syntax = "proto3";
+package google.logging.v2;
+option go_package = "google.golang.org/genproto/googleapis/logging/v2;logging";
+
+message WriteLogEntriesRequest{}
diff --git a/exporter/collector/integrationtest/logging_test_server.go b/exporter/collector/integrationtest/logging_test_server.go
new file mode 100644
index 0000000..4c8e1d6
--- /dev/null
+++ b/exporter/collector/integrationtest/logging_test_server.go
@@ -0,0 +1,117 @@
+// 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
+//
+//      https://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 integrationtest
+
+import (
+	"context"
+	"net"
+	"sync"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"go.uber.org/zap"
+	logpb "google.golang.org/genproto/googleapis/logging/v2"
+	"google.golang.org/grpc"
+
+	"github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/collector"
+)
+
+type LogsTestServer struct {
+	// Address where the gRPC server is listening
+	Endpoint string
+
+	writeLogEntriesRequests []*logpb.WriteLogEntriesRequest
+
+	lis net.Listener
+	srv *grpc.Server
+	mu  sync.Mutex
+}
+
+func (l *LogsTestServer) Shutdown() {
+	l.srv.GracefulStop()
+}
+
+func (l *LogsTestServer) Serve() {
+	l.srv.Serve(l.lis)
+}
+
+func (l *LogsTestServer) CreateWriteLogEntriesRequests() []*logpb.WriteLogEntriesRequest {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	reqs := l.writeLogEntriesRequests
+	l.writeLogEntriesRequests = nil
+	return reqs
+}
+
+func (l *LogsTestServer) appendWriteLogEntriesRequest(req *logpb.WriteLogEntriesRequest) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	l.writeLogEntriesRequests = append(l.writeLogEntriesRequests, req)
+}
+
+type fakeLoggingServiceServer struct {
+	logpb.UnimplementedLoggingServiceV2Server
+	logsTestServer *LogsTestServer
+}
+
+func (f *fakeLoggingServiceServer) WriteLogEntries(
+	ctx context.Context,
+	request *logpb.WriteLogEntriesRequest,
+) (*logpb.WriteLogEntriesResponse, error) {
+	f.logsTestServer.appendWriteLogEntriesRequest(request)
+	return &logpb.WriteLogEntriesResponse{}, nil
+}
+
+func NewLoggingTestServer() (*LogsTestServer, error) {
+	srv := grpc.NewServer()
+	lis, err := net.Listen("tcp", "localhost:0")
+	if err != nil {
+		return nil, err
+	}
+	testServer := &LogsTestServer{
+		Endpoint: lis.Addr().String(),
+		lis:      lis,
+		srv:      srv,
+	}
+	logpb.RegisterLoggingServiceV2Server(
+		srv,
+		&fakeLoggingServiceServer{logsTestServer: testServer},
+	)
+
+	return testServer, nil
+}
+
+func (l *LogsTestServer) NewExporter(
+	ctx context.Context,
+	t testing.TB,
+	cfg collector.Config,
+) *collector.LogsExporter {
+
+	cfg.LogConfig.ClientConfig.Endpoint = l.Endpoint
+	cfg.LogConfig.ClientConfig.UseInsecure = true
+	cfg.ProjectID = "fakeprojectid"
+
+	logger, err := zap.NewDevelopment()
+	require.NoError(t, err)
+
+	exporter, err := collector.NewGoogleCloudLogsExporter(
+		ctx,
+		cfg,
+		logger,
+	)
+	require.NoError(t, err)
+	t.Logf("Collector LogsTestServer exporter started, pointing at %v", cfg.LogConfig.ClientConfig.Endpoint)
+	return exporter
+}
diff --git a/exporter/collector/integrationtest/logs_integration_test.go b/exporter/collector/integrationtest/logs_integration_test.go
new file mode 100644
index 0000000..6cd7dd1
--- /dev/null
+++ b/exporter/collector/integrationtest/logs_integration_test.go
@@ -0,0 +1,67 @@
+// 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
+//
+//      https://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 integrationtest
+// +build integrationtest
+
+package integrationtest
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	"go.uber.org/zap"
+
+	"github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/collector"
+)
+
+func createLogsExporter(
+	ctx context.Context,
+	t *testing.T,
+	test *TestCase,
+) *collector.LogsExporter {
+	logger, _ := zap.NewProduction()
+	exporter, err := collector.NewGoogleCloudLogsExporter(
+		ctx,
+		test.CreateLogConfig(),
+		logger,
+	)
+	require.NoError(t, err)
+	t.Log("Collector logs exporter started")
+	return exporter
+}
+
+func TestIntegrationLogs(t *testing.T) {
+	ctx := context.Background()
+	endTime := time.Now()
+	startTime := endTime.Add(-time.Second)
+
+	for _, test := range LogsTestCases {
+		test := test
+
+		t.Run(test.Name, func(t *testing.T) {
+			logs := test.LoadOTLPLogsInput(t, startTime)
+			exporter := createLogsExporter(ctx, t, &test)
+			defer func() { require.NoError(t, exporter.Shutdown(ctx)) }()
+
+			require.NoError(
+				t,
+				exporter.PushLogs(ctx, logs),
+				"Failed to export logs",
+			)
+		})
+	}
+}
diff --git a/exporter/collector/integrationtest/logs_test.go b/exporter/collector/integrationtest/logs_test.go
new file mode 100644
index 0000000..438029e
--- /dev/null
+++ b/exporter/collector/integrationtest/logs_test.go
@@ -0,0 +1,72 @@
+// 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
+//
+//      https://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 integrationtest
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestLogs(t *testing.T) {
+
+	ctx := context.Background()
+	timestamp := time.Now()
+
+	for _, test := range LogsTestCases {
+		test := test
+
+		t.Run(test.Name, func(t *testing.T) {
+			logs := test.LoadOTLPLogsInput(t, timestamp)
+
+			testServer, err := NewLoggingTestServer()
+			require.NoError(t, err)
+			go testServer.Serve()
+			defer testServer.Shutdown()
+
+			testServerExporter := testServer.NewExporter(ctx, t, test.CreateLogConfig())
+
+			require.NoError(
+				t,
+				testServerExporter.PushLogs(ctx, logs),
+				"Failed to export logs to local test server",
+			)
+
+			expectFixture := test.LoadLogExpectFixture(
+				t,
+				timestamp,
+			)
+
+			diff := DiffLogProtos(
+				t,
+				&LogExpectFixture{
+					WriteLogEntriesRequests: testServer.CreateWriteLogEntriesRequests(),
+				},
+				expectFixture,
+			)
+
+			if diff != "" {
+				require.Fail(
+					t,
+					"Expected requests fixture and actual GCM requests differ",
+					diff,
+				)
+			}
+		})
+	}
+
+}
diff --git a/exporter/collector/integrationtest/metrics_integration_test.go b/exporter/collector/integrationtest/metrics_integration_test.go
index 73b9dee..4156946 100644
--- a/exporter/collector/integrationtest/metrics_integration_test.go
+++ b/exporter/collector/integrationtest/metrics_integration_test.go
@@ -31,12 +31,12 @@ import (
 func createMetricsExporter(
 	ctx context.Context,
 	t *testing.T,
-	test *MetricsTestCase,
+	test *TestCase,
 ) *collector.MetricsExporter {
 	logger, _ := zap.NewProduction()
 	exporter, err := collector.NewGoogleCloudMetricsExporter(
 		ctx,
-		test.CreateConfig(),
+		test.CreateMetricConfig(),
 		logger,
 		"latest",
 		collector.DefaultTimeout,
@@ -51,7 +51,7 @@ func TestIntegrationMetrics(t *testing.T) {
 	endTime := time.Now()
 	startTime := endTime.Add(-time.Second)
 
-	for _, test := range TestCases {
+	for _, test := range MetricsTestCases {
 		test := test
 
 		t.Run(test.Name, func(t *testing.T) {
diff --git a/exporter/collector/integrationtest/metrics_test.go b/exporter/collector/integrationtest/metrics_test.go
index d062cc7..86c4409 100644
--- a/exporter/collector/integrationtest/metrics_test.go
+++ b/exporter/collector/integrationtest/metrics_test.go
@@ -27,7 +27,7 @@ func TestMetrics(t *testing.T) {
 	endTime := time.Now()
 	startTime := endTime.Add(-time.Second)
 
-	for _, test := range TestCases {
+	for _, test := range MetricsTestCases {
 		test := test
 
 		t.Run(test.Name, func(t *testing.T) {
@@ -38,7 +38,7 @@ func TestMetrics(t *testing.T) {
 			require.NoError(t, err)
 			go testServer.Serve()
 			defer testServer.Shutdown()
-			testServerExporter := testServer.NewExporter(ctx, t, test.CreateConfig())
+			testServerExporter := testServer.NewExporter(ctx, t, test.CreateMetricConfig())
 			// For collecting self observability metrics
 			inMemoryOCExporter, err := NewInMemoryOCViewExporter()
 			require.NoError(t, err)
@@ -51,7 +51,7 @@ func TestMetrics(t *testing.T) {
 			)
 			require.NoError(t, testServerExporter.Shutdown(ctx))
 
-			expectFixture := test.LoadExpectFixture(
+			expectFixture := test.LoadMetricExpectFixture(
 				t,
 				startTime,
 				endTime,
@@ -59,7 +59,7 @@ func TestMetrics(t *testing.T) {
 
 			selfObsMetrics, err := inMemoryOCExporter.Proto(ctx)
 			require.NoError(t, err)
-			diff := DiffProtos(
+			diff := DiffMetricProtos(
 				t,
 				&MetricExpectFixture{
 					CreateTimeSeriesRequests:        testServer.CreateTimeSeriesRequests(),
diff --git a/exporter/collector/integrationtest/testcase.go b/exporter/collector/integrationtest/testcase.go
index a050cc1..f009375 100644
--- a/exporter/collector/integrationtest/testcase.go
+++ b/exporter/collector/integrationtest/testcase.go
@@ -30,6 +30,8 @@ import (
 	"github.com/stretchr/testify/require"
 	"go.opentelemetry.io/collector/model/otlp"
 	"go.opentelemetry.io/collector/model/pdata"
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/plog"
 	"go.opentelemetry.io/collector/pdata/pmetric"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/types/known/timestamppb"
@@ -48,7 +50,7 @@ var (
 	}
 )
 
-type MetricsTestCase struct {
+type TestCase struct {
 	// Name of the test case
 	Name string
 
@@ -66,14 +68,82 @@ type MetricsTestCase struct {
 	Configure func(cfg *collector.Config)
 }
 
+func (tc *TestCase) LoadOTLPLogsInput(
+	t testing.TB,
+	timestamp time.Time,
+) plog.Logs {
+	bytes, err := ioutil.ReadFile(tc.OTLPInputFixturePath)
+	require.NoError(t, err)
+	logs, err := plog.NewJSONUnmarshaler().UnmarshalLogs(bytes)
+	require.NoError(t, err)
+
+	for i := 0; i < logs.ResourceLogs().Len(); i++ {
+		rl := logs.ResourceLogs().At(i)
+		for j := 0; j < rl.ScopeLogs().Len(); j++ {
+			sls := rl.ScopeLogs().At(j)
+			for k := 0; k < sls.LogRecords().Len(); k++ {
+				log := sls.LogRecords().At(k)
+				if log.Timestamp() != 0 {
+					log.SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
+				}
+			}
+		}
+	}
+	return logs
+}
+
+func (tc *TestCase) CreateLogConfig() collector.Config {
+	cfg := collector.DefaultConfig()
+	// If not set it will use ADC
+	cfg.ProjectID = os.Getenv("PROJECT_ID")
+
+	if tc.Configure != nil {
+		tc.Configure(&cfg)
+	}
+
+	return cfg
+}
+
+func (tc *TestCase) LoadLogExpectFixture(
+	t testing.TB,
+	timestamp time.Time,
+) *LogExpectFixture {
+	bytes, err := ioutil.ReadFile(tc.ExpectFixturePath)
+	require.NoError(t, err)
+	fixture := &LogExpectFixture{}
+	require.NoError(t, protojson.Unmarshal(bytes, fixture))
+
+	for _, request := range fixture.WriteLogEntriesRequests {
+		for _, entry := range request.Entries {
+			entry.Timestamp = timestamppb.New(timestamp)
+		}
+	}
+
+	return fixture
+}
+
+func (tc *TestCase) SaveRecordedLogFixtures(
+	t testing.TB,
+	fixture *LogExpectFixture,
+) {
+
+	jsonBytes, err := protojson.Marshal(fixture)
+	require.NoError(t, err)
+	formatted := bytes.Buffer{}
+	require.NoError(t, json.Indent(&formatted, jsonBytes, "", "  "))
+	formatted.WriteString("\n")
+	require.NoError(t, ioutil.WriteFile(tc.ExpectFixturePath, formatted.Bytes(), 0640))
+	t.Logf("Updated fixture %v", tc.ExpectFixturePath)
+}
+
 // Load OTLP metric fixture, test expectation fixtures and modify them so they're suitable for
 // testing. Currently, this just updates the timestamps.
-func (m *MetricsTestCase) LoadOTLPMetricsInput(
+func (tc *TestCase) LoadOTLPMetricsInput(
 	t testing.TB,
 	startTime time.Time,
 	endTime time.Time,
 ) pdata.Metrics {
-	bytes, err := ioutil.ReadFile(m.OTLPInputFixturePath)
+	bytes, err := ioutil.ReadFile(tc.OTLPInputFixturePath)
 	require.NoError(t, err)
 	metrics, err := otlp.NewJSONMetricsUnmarshaler().UnmarshalMetrics(bytes)
 	require.NoError(t, err)
@@ -130,21 +200,21 @@ func (m *MetricsTestCase) LoadOTLPMetricsInput(
 	return metrics
 }
 
-func (m *MetricsTestCase) LoadExpectFixture(
+func (tc *TestCase) LoadMetricExpectFixture(
 	t testing.TB,
 	startTime time.Time,
 	endTime time.Time,
 ) *MetricExpectFixture {
-	bytes, err := ioutil.ReadFile(m.ExpectFixturePath)
+	bytes, err := ioutil.ReadFile(tc.ExpectFixturePath)
 	require.NoError(t, err)
 	fixture := &MetricExpectFixture{}
 	require.NoError(t, protojson.Unmarshal(bytes, fixture))
-	m.updateExpectFixture(t, startTime, endTime, fixture)
+	tc.updateMetricExpectFixture(t, startTime, endTime, fixture)
 
 	return fixture
 }
 
-func (m *MetricsTestCase) updateExpectFixture(
+func (tc *TestCase) updateMetricExpectFixture(
 	t testing.TB,
 	startTime time.Time,
 	endTime time.Time,
@@ -169,7 +239,7 @@ func (m *MetricsTestCase) updateExpectFixture(
 	}
 }
 
-func (m *MetricsTestCase) SaveRecordedFixtures(
+func (tc *TestCase) SaveRecordedMetricFixtures(
 	t testing.TB,
 	fixture *MetricExpectFixture,
 ) {
@@ -180,8 +250,8 @@ func (m *MetricsTestCase) SaveRecordedFixtures(
 	formatted := bytes.Buffer{}
 	require.NoError(t, json.Indent(&formatted, jsonBytes, "", "  "))
 	formatted.WriteString("\n")
-	require.NoError(t, ioutil.WriteFile(m.ExpectFixturePath, formatted.Bytes(), 0640))
-	t.Logf("Updated fixture %v", m.ExpectFixturePath)
+	require.NoError(t, ioutil.WriteFile(tc.ExpectFixturePath, formatted.Bytes(), 0640))
+	t.Logf("Updated fixture %v", tc.ExpectFixturePath)
 }
 
 // Normalizes timestamps and removes project ID fields which create noise in the fixture
@@ -275,13 +345,13 @@ func normalizeSelfObs(t testing.TB, selfObs *SelfObservabilityMetric) {
 	})
 }
 
-func (m *MetricsTestCase) SkipIfNeeded(t testing.TB) {
-	if m.Skip {
+func (tc *TestCase) SkipIfNeeded(t testing.TB) {
+	if tc.Skip {
 		t.Skip("Test case is marked to skip in internal/integrationtest/testcases.go")
 	}
 }
 
-func (m *MetricsTestCase) CreateConfig() collector.Config {
+func (tc *TestCase) CreateMetricConfig() collector.Config {
 	cfg := collector.DefaultConfig()
 	// If not set it will use ADC
 	cfg.ProjectID = os.Getenv("PROJECT_ID")
@@ -289,8 +359,8 @@ func (m *MetricsTestCase) CreateConfig() collector.Config {
 	cfg.MetricConfig.CreateMetricDescriptorBufferSize = 500
 	cfg.MetricConfig.InstrumentationLibraryLabels = false
 
-	if m.Configure != nil {
-		m.Configure(&cfg)
+	if tc.Configure != nil {
+		tc.Configure(&cfg)
 	}
 
 	return cfg
diff --git a/exporter/collector/integrationtest/testcases.go b/exporter/collector/integrationtest/testcases.go
index 593bab0..4199409 100644
--- a/exporter/collector/integrationtest/testcases.go
+++ b/exporter/collector/integrationtest/testcases.go
@@ -17,7 +17,20 @@ package integrationtest
 import "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/collector"
 
 var (
-	TestCases = []MetricsTestCase{
+	LogsTestCases = []TestCase{
+		{
+			Name:                 "Apache access log with HTTPRequest",
+			OTLPInputFixturePath: "testdata/fixtures/logs_apache_access.json",
+			ExpectFixturePath:    "testdata/fixtures/logs_apache_access_expected.json",
+		},
+		{
+			Name:                 "Apache error log with severity",
+			OTLPInputFixturePath: "testdata/fixtures/logs_apache_error.json",
+			ExpectFixturePath:    "testdata/fixtures/logs_apache_error_expected.json",
+		},
+	}
+
+	MetricsTestCases = []TestCase{
 		{
 			Name:                 "Basic Counter",
 			OTLPInputFixturePath: "testdata/fixtures/basic_counter_metrics.json",
diff --git a/exporter/collector/integrationtest/testdata/fixtures/logs_apache_access.json b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_access.json
new file mode 100644
index 0000000..1dd192e
--- /dev/null
+++ b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_access.json
@@ -0,0 +1,1672 @@
+{
+  "resourceLogs": [
+    {
+      "resource": {
+        "attributes": [
+          {
+            "key": "cloud.platform",
+            "value": {
+              "stringValue": "gcp_compute_engine"
+            }
+          }
+        ]
+      },
+      "scopeLogs": [
+        {
+          "scope": {},
+          "logRecords": [
+            {
+              "timeUnixNano": "1650994483968433711",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:36 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:36 +0800"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968470678",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:37 +0800] \"GET /lamp.png HTTP/1.1\" 200 51164"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:37 +0800"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/lamp.png"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "51164"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968481764",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:37 +0800] \"GET /favicon.ico HTTP/1.1\" 200 3990"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "3990"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/favicon.ico"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:37 +0800"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968490059",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:51 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:51 +0800"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968496637",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:52 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:52 +0800"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968517647",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:53 +0800"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968525857",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:53 +0800"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968532546",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:53 +0800"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968539797",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:53 +0800"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968548970",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:54 +0800"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968557408",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:54 +0800"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968564828",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:54 +0800"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968571303",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:54 +0800"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968578924",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:54 +0800"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968584858",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "1247"
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:53:54 +0800"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968591035",
+              "body": {
+                "stringValue": "127.0.0.2 - - [26/Apr/2022:22:54:38 +0800] \"GET / HTTP/1.1\" 200 4429"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:54:38 +0800"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": "/"
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "200"
+                          }
+                        },
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": "HTTP/1.1"
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "GET"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "4429"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.2"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650994483968597344",
+              "body": {
+                "stringValue": "127.0.0.1 - - [26/Apr/2022:22:54:43 +0800] \"-\" 408 -"
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.httpRequest",
+                  "value": {
+                    "kvlistValue": {
+                      "values": [
+                        {
+                          "key": "userAgent",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "remoteIp",
+                          "value": {
+                            "stringValue": "127.0.0.1"
+                          }
+                        },
+                        {
+                          "key": "requestUrl",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "host",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "user",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "responseSize",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "requestMethod",
+                          "value": {
+                            "stringValue": "-"
+                          }
+                        },
+                        {
+                          "key": "protocol",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "status",
+                          "value": {
+                            "stringValue": "408"
+                          }
+                        },
+                        {
+                          "key": "referer",
+                          "value": {
+                            "stringValue": ""
+                          }
+                        },
+                        {
+                          "key": "time",
+                          "value": {
+                            "stringValue": "26/Apr/2022:22:54:43 +0800"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "my-log-name-foo"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/exporter/collector/integrationtest/testdata/fixtures/logs_apache_access_expected.json b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_access_expected.json
new file mode 100644
index 0000000..03652ab
--- /dev/null
+++ b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_access_expected.json
@@ -0,0 +1,341 @@
+{
+  "writeLogEntriesRequests": [
+    {
+      "entries": [
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:36 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:37 +0800] \"GET /lamp.png HTTP/1.1\" 200 51164",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/lamp.png",
+            "status": 200,
+            "responseSize": "51164",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:37 +0800] \"GET /favicon.ico HTTP/1.1\" 200 3990",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/favicon.ico",
+            "status": 200,
+            "responseSize": "3990",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:51 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:52 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:53 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:53:54 +0800] \"GET / HTTP/1.1\" 200 1247",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "1247",
+            "remoteIp": "127.0.0.1",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.2 - - [26/Apr/2022:22:54:38 +0800] \"GET / HTTP/1.1\" 200 4429",
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "httpRequest": {
+            "requestMethod": "GET",
+            "requestUrl": "/",
+            "status": 200,
+            "responseSize": "4429",
+            "remoteIp": "127.0.0.2",
+            "protocol": "HTTP/1.1"
+          }
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/my-log-name-foo",
+          "resource": {
+            "type": "gce_instance",
+            "labels": {
+              "instance_id": "",
+              "zone": ""
+            }
+          },
+          "textPayload": "127.0.0.1 - - [26/Apr/2022:22:54:43 +0800] \"-\" 408 -",
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        }
+      ],
+      "partialSuccess": true
+    }
+  ]
+}
diff --git a/exporter/collector/integrationtest/testdata/fixtures/logs_apache_error.json b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_error.json
new file mode 100644
index 0000000..ac02d4e
--- /dev/null
+++ b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_error.json
@@ -0,0 +1,405 @@
+{
+  "resourceLogs": [
+    {
+      "resource": {},
+      "scopeLogs": [
+        {
+          "scope": {},
+          "logRecords": [
+            {
+              "timeUnixNano": "1650933981412645000",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 00:46:21.412645 2022"
+                      }
+                    },
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "notice"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 417652:tid 139808755448704] AH01232: suEXEC mechanism enabled (wrapper: /usr/local/apache/bin/suexec)"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650933981457314000",
+              "severityNumber": "SEVERITY_NUMBER_WARN",
+              "severityText": "Warn",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "warn"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 417653:tid 139808755448704] AH01873: Init: Session Cache is not configured [hint: SSLSessionCache]"
+                      }
+                    },
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 00:46:21.457314 2022"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650933981502940000",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 00:46:21.502940 2022"
+                      }
+                    },
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "notice"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 417653:tid 139808755448704] AH00489: Apache/2.4.46 (Unix) OpenSSL/1.1.1g mod_wsgi/4.7.1 Python/3.6 configured -- resuming normal operations"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650933981503003000",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "notice"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 417653:tid 139808755448704] AH00094: Command line: '/usr/local/apache/bin/httpd'"
+                      }
+                    },
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 00:46:21.503003 2022"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1650933998988423000",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 417653:tid 139808755448704] AH00491: caught SIGTERM, shutting down"
+                      }
+                    },
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 00:46:38.988423 2022"
+                      }
+                    },
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "notice"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1651013314466058000",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 22:48:34.466058 2022"
+                      }
+                    },
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "notice"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 712:tid 140159306111872] AH01232: suEXEC mechanism enabled (wrapper: /usr/local/apache/bin/suexec)"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1651013314740290000",
+              "severityNumber": "SEVERITY_NUMBER_WARN",
+              "severityText": "Warn",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 22:48:34.740290 2022"
+                      }
+                    },
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "warn"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 769:tid 140159306111872] AH01873: Init: Session Cache is not configured [hint: SSLSessionCache]"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1651013315606223000",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "notice"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 769:tid 140159306111872] AH00489: Apache/2.4.46 (Unix) OpenSSL/1.1.1g mod_wsgi/4.7.1 Python/3.6 configured -- resuming normal operations"
+                      }
+                    },
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 22:48:35.606223 2022"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            },
+            {
+              "timeUnixNano": "1651013315606298000",
+              "body": {
+                "kvlistValue": {
+                  "values": [
+                    {
+                      "key": "severity",
+                      "value": {
+                        "stringValue": "notice"
+                      }
+                    },
+                    {
+                      "key": "message",
+                      "value": {
+                        "stringValue": "[pid 769:tid 140159306111872] AH00094: Command line: '/usr/local/apache/bin/httpd'"
+                      }
+                    },
+                    {
+                      "key": "time",
+                      "value": {
+                        "stringValue": "Tue Apr 26 22:48:35.606298 2022"
+                      }
+                    }
+                  ]
+                }
+              },
+              "attributes": [
+                {
+                  "key": "log.file.name",
+                  "value": {
+                    "stringValue": "test.log"
+                  }
+                },
+                {
+                  "key": "com.google.logName",
+                  "value": {
+                    "stringValue": "apache-error-fixture"
+                  }
+                }
+              ],
+              "traceId": "",
+              "spanId": ""
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/exporter/collector/integrationtest/testdata/fixtures/logs_apache_error_expected.json b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_error_expected.json
new file mode 100644
index 0000000..c987975
--- /dev/null
+++ b/exporter/collector/integrationtest/testdata/fixtures/logs_apache_error_expected.json
@@ -0,0 +1,164 @@
+{
+  "writeLogEntriesRequests": [
+    {
+      "entries": [
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 417652:tid 139808755448704] AH01232: suEXEC mechanism enabled (wrapper: /usr/local/apache/bin/suexec)",
+            "severity": "notice",
+            "time": "Tue Apr 26 00:46:21.412645 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 417653:tid 139808755448704] AH01873: Init: Session Cache is not configured [hint: SSLSessionCache]",
+            "severity": "warn",
+            "time": "Tue Apr 26 00:46:21.457314 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "severity": "WARNING"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 417653:tid 139808755448704] AH00489: Apache/2.4.46 (Unix) OpenSSL/1.1.1g mod_wsgi/4.7.1 Python/3.6 configured -- resuming normal operations",
+            "severity": "notice",
+            "time": "Tue Apr 26 00:46:21.502940 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 417653:tid 139808755448704] AH00094: Command line: '/usr/local/apache/bin/httpd'",
+            "severity": "notice",
+            "time": "Tue Apr 26 00:46:21.503003 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 417653:tid 139808755448704] AH00491: caught SIGTERM, shutting down",
+            "severity": "notice",
+            "time": "Tue Apr 26 00:46:38.988423 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 712:tid 140159306111872] AH01232: suEXEC mechanism enabled (wrapper: /usr/local/apache/bin/suexec)",
+            "severity": "notice",
+            "time": "Tue Apr 26 22:48:34.466058 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 769:tid 140159306111872] AH01873: Init: Session Cache is not configured [hint: SSLSessionCache]",
+            "severity": "warn",
+            "time": "Tue Apr 26 22:48:34.740290 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z",
+          "severity": "WARNING"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 769:tid 140159306111872] AH00489: Apache/2.4.46 (Unix) OpenSSL/1.1.1g mod_wsgi/4.7.1 Python/3.6 configured -- resuming normal operations",
+            "severity": "notice",
+            "time": "Tue Apr 26 22:48:35.606223 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        },
+        {
+          "logName": "projects/fakeprojectid/logs/apache-error-fixture",
+          "resource": {
+            "type": "generic_node",
+            "labels": {
+              "location": "global",
+              "namespace": "",
+              "node_id": ""
+            }
+          },
+          "jsonPayload": {
+            "message": "[pid 769:tid 140159306111872] AH00094: Command line: '/usr/local/apache/bin/httpd'",
+            "severity": "notice",
+            "time": "Tue Apr 26 22:48:35.606298 2022"
+          },
+          "timestamp": "2022-04-28T17:26:45.136110082Z"
+        }
+      ],
+      "partialSuccess": true
+    }
+  ]
+}
diff --git a/exporter/collector/logs_test.go b/exporter/collector/logs_test.go
new file mode 100644
index 0000000..700e893
--- /dev/null
+++ b/exporter/collector/logs_test.go
@@ -0,0 +1,293 @@
+// 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 collector
+
+import (
+	"io"
+	"net/http"
+	"testing"
+	"time"
+
+	"cloud.google.com/go/logging"
+	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/plog"
+	"go.uber.org/zap"
+	"google.golang.org/genproto/googleapis/api/monitoredres"
+	logpb "google.golang.org/genproto/googleapis/logging/v2"
+)
+
+func newTestLogMapper() logMapper {
+	obs := selfObservability{log: zap.NewNop()}
+	cfg := DefaultConfig()
+	cfg.LogConfig.DefaultLogName = "default-log"
+	return logMapper{
+		cfg: cfg,
+		obs: obs,
+	}
+}
+
+func TestLogMapping(t *testing.T) {
+	testObservedTime, _ := time.Parse("2006-01-02", "2022-04-12")
+	testSampleTime, _ := time.Parse("2006-01-02", "2021-07-10")
+	testTraceID := pcommon.NewTraceID([16]byte{
+		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+	})
+	testSpanID := pcommon.NewSpanID([8]byte{
+		0, 0, 0, 0, 0, 0, 0, 1,
+	})
+
+	testCases := []struct {
+		name          string
+		log           func() plog.LogRecord
+		mr            func() *monitoredres.MonitoredResource
+		expectError   bool
+		expectedEntry logging.Entry
+	}{
+		{
+			name: "empty log, empty monitoredresource",
+			log: func() plog.LogRecord {
+				return plog.NewLogRecord()
+			},
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			expectedEntry: logging.Entry{
+				Timestamp: testObservedTime,
+			},
+		},
+		{
+			name: "log with json, empty monitoredresource",
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.Body().SetBytesVal([]byte(`{"this": "is json"}`))
+				return log
+			},
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			expectedEntry: logging.Entry{
+				Payload:   []byte(`{"this": "is json"}`),
+				Timestamp: testObservedTime,
+			},
+		},
+		{
+			name: "log with json and httpRequest, empty monitoredresource",
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.Body().SetBytesVal([]byte(`{"message": "hello!"}`))
+				log.Attributes().Insert(HTTPRequestAttributeKey, pcommon.NewValueBytes([]byte(`{
+						"requestMethod": "GET", 
+						"requestURL": "https://www.example.com", 
+						"requestSize": "1",
+						"status": "200",
+						"responseSize": "1",
+						"userAgent": "test",
+						"remoteIP": "192.168.0.1",
+						"serverIP": "192.168.0.2",
+						"referer": "https://www.example2.com",
+						"cacheHit": false,
+						"cacheValidatedWithOriginServer": false,
+						"cacheFillBytes": "1",
+						"protocol": "HTTP/2"
+					}`)))
+				return log
+			},
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			expectedEntry: logging.Entry{
+				Payload:   []byte(`{"message": "hello!"}`),
+				Timestamp: testObservedTime,
+				HTTPRequest: &logging.HTTPRequest{
+					Request:                        makeExpectedHTTPReq("GET", "https://www.example.com", "https://www.example2.com", "test", nil),
+					RequestSize:                    1,
+					Status:                         200,
+					ResponseSize:                   1,
+					LocalIP:                        "192.168.0.2",
+					RemoteIP:                       "192.168.0.1",
+					CacheHit:                       false,
+					CacheValidatedWithOriginServer: false,
+					CacheFillBytes:                 1,
+					CacheLookup:                    false,
+				},
+			},
+		},
+		{
+			name: "log with timestamp",
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.SetTimestamp(pcommon.NewTimestampFromTime(testSampleTime))
+				return log
+			},
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			expectedEntry: logging.Entry{
+				Timestamp: testSampleTime,
+			},
+		},
+		{
+			name: "log body with string value",
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.Body().SetStringVal("{\"message\": \"hello!\"}")
+				return log
+			},
+			expectedEntry: logging.Entry{
+				Payload:   `{"message": "hello!"}`,
+				Timestamp: testObservedTime,
+			},
+		},
+		{
+			// TODO(damemi): parse/test sourceLocation from more than just bytes values
+			name: "log with sourceLocation (bytes)",
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.Attributes().Insert(
+					"com.google.sourceLocation",
+					pcommon.NewValueBytes([]byte(`{"file": "test.php", "line":100, "function":"helloWorld"}`)),
+				)
+				return log
+			},
+			expectedEntry: logging.Entry{
+				SourceLocation: &logpb.LogEntrySourceLocation{
+					File:     "test.php",
+					Line:     100,
+					Function: "helloWorld",
+				},
+				Timestamp: testObservedTime,
+			},
+		},
+		{
+			name: "log with trace and span id",
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.SetTraceID(testTraceID)
+				log.SetSpanID(testSpanID)
+				return log
+			},
+			expectedEntry: logging.Entry{
+				Trace:     testTraceID.HexString(),
+				SpanID:    testSpanID.HexString(),
+				Timestamp: testObservedTime,
+			},
+		},
+		{
+			name: "log with severity number",
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.SetSeverityNumber(plog.SeverityNumberFATAL)
+				return log
+			},
+			expectedEntry: logging.Entry{
+				Timestamp: testObservedTime,
+				Severity:  logging.Critical,
+			},
+		},
+		{
+			name: "log with invalid severity number",
+			mr: func() *monitoredres.MonitoredResource {
+				return nil
+			},
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.SetSeverityNumber(plog.SeverityNumber(100))
+				return log
+			},
+			expectError: true,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			log := testCase.log()
+			mr := testCase.mr()
+			mapper := newTestLogMapper()
+			entry, err := mapper.logToEntry(
+				log,
+				mr,
+				"",
+				"",
+				testObservedTime)
+			if testCase.expectError {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.Equal(t, testCase.expectedEntry, entry)
+			}
+		})
+	}
+}
+
+func TestGetLogName(t *testing.T) {
+	testCases := []struct {
+		name         string
+		log          func() plog.LogRecord
+		expectError  bool
+		expectedName string
+	}{
+		{
+			name: "log with name attribute",
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				log.Attributes().Insert("com.google.logName", pcommon.NewValueString("foo-log"))
+				return log
+			},
+			expectedName: "foo-log",
+		},
+		{
+			name: "log with no name attribute",
+			log: func() plog.LogRecord {
+				log := plog.NewLogRecord()
+				return log
+			},
+			expectedName: "default-log",
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			log := testCase.log()
+			mapper := newTestLogMapper()
+			name, err := mapper.getLogName(log)
+			if testCase.expectError {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.Equal(t, testCase.expectedName, name)
+			}
+		})
+	}
+}
+
+func makeExpectedHTTPReq(method, url, referer, userAgent string, body io.Reader) *http.Request {
+	req, _ := http.NewRequest(method, url, body)
+	req.Header.Set("Referer", referer)
+	req.Header.Set("User-Agent", userAgent)
+	return req
+}