feat(logging): add new jsonlog package for logging to stderr (#4170)

This new package is meant to be used in environments like Cloud
Run, Cloud Function, GKE, etc. In these environments the Cloud
Logging agent can parse the output of this package and transform
it to the same format used by the Cloud Logging API. Fields like
the monitored resource are auto-detected by the agent and do not
need to be filled out by log producer.

For better code reuse and to not expose any additional unneeded
APIs I refactored some common code into internal.
diff --git a/logging/go.mod b/logging/go.mod
index 98e3438..180c0a8 100644
--- a/logging/go.mod
+++ b/logging/go.mod
@@ -13,4 +13,5 @@
 	google.golang.org/api v0.48.0
 	google.golang.org/genproto v0.0.0-20210607140030-00d4fb20b1ae
 	google.golang.org/grpc v1.38.0
+	google.golang.org/protobuf v1.26.0
 )
diff --git a/logging/internal/common.go b/logging/internal/common.go
index c5788fe..0f2bc98 100644
--- a/logging/internal/common.go
+++ b/logging/internal/common.go
@@ -15,13 +15,22 @@
 package internal
 
 import (
+	"bytes"
+	"encoding/json"
 	"fmt"
+	"regexp"
 	"strings"
+	"unicode/utf8"
+
+	"google.golang.org/protobuf/types/known/structpb"
 )
 
 const (
 	// ProdAddr is the production address.
 	ProdAddr = "logging.googleapis.com:443"
+
+	// TraceHeader is the HTTP header trace information is stored in.
+	TraceHeader = "X-Cloud-Trace-Context"
 )
 
 // LogPath creates a formatted path from a parent and a logID.
@@ -39,3 +48,116 @@
 	logID := path[start:]
 	return strings.Replace(logID, "%2F", "/", -1)
 }
+
+var reCloudTraceContext = regexp.MustCompile(
+	// Matches on "TRACE_ID"
+	`([a-f\d]+)?` +
+		// Matches on "/SPAN_ID"
+		`(?:/([a-f\d]+))?` +
+		// Matches on ";0=TRACE_TRUE"
+		`(?:;o=(\d))?`)
+
+// DeconstructXCloudTraceContext extracts trace information from the trace
+// header.
+//
+// As per the format described at https://cloud.google.com/trace/docs/setup#force-trace
+//    "X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=TRACE_TRUE"
+// for example:
+//    "X-Cloud-Trace-Context: 105445aa7843bc8bf206b120001000/1;o=1"
+//
+// We expect:
+//   * traceID (optional): 			"105445aa7843bc8bf206b120001000"
+//   * spanID (optional):       	"1"
+//   * traceSampled (optional): 	true
+func DeconstructXCloudTraceContext(s string) (traceID, spanID string, traceSampled bool) {
+	matches := reCloudTraceContext.FindStringSubmatch(s)
+
+	traceID, spanID, traceSampled = matches[1], matches[2], matches[3] == "1"
+
+	if spanID == "0" {
+		spanID = ""
+	}
+
+	return
+}
+
+// FixUTF8 is a helper that fixes an invalid UTF-8 string by replacing
+// invalid UTF-8 runes with the Unicode replacement character (U+FFFD).
+// See Issue https://github.com/googleapis/google-cloud-go/issues/1383.
+func FixUTF8(s string) string {
+	if utf8.ValidString(s) {
+		return s
+	}
+
+	// Otherwise time to build the sequence.
+	buf := new(bytes.Buffer)
+	buf.Grow(len(s))
+	for _, r := range s {
+		if utf8.ValidRune(r) {
+			buf.WriteRune(r)
+		} else {
+			buf.WriteRune('\uFFFD')
+		}
+	}
+	return buf.String()
+}
+
+// ToProtoStruct converts v, which must marshal into a JSON object,
+// into a Google Struct proto.
+func ToProtoStruct(v interface{}) (*structpb.Struct, error) {
+	// Fast path: if v is already a *structpb.Struct, nothing to do.
+	if s, ok := v.(*structpb.Struct); ok {
+		return s, nil
+	}
+	// v is a Go value that supports JSON marshalling. We want a Struct
+	// protobuf. Some day we may have a more direct way to get there, but right
+	// now the only way is to marshal the Go value to JSON, unmarshal into a
+	// map, and then build the Struct proto from the map.
+	var jb []byte
+	var err error
+	if raw, ok := v.(json.RawMessage); ok { // needed for Go 1.7 and below
+		jb = []byte(raw)
+	} else {
+		jb, err = json.Marshal(v)
+		if err != nil {
+			return nil, fmt.Errorf("logging: json.Marshal: %v", err)
+		}
+	}
+	var m map[string]interface{}
+	err = json.Unmarshal(jb, &m)
+	if err != nil {
+		return nil, fmt.Errorf("logging: json.Unmarshal: %v", err)
+	}
+	return jsonMapToProtoStruct(m), nil
+}
+
+func jsonMapToProtoStruct(m map[string]interface{}) *structpb.Struct {
+	fields := map[string]*structpb.Value{}
+	for k, v := range m {
+		fields[k] = jsonValueToStructValue(v)
+	}
+	return &structpb.Struct{Fields: fields}
+}
+
+func jsonValueToStructValue(v interface{}) *structpb.Value {
+	switch x := v.(type) {
+	case bool:
+		return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: x}}
+	case float64:
+		return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: x}}
+	case string:
+		return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: x}}
+	case nil:
+		return &structpb.Value{Kind: &structpb.Value_NullValue{}}
+	case map[string]interface{}:
+		return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: jsonMapToProtoStruct(x)}}
+	case []interface{}:
+		var vals []*structpb.Value
+		for _, e := range x {
+			vals = append(vals, jsonValueToStructValue(e))
+		}
+		return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: vals}}}
+	default:
+		return &structpb.Value{Kind: &structpb.Value_NullValue{}}
+	}
+}
diff --git a/logging/internal/common_test.go b/logging/internal/common_test.go
new file mode 100644
index 0000000..fbcc265
--- /dev/null
+++ b/logging/internal/common_test.go
@@ -0,0 +1,81 @@
+// Copyright 2021 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 internal
+
+import (
+	"testing"
+
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/structpb"
+)
+
+func TestToProtoStruct(t *testing.T) {
+	v := struct {
+		Foo string                 `json:"foo"`
+		Bar int                    `json:"bar,omitempty"`
+		Baz []float64              `json:"baz"`
+		Moo map[string]interface{} `json:"moo"`
+	}{
+		Foo: "foovalue",
+		Baz: []float64{1.1},
+		Moo: map[string]interface{}{
+			"a": 1,
+			"b": "two",
+			"c": true,
+		},
+	}
+
+	got, err := ToProtoStruct(v)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := &structpb.Struct{
+		Fields: map[string]*structpb.Value{
+			"foo": {Kind: &structpb.Value_StringValue{StringValue: v.Foo}},
+			"baz": {Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{
+				{Kind: &structpb.Value_NumberValue{NumberValue: 1.1}},
+			}}}},
+			"moo": {Kind: &structpb.Value_StructValue{
+				StructValue: &structpb.Struct{
+					Fields: map[string]*structpb.Value{
+						"a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}},
+						"b": {Kind: &structpb.Value_StringValue{StringValue: "two"}},
+						"c": {Kind: &structpb.Value_BoolValue{BoolValue: true}},
+					},
+				},
+			}},
+		},
+	}
+	if !proto.Equal(got, want) {
+		t.Errorf("got  %+v\nwant %+v", got, want)
+	}
+
+	// Non-structs should fail to convert.
+	for v := range []interface{}{3, "foo", []int{1, 2, 3}} {
+		_, err := ToProtoStruct(v)
+		if err == nil {
+			t.Errorf("%v: got nil, want error", v)
+		}
+	}
+
+	// Test fast path.
+	got, err = ToProtoStruct(want)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got != want {
+		t.Error("got and want should be identical, but are not")
+	}
+}
diff --git a/logging/jsonlog/example_test.go b/logging/jsonlog/example_test.go
new file mode 100644
index 0000000..0832def
--- /dev/null
+++ b/logging/jsonlog/example_test.go
@@ -0,0 +1,67 @@
+// Copyright 2021 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 jsonlog_test
+
+import (
+	"io"
+	"net/http"
+	"os"
+
+	"cloud.google.com/go/logging/jsonlog"
+)
+
+func ExampleNewLogger() {
+	l, err := jsonlog.NewLogger("projects/PROJECT_ID")
+	if err != nil {
+		// TODO: handle error.
+	}
+	l.Infof("Hello World!")
+}
+
+func ExampLogger_WithRequest() {
+	var req *http.Request
+	l, err := jsonlog.NewLogger("projects/PROJECT_ID")
+	if err != nil {
+		// TODO: handle error.
+	}
+	// Create a Logger with additional information pulled from the current
+	// request context.
+	l = l.WithRequest(req)
+	l.Infof("Hello World!")
+}
+
+func ExampleLogger_WithLabels() {
+	l, err := jsonlog.NewLogger("projects/PROJECT_ID")
+	if err != nil {
+		// TODO: handle error.
+	}
+	l.Infof("Hello World!")
+
+	// Create a logger that always provides additional context by adding labels
+	// to all logged messages.
+	l2 := l.WithLabels(map[string]string{"foo": "bar"})
+	l2.Infof("Hello World, with more context!")
+}
+
+func ExampleWithWriter_multiwriter() {
+	// Create a new writer that also logs messages to a second location
+	w := io.MultiWriter(os.Stderr, os.Stdout)
+	l, err := jsonlog.NewLogger("projects/PROJECT_ID", jsonlog.WithWriter(w))
+	if err != nil {
+		// TODO: handle error.
+	}
+	l.Infof("Hello World!")
+	// Output: {"message":"Hello World!","severity":"INFO"}
+}
diff --git a/logging/jsonlog/jsonlog.go b/logging/jsonlog/jsonlog.go
new file mode 100644
index 0000000..2904c2b
--- /dev/null
+++ b/logging/jsonlog/jsonlog.go
@@ -0,0 +1,346 @@
+// Copyright 2021 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 jsonlog provides a Logger that logs structured JSON to Stderr by
+// default. When used on the various Cloud Compute environments (Cloud Run,
+// Cloud Functions, GKE, etc.) these JSON messages will be parsed by the Cloud
+// Logging agent and transformed into a message format that mirrors that of the
+// Cloud Logging API.
+package jsonlog
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"strings"
+	"time"
+
+	"cloud.google.com/go/logging"
+	"cloud.google.com/go/logging/internal"
+	logtypepb "google.golang.org/genproto/googleapis/logging/type"
+	logpb "google.golang.org/genproto/googleapis/logging/v2"
+	"google.golang.org/protobuf/encoding/protojson"
+)
+
+const (
+	debugSeverity     = "DEBUG"
+	infoSeverity      = "INFO"
+	noticeSeverity    = "NOTICE"
+	warnSeverity      = "WARNING"
+	errorSeverity     = "ERROR"
+	criticalSeverity  = "CRITICAL"
+	alertSeverity     = "ALERT"
+	emergencySeverity = "EMERGENCY"
+)
+
+// NewLogger creates a Logger that logs structured JSON to Stderr. The value of
+// parent must be in the format of:
+//    projects/PROJECT_ID
+//    folders/FOLDER_ID
+//    billingAccounts/ACCOUNT_ID
+//    organizations/ORG_ID
+func NewLogger(parent string, opts ...LoggerOption) (*Logger, error) {
+	if err := validateParent(parent); err != nil {
+		return nil, err
+	}
+	l := &Logger{
+		w:      os.Stderr,
+		parent: parent,
+	}
+	for _, opt := range opts {
+		opt.set(l)
+	}
+	return l, nil
+}
+
+// Logger is used for logging JSON entries.
+type Logger struct {
+	w       io.Writer
+	now     func() time.Time
+	errhook func(error)
+	parent  string
+
+	// read-only fields
+	labels  map[string]string
+	req     *logtypepb.HttpRequest
+	traceID string
+	sampled bool
+	spanID  string
+}
+
+// copy does a shallow copy of the logger. Individual fields should not be
+// modified but replaced.
+func (l *Logger) copy() *Logger {
+	return &Logger{
+		w:       l.w,
+		now:     l.now,
+		errhook: l.errhook,
+		parent:  l.parent,
+		labels:  l.labels,
+		req:     l.req,
+		traceID: l.traceID,
+		sampled: l.sampled,
+		spanID:  l.spanID,
+	}
+}
+
+// Debugf is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Debugf(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: debugSeverity,
+	}
+	l.log(e)
+}
+
+// Infof is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Infof(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: infoSeverity,
+	}
+	l.log(e)
+}
+
+// Noticef is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Noticef(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: noticeSeverity,
+	}
+	l.log(e)
+}
+
+// Warnf is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Warnf(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: warnSeverity,
+	}
+	l.log(e)
+}
+
+// Errorf is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Errorf(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: errorSeverity,
+	}
+	l.log(e)
+}
+
+// Criticalf is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Criticalf(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: criticalSeverity,
+	}
+	l.log(e)
+}
+
+// Alertf is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Alertf(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: alertSeverity,
+	}
+	l.log(e)
+}
+
+// Emergencyf is a convenience method for writing an Entry with a Debug Severity
+// and the provided formatted message.
+func (l *Logger) Emergencyf(format string, a ...interface{}) {
+	e := entry{
+		Message:  fmt.Sprintf(format, a...),
+		Severity: emergencySeverity,
+	}
+	l.log(e)
+}
+
+// Log an Entry. Note that not all of the fields in entry will used when
+// writting the log message, only those that are mentioned
+// https://cloud.google.com/logging/docs/structured-logging will be logged.
+func (l *Logger) Log(e logging.Entry) {
+	le := entry{
+		Severity:       e.Severity.String(),
+		Labels:         e.Labels,
+		InsertID:       e.InsertID,
+		Operation:      e.Operation,
+		SourceLocation: e.SourceLocation,
+		SpanID:         e.SpanID,
+		Trace:          e.Trace,
+		TraceSampled:   e.TraceSampled,
+	}
+	if e.HTTPRequest != nil {
+		le.HTTPRequest = toLogpbHTTPRequest(e.HTTPRequest.Request)
+	}
+	if !e.Timestamp.IsZero() {
+		le.Timestamp = e.Timestamp.Format(time.RFC3339)
+	}
+	switch p := e.Payload.(type) {
+	case string:
+		le.Message = p
+	default:
+		s, err := internal.ToProtoStruct(p)
+		if err != nil {
+			if l.errhook != nil {
+				l.errhook(err)
+			}
+			return
+		}
+		b, err := protojson.Marshal(s)
+		if err != nil {
+			if l.errhook != nil {
+				l.errhook(err)
+			}
+			return
+		}
+		le.Message = string(b)
+	}
+	l.log(le)
+}
+
+func (l *Logger) log(e entry) {
+	if e.Timestamp == "" && l.now != nil {
+		e.Timestamp = l.now().Format(time.RFC3339)
+	}
+	if e.Trace == "" {
+		e.Trace = l.traceID
+	}
+	if e.SpanID == "" {
+		e.SpanID = l.spanID
+	}
+	if !e.TraceSampled {
+		e.TraceSampled = l.sampled
+	}
+	if e.HTTPRequest == nil && l.req != nil {
+		e.HTTPRequest = l.req
+	}
+	if l.labels != nil {
+		if e.Labels == nil {
+			e.Labels = l.labels
+		} else {
+			for k, v := range l.labels {
+				if _, ok := e.Labels[k]; !ok {
+					e.Labels[k] = v
+				}
+			}
+		}
+	}
+	if err := json.NewEncoder(l.w).Encode(e); err != nil && l.errhook != nil {
+		l.errhook(err)
+	}
+}
+
+// WithLabels creates a new JSONLogger based off an existing one. The labels
+// provided will be added to the loggers existing labels, replacing any
+// overlapping keys with the new values.
+func (l *Logger) WithLabels(labels map[string]string) *Logger {
+	new := l.copy()
+	newLabels := make(map[string]string, len(new.labels))
+	for k, v := range new.labels {
+		newLabels[k] = v
+	}
+	for k, v := range labels {
+		newLabels[k] = v
+	}
+	new.labels = newLabels
+	return new
+}
+
+// WithRequest creates a new JSONLogger based off an existing one with request
+// information populated. By giving a Logger a request context all logs
+// will be auto-populated with some basic information about the request as well
+// as tracing details, if included.
+func (l *Logger) WithRequest(r *http.Request) *Logger {
+	new := l.copy()
+	var req *logtypepb.HttpRequest
+	if r != nil {
+		u := *r.URL
+		req = &logtypepb.HttpRequest{
+			RequestMethod: r.Method,
+			RequestUrl:    internal.FixUTF8(u.String()),
+			UserAgent:     r.UserAgent(),
+			Referer:       r.Referer(),
+			Protocol:      r.Proto,
+		}
+		if r.Response != nil {
+			req.Status = int32(r.Response.StatusCode)
+		}
+		new.req = req
+	}
+	var traceHeader string
+	if r != nil && r.Header != nil {
+		traceHeader = r.Header.Get(internal.TraceHeader)
+	}
+	if traceHeader != "" {
+		traceID, spanID, traceSampled := internal.DeconstructXCloudTraceContext(traceHeader)
+		new.traceID = fmt.Sprintf("%s/traces/%s", new.parent, traceID)
+		new.spanID = spanID
+		new.sampled = traceSampled
+	}
+	return new
+}
+
+// validateParent checks to make sure name is in the format.
+func validateParent(parent string) error {
+	if !strings.HasPrefix(parent, "projects/") &&
+		!strings.HasPrefix(parent, "folders/") &&
+		!strings.HasPrefix(parent, "billingAccounts/") &&
+		!strings.HasPrefix(parent, "organizations/") {
+		return fmt.Errorf("jsonlog: name formatting incorrect")
+	}
+	return nil
+}
+
+// entry represents the fields of a logging.Entry that can be parsed by Logging
+// agent. To see a list of these mappings see
+// https://cloud.google.com/logging/docs/structured-logging.
+type entry struct {
+	Message        string                        `json:"message"`
+	Severity       string                        `json:"severity,omitempty"`
+	HTTPRequest    *logtypepb.HttpRequest        `json:"httpRequest,omitempty"`
+	Timestamp      string                        `json:"timestamp,omitempty"`
+	Labels         map[string]string             `json:"logging.googleapis.com/labels,omitempty"`
+	InsertID       string                        `json:"logging.googleapis.com/insertId,omitempty"`
+	Operation      *logpb.LogEntryOperation      `json:"logging.googleapis.com/operation,omitempty"`
+	SourceLocation *logpb.LogEntrySourceLocation `json:"logging.googleapis.com/sourceLocation,omitempty"`
+	SpanID         string                        `json:"logging.googleapis.com/spanId,omitempty"`
+	Trace          string                        `json:"logging.googleapis.com/trace,omitempty"`
+	TraceSampled   bool                          `json:"logging.googleapis.com/trace_sampled,omitempty"`
+}
+
+func toLogpbHTTPRequest(r *http.Request) *logtypepb.HttpRequest {
+	if r == nil {
+		return nil
+	}
+	u := *r.URL
+	return &logtypepb.HttpRequest{
+		RequestMethod: r.Method,
+		RequestUrl:    internal.FixUTF8(u.String()),
+		Status:        int32(r.Response.StatusCode),
+		UserAgent:     r.UserAgent(),
+		Referer:       r.Referer(),
+		Protocol:      r.Proto,
+	}
+}
diff --git a/logging/jsonlog/jsonlog_test.go b/logging/jsonlog/jsonlog_test.go
new file mode 100644
index 0000000..b475fde
--- /dev/null
+++ b/logging/jsonlog/jsonlog_test.go
@@ -0,0 +1,312 @@
+// Copyright 2021 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 jsonlog
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"cloud.google.com/go/logging"
+	"cloud.google.com/go/logging/internal"
+	"github.com/google/go-cmp/cmp"
+	ltype "google.golang.org/genproto/googleapis/logging/type"
+)
+
+func TestLoggerLevel(t *testing.T) {
+	now := time.Date(1977, time.May, 25, 0, 0, 0, 0, time.UTC)
+	fnow := now.Format(time.RFC3339)
+	tests := []struct {
+		severity string
+		lFn      func(*Logger)
+	}{
+		{severity: debugSeverity, lFn: func(l *Logger) { l.Debugf("Hello, World!") }},
+		{severity: infoSeverity, lFn: func(l *Logger) { l.Infof("Hello, World!") }},
+		{severity: noticeSeverity, lFn: func(l *Logger) { l.Noticef("Hello, World!") }},
+		{severity: warnSeverity, lFn: func(l *Logger) { l.Warnf("Hello, World!") }},
+		{severity: errorSeverity, lFn: func(l *Logger) { l.Errorf("Hello, World!") }},
+		{severity: criticalSeverity, lFn: func(l *Logger) { l.Criticalf("Hello, World!") }},
+		{severity: alertSeverity, lFn: func(l *Logger) { l.Alertf("Hello, World!") }},
+		{severity: emergencySeverity, lFn: func(l *Logger) { l.Emergencyf("Hello, World!") }},
+	}
+	for _, tt := range tests {
+		t.Run(tt.severity, func(t *testing.T) {
+			buf := bytes.NewBuffer(nil)
+			l, err := NewLogger("projects/test")
+			if err != nil {
+				t.Fatalf("unable to create logger: %v", err)
+			}
+			l.w = buf
+			l.now = func() time.Time {
+				return now
+			}
+
+			e := &entry{
+				Message:   "Hello, World!",
+				Timestamp: fnow,
+			}
+			e.Severity = tt.severity
+			b, err := json.Marshal(e)
+			if err != nil {
+				t.Fatalf("unable to marshal: %v", err)
+			}
+			tt.lFn(l)
+			if diff := cmp.Diff(string(b), strings.TrimSpace(buf.String())); diff != "" {
+				t.Fatalf("Logger.Debugf() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+
+}
+
+func TestLoggerWithLabels(t *testing.T) {
+	l, err := NewLogger("projects/test/log123")
+	if err != nil {
+		t.Fatalf("unable to create logger: %v", err)
+	}
+
+	tests := []struct {
+		name           string
+		commonLabels   map[string]string
+		entryLabels    map[string]string
+		outputLabelLen int
+	}{
+		{
+			name:           "empty labels",
+			outputLabelLen: 0,
+		},
+		{
+			name:           "two common labels",
+			commonLabels:   map[string]string{"one": "foo", "two": "bar"},
+			outputLabelLen: 2,
+		},
+		{
+			name:           "two common labels and one entry",
+			commonLabels:   map[string]string{"one": "foo", "two": "bar"},
+			entryLabels:    map[string]string{"three": "baz"},
+			outputLabelLen: 3,
+		},
+		{
+			name:           "three unique labels with overlap",
+			commonLabels:   map[string]string{"one": "foo", "two": "bar", "three": "baz"},
+			entryLabels:    map[string]string{"three": "baz"},
+			outputLabelLen: 3,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			buf := bytes.NewBuffer(nil)
+			l.w = buf
+			e := entry{}
+
+			ll := l.WithLabels(tt.commonLabels)
+			ll.Log(logging.Entry{
+				Severity: logging.Info,
+				Payload:  "test",
+				Labels:   tt.entryLabels,
+			})
+			if err := json.Unmarshal(buf.Bytes(), &e); err != nil {
+				t.Fatalf("unable to unmarshal data: %v", err)
+			}
+			if len(e.Labels) != tt.outputLabelLen {
+				t.Fatalf("len(LogEntry.Labels) = %v, want %v", len(e.Labels), tt.outputLabelLen)
+			}
+		})
+	}
+}
+
+func TestLoggerWithLabels_WithOverlap(t *testing.T) {
+	l, err := NewLogger("projects/test")
+	if err != nil {
+		t.Fatalf("unable to create logger: %v", err)
+	}
+	buf := bytes.NewBuffer(nil)
+	l.w = buf
+	e := entry{}
+
+	l = l.WithLabels(map[string]string{"one": "foo", "two": "bar", "three": "baz"})
+	l.Log(logging.Entry{
+		Severity: logging.Info,
+		Payload:  "test",
+		Labels:   map[string]string{"three": "newbaz"},
+	})
+	if err := json.Unmarshal(buf.Bytes(), &e); err != nil {
+		t.Fatalf("unable to unmarshal data: %v", err)
+	}
+	if len(e.Labels) != 3 {
+		t.Fatalf("len(LogEntry.Labels) = %v, want %v", len(e.Labels), 3)
+	}
+	if e.Labels["three"] != "newbaz" {
+		t.Fatalf("le.Labels[\"three\"] = %q, want \"newbaz\"", e.Labels["three"])
+	}
+}
+
+func TestLoggerWithLabels_Twice(t *testing.T) {
+	l, err := NewLogger("projects/test/log123")
+	if err != nil {
+		t.Fatalf("unable to create logger: %v", err)
+	}
+	buf := bytes.NewBuffer(nil)
+	l.w = buf
+	e := entry{}
+
+	l = l.WithLabels(map[string]string{"one": "foo", "two": "bar"})
+	l = l.WithLabels(map[string]string{"one": "foo", "two": "newbar", "three": "baz"})
+	l.Log(logging.Entry{
+		Severity: logging.Info,
+		Payload:  "test",
+		Labels:   map[string]string{"three": "newbaz"},
+	})
+	if err := json.Unmarshal(buf.Bytes(), &e); err != nil {
+		t.Fatalf("unable to unmarshal data: %v", err)
+	}
+	if len(e.Labels) != 3 {
+		t.Fatalf("len(LogEntry.Labels) = %v, want %v", len(e.Labels), 3)
+	}
+	if e.Labels["three"] != "newbaz" {
+		t.Fatalf("le.Labels[\"three\"] = %q, want \"newbaz\"", e.Labels["three"])
+	}
+	if e.Labels["two"] != "newbar" {
+		t.Fatalf("le.Labels[\"two\"] = %q, want \"newbar\"", e.Labels["two"])
+	}
+}
+
+func TestParseName(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       string
+		wantParent  string
+		wantLogName string
+		wantErr     bool
+	}{
+		{
+			name:    "valid project style",
+			input:   "projects/test",
+			wantErr: false,
+		},
+		{
+			name:    "valid folder style",
+			input:   "folders/tes",
+			wantErr: false,
+		},
+		{
+			name:    "valid billing style",
+			input:   "billingAccounts/tes",
+			wantErr: false,
+		},
+		{
+			name:    "valid org style",
+			input:   "organizations/tes",
+			wantErr: false,
+		},
+		{
+			name:    "invalid parent",
+			input:   "blah/blah",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotErr := validateParent(tt.input)
+			if (tt.wantErr && gotErr == nil) || (!tt.wantErr && gotErr != nil) {
+				t.Errorf("err: got %v, want %v", gotErr, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestLogger_WithRequest(t *testing.T) {
+	now := time.Date(1977, time.May, 25, 0, 0, 0, 0, time.UTC)
+	fnow := now.Format(time.RFC3339)
+
+	t.Run("basic, without trace info", func(t *testing.T) {
+		r, err := http.NewRequest("Get", "http://example.com", nil)
+		if err != nil {
+			t.Fatalf("unable to create request: %v", err)
+		}
+		l, err := NewLogger("projects/test/log123")
+		if err != nil {
+			t.Fatalf("unable to create logger: %v", err)
+		}
+		l = l.WithRequest(r)
+		buf := bytes.NewBuffer(nil)
+		l.w = buf
+		l.now = func() time.Time {
+			return now
+		}
+		e := entry{
+			Message:   "Hello, World!",
+			Severity:  "INFO",
+			Timestamp: fnow,
+			HTTPRequest: &ltype.HttpRequest{
+				RequestMethod: "Get",
+				RequestUrl:    "http://example.com",
+				Protocol:      "HTTP/1.1",
+			},
+		}
+
+		l.Infof("Hello, World!")
+		b, err := json.Marshal(e)
+		if err != nil {
+			t.Fatalf("unable to marshal: %v", err)
+		}
+		if diff := cmp.Diff(string(b), strings.TrimSpace(buf.String())); diff != "" {
+			t.Fatalf("Logger.Warnf() mismatch (-want +got):\n%s", diff)
+		}
+	})
+
+	t.Run("with trace header", func(t *testing.T) {
+		r, err := http.NewRequest("Get", "http://example.com", nil)
+		if err != nil {
+			t.Fatalf("unable to create request: %v", err)
+		}
+		r.Header.Set(internal.TraceHeader, "105445aa7843bc8bf206b120001000/1;o=1")
+		l, err := NewLogger("projects/test")
+		if err != nil {
+			t.Fatalf("unable to create logger: %v", err)
+		}
+		l = l.WithRequest(r)
+		buf := bytes.NewBuffer(nil)
+		l.w = buf
+		l.now = func() time.Time {
+			return now
+		}
+		e := entry{
+			Message:   "Hello, World!",
+			Severity:  "INFO",
+			Timestamp: fnow,
+			HTTPRequest: &ltype.HttpRequest{
+				RequestMethod: "Get",
+				RequestUrl:    "http://example.com",
+				Protocol:      "HTTP/1.1",
+			},
+			Trace:        "projects/test/traces/105445aa7843bc8bf206b120001000",
+			SpanID:       "1",
+			TraceSampled: true,
+		}
+
+		l.Infof("Hello, World!")
+		b, err := json.Marshal(e)
+		if err != nil {
+			t.Fatalf("unable to marshal: %v", err)
+		}
+		if diff := cmp.Diff(string(b), strings.TrimSpace(buf.String())); diff != "" {
+			t.Fatalf("Logger.Warnf() mismatch (-want +got):\n%s", diff)
+		}
+	})
+}
diff --git a/logging/jsonlog/option.go b/logging/jsonlog/option.go
new file mode 100644
index 0000000..7d2c27d
--- /dev/null
+++ b/logging/jsonlog/option.go
@@ -0,0 +1,63 @@
+// Copyright 2021 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 jsonlog
+
+import (
+	"io"
+)
+
+// LoggerOption is a configuration option for a Logger.
+type LoggerOption interface {
+	set(*Logger)
+}
+
+// OnErrorHook registers a function that will be called anytime an error occurs
+// during logging.
+func OnErrorHook(hook func(error)) LoggerOption { return onErrorHook{hook: hook} }
+
+type onErrorHook struct{ hook func(error) }
+
+func (h onErrorHook) set(l *Logger) {
+	l.errhook = h.hook
+}
+
+// WithWriter changes where the JSON payloads of a Logger
+// are written to. By default they are written to Stderr.
+func WithWriter(w io.Writer) LoggerOption { return withWriter{w: w} }
+
+type withWriter struct {
+	w io.Writer
+}
+
+func (w withWriter) set(l *Logger) {
+	l.w = w.w
+}
+
+// CommonLabels are labels that apply to all log entries written from a Logger,
+// so that you don't have to repeat them in each log entry's Labels field. If
+// any of the log entries contains a (key, value) with the same key that is in
+// CommonLabels, then the entry's (key, value) overrides the one in
+// CommonLabels.
+func CommonLabels(m map[string]string) LoggerOption { return commonLabels(m) }
+
+type commonLabels map[string]string
+
+func (c commonLabels) set(l *Logger) {
+	labels := map[string]string{}
+	for k, v := range c {
+		labels[k] = v
+	}
+	l.labels = labels
+}
diff --git a/logging/jsonlog/option_test.go b/logging/jsonlog/option_test.go
new file mode 100644
index 0000000..0e4f13e
--- /dev/null
+++ b/logging/jsonlog/option_test.go
@@ -0,0 +1,72 @@
+// Copyright 2021 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 jsonlog
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+
+	"cloud.google.com/go/logging"
+)
+
+func TestOnErrorHook(t *testing.T) {
+	var i *int = new(int)
+	fn := func(error) {
+		*i++
+	}
+	l, err := NewLogger("projects/test", OnErrorHook(fn))
+	if err != nil {
+		t.Fatalf("unable to create logger: %v", err)
+	}
+	l.Log(logging.Entry{
+		Payload: false,
+	})
+	if *i != 1 {
+		t.Fatalf("got %d, want %d", *i, 1)
+	}
+}
+
+func TestWithWriter(t *testing.T) {
+	buf := bytes.NewBuffer(nil)
+	l, err := NewLogger("projects/test", WithWriter(buf))
+	if err != nil {
+		t.Fatalf("unable to create logger: %v", err)
+	}
+	if buf.Len() != 0 {
+		t.Fatalf("buf.Len() = %d, want 0", buf.Len())
+	}
+	l.Infof("Test")
+	if buf.Len() == 0 {
+		t.Fatalf("buf.Len() = %d, want >0", buf.Len())
+	}
+}
+
+func TestCommonLabels(t *testing.T) {
+	buf := bytes.NewBuffer(nil)
+	l, err := NewLogger("projects/test", CommonLabels(map[string]string{"foo": "bar"}))
+	if err != nil {
+		t.Fatalf("unable to create logger: %v", err)
+	}
+	l.w = buf
+	l.Infof("Test")
+	e := &entry{}
+	if err := json.Unmarshal(buf.Bytes(), e); err != nil {
+		t.Fatalf("unable to unmarshal: %v", err)
+	}
+	if e.Labels["foo"] != "bar" {
+		t.Fatalf("le.Labels[\"foo\"] = %q, want \"bar\"", e.Labels["foo"])
+	}
+}
diff --git a/logging/logging.go b/logging/logging.go
index 81ede6a..9865bca 100644
--- a/logging/logging.go
+++ b/logging/logging.go
@@ -25,31 +25,28 @@
 package logging
 
 import (
-	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"log"
 	"net/http"
-	"regexp"
 	"strconv"
 	"strings"
 	"sync"
 	"time"
-	"unicode/utf8"
 
 	"cloud.google.com/go/internal/version"
 	vkit "cloud.google.com/go/logging/apiv2"
 	"cloud.google.com/go/logging/internal"
-	"github.com/golang/protobuf/proto"
-	"github.com/golang/protobuf/ptypes"
-	structpb "github.com/golang/protobuf/ptypes/struct"
 	"google.golang.org/api/option"
 	"google.golang.org/api/support/bundler"
 	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
 	logtypepb "google.golang.org/genproto/googleapis/logging/type"
 	logpb "google.golang.org/genproto/googleapis/logging/v2"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/durationpb"
+	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
 const (
@@ -179,16 +176,13 @@
 // authentication configuration are valid. To accomplish this, Ping writes a
 // log entry "ping" to a log named "ping".
 func (c *Client) Ping(ctx context.Context) error {
-	unixZeroTimestamp, err := ptypes.TimestampProto(time.Unix(0, 0))
-	if err != nil {
-		return err
-	}
+	unixZeroTimestamp := timestamppb.New(time.Unix(0, 0))
 	ent := &logpb.LogEntry{
 		Payload:   &logpb.LogEntry_TextPayload{TextPayload: "ping"},
 		Timestamp: unixZeroTimestamp, // Identical timestamps and insert IDs are both
 		InsertId:  "ping",            // necessary for the service to dedup these entries.
 	}
-	_, err = c.client.WriteLogEntries(ctx, &logpb.WriteLogEntriesRequest{
+	_, err := c.client.WriteLogEntries(ctx, &logpb.WriteLogEntriesRequest{
 		LogName:  internal.LogPath(c.parent, "ping"),
 		Resource: monitoredResource(c.parent),
 		Entries:  []*logpb.LogEntry{ent},
@@ -600,7 +594,7 @@
 	u.Fragment = ""
 	pb := &logtypepb.HttpRequest{
 		RequestMethod:                  r.Request.Method,
-		RequestUrl:                     fixUTF8(u.String()),
+		RequestUrl:                     internal.FixUTF8(u.String()),
 		RequestSize:                    r.RequestSize,
 		Status:                         int32(r.Status),
 		ResponseSize:                   r.ResponseSize,
@@ -615,92 +609,11 @@
 		CacheLookup:                    r.CacheLookup,
 	}
 	if r.Latency != 0 {
-		pb.Latency = ptypes.DurationProto(r.Latency)
+		pb.Latency = durationpb.New(r.Latency)
 	}
 	return pb, nil
 }
 
-// fixUTF8 is a helper that fixes an invalid UTF-8 string by replacing
-// invalid UTF-8 runes with the Unicode replacement character (U+FFFD).
-// See Issue https://github.com/googleapis/google-cloud-go/issues/1383.
-func fixUTF8(s string) string {
-	if utf8.ValidString(s) {
-		return s
-	}
-
-	// Otherwise time to build the sequence.
-	buf := new(bytes.Buffer)
-	buf.Grow(len(s))
-	for _, r := range s {
-		if utf8.ValidRune(r) {
-			buf.WriteRune(r)
-		} else {
-			buf.WriteRune('\uFFFD')
-		}
-	}
-	return buf.String()
-}
-
-// toProtoStruct converts v, which must marshal into a JSON object,
-// into a Google Struct proto.
-func toProtoStruct(v interface{}) (*structpb.Struct, error) {
-	// Fast path: if v is already a *structpb.Struct, nothing to do.
-	if s, ok := v.(*structpb.Struct); ok {
-		return s, nil
-	}
-	// v is a Go value that supports JSON marshalling. We want a Struct
-	// protobuf. Some day we may have a more direct way to get there, but right
-	// now the only way is to marshal the Go value to JSON, unmarshal into a
-	// map, and then build the Struct proto from the map.
-	var jb []byte
-	var err error
-	if raw, ok := v.(json.RawMessage); ok { // needed for Go 1.7 and below
-		jb = []byte(raw)
-	} else {
-		jb, err = json.Marshal(v)
-		if err != nil {
-			return nil, fmt.Errorf("logging: json.Marshal: %v", err)
-		}
-	}
-	var m map[string]interface{}
-	err = json.Unmarshal(jb, &m)
-	if err != nil {
-		return nil, fmt.Errorf("logging: json.Unmarshal: %v", err)
-	}
-	return jsonMapToProtoStruct(m), nil
-}
-
-func jsonMapToProtoStruct(m map[string]interface{}) *structpb.Struct {
-	fields := map[string]*structpb.Value{}
-	for k, v := range m {
-		fields[k] = jsonValueToStructValue(v)
-	}
-	return &structpb.Struct{Fields: fields}
-}
-
-func jsonValueToStructValue(v interface{}) *structpb.Value {
-	switch x := v.(type) {
-	case bool:
-		return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: x}}
-	case float64:
-		return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: x}}
-	case string:
-		return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: x}}
-	case nil:
-		return &structpb.Value{Kind: &structpb.Value_NullValue{}}
-	case map[string]interface{}:
-		return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: jsonMapToProtoStruct(x)}}
-	case []interface{}:
-		var vals []*structpb.Value
-		for _, e := range x {
-			vals = append(vals, jsonValueToStructValue(e))
-		}
-		return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: vals}}}
-	default:
-		return &structpb.Value{Kind: &structpb.Value_NullValue{}}
-	}
-}
-
 // LogSync logs the Entry synchronously without any buffering. Because LogSync is slow
 // and will block, it is intended primarily for debugging or critical errors.
 // Prefer Log for most uses.
@@ -767,35 +680,6 @@
 // (for example by calling SetFlags or SetPrefix).
 func (l *Logger) StandardLogger(s Severity) *log.Logger { return l.stdLoggers[s] }
 
-var reCloudTraceContext = regexp.MustCompile(
-	// Matches on "TRACE_ID"
-	`([a-f\d]+)?` +
-		// Matches on "/SPAN_ID"
-		`(?:/([a-f\d]+))?` +
-		// Matches on ";0=TRACE_TRUE"
-		`(?:;o=(\d))?`)
-
-func deconstructXCloudTraceContext(s string) (traceID, spanID string, traceSampled bool) {
-	// As per the format described at https://cloud.google.com/trace/docs/setup#force-trace
-	//    "X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=TRACE_TRUE"
-	// for example:
-	//    "X-Cloud-Trace-Context: 105445aa7843bc8bf206b120001000/1;o=1"
-	//
-	// We expect:
-	//   * traceID (optional): 			"105445aa7843bc8bf206b120001000"
-	//   * spanID (optional):       	"1"
-	//   * traceSampled (optional): 	true
-	matches := reCloudTraceContext.FindStringSubmatch(s)
-
-	traceID, spanID, traceSampled = matches[1], matches[2], matches[3] == "1"
-
-	if spanID == "0" {
-		spanID = ""
-	}
-
-	return
-}
-
 // ToLogEntry takes an Entry structure and converts it to the LogEntry proto.
 // A parent can take any of the following forms:
 //    projects/PROJECT_ID
@@ -823,16 +707,13 @@
 	if t.IsZero() {
 		t = now()
 	}
-	ts, err := ptypes.TimestampProto(t)
-	if err != nil {
-		return nil, err
-	}
+	ts := timestamppb.New(t)
 	if e.Trace == "" && e.HTTPRequest != nil && e.HTTPRequest.Request != nil {
-		traceHeader := e.HTTPRequest.Request.Header.Get("X-Cloud-Trace-Context")
+		traceHeader := e.HTTPRequest.Request.Header.Get(internal.TraceHeader)
 		if traceHeader != "" {
 			// Set to a relative resource name, as described at
 			// https://cloud.google.com/appengine/docs/flexible/go/writing-application-logs.
-			traceID, spanID, traceSampled := deconstructXCloudTraceContext(traceHeader)
+			traceID, spanID, traceSampled := internal.DeconstructXCloudTraceContext(traceHeader)
 			if traceID != "" {
 				e.Trace = fmt.Sprintf("%s/traces/%s", parent, traceID)
 			}
@@ -871,7 +752,7 @@
 	case string:
 		ent.Payload = &logpb.LogEntry_TextPayload{TextPayload: p}
 	default:
-		s, err := toProtoStruct(p)
+		s, err := internal.ToProtoStruct(p)
 		if err != nil {
 			return nil, err
 		}
diff --git a/logging/logging_test.go b/logging/logging_test.go
index 495303e..a79b0c0 100644
--- a/logging/logging_test.go
+++ b/logging/logging_test.go
@@ -37,6 +37,7 @@
 	"cloud.google.com/go/internal/testutil"
 	"cloud.google.com/go/internal/uid"
 	"cloud.google.com/go/logging"
+	"cloud.google.com/go/logging/internal"
 	ltesting "cloud.google.com/go/logging/internal/testing"
 	"cloud.google.com/go/logging/logadmin"
 	gax "github.com/googleapis/gax-go/v2"
@@ -282,7 +283,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/000000000000004a;o=1"}},
+						Header: http.Header{internal.TraceHeader: {"105445aa7843bc8bf206b120001000/000000000000004a;o=1"}},
 					},
 				},
 			},
@@ -298,7 +299,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/000000000000004a;o=0"}},
+						Header: http.Header{internal.TraceHeader: {"105445aa7843bc8bf206b120001000/000000000000004a;o=0"}},
 					},
 				},
 			},
@@ -313,7 +314,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/000000000000004a;o=1"}},
+						Header: http.Header{internal.TraceHeader: {"105445aa7843bc8bf206b120001000/000000000000004a;o=1"}},
 					},
 				},
 			},
@@ -328,7 +329,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {"/0;o=1"}},
+						Header: http.Header{internal.TraceHeader: {"/0;o=1"}},
 					},
 				},
 			},
@@ -341,7 +342,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/;o=0"}},
+						Header: http.Header{internal.TraceHeader: {"105445aa7843bc8bf206b120001000/;o=0"}},
 					},
 				},
 			},
@@ -354,7 +355,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/0"}},
+						Header: http.Header{internal.TraceHeader: {"105445aa7843bc8bf206b120001000/0"}},
 					},
 				},
 			},
@@ -367,7 +368,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {""}},
+						Header: http.Header{internal.TraceHeader: {""}},
 					},
 				},
 			},
@@ -378,7 +379,7 @@
 				HTTPRequest: &logging.HTTPRequest{
 					Request: &http.Request{
 						URL:    u,
-						Header: http.Header{"X-Cloud-Trace-Context": {"t3"}},
+						Header: http.Header{internal.TraceHeader: {"t3"}},
 					},
 				},
 				Trace: "t4",
diff --git a/logging/logging_unexported_test.go b/logging/logging_unexported_test.go
index 6d1ac9c..f7b32b6 100644
--- a/logging/logging_unexported_test.go
+++ b/logging/logging_unexported_test.go
@@ -24,12 +24,12 @@
 	"time"
 
 	"cloud.google.com/go/internal/testutil"
-	"github.com/golang/protobuf/proto"
 	durpb "github.com/golang/protobuf/ptypes/duration"
 	structpb "github.com/golang/protobuf/ptypes/struct"
 	"google.golang.org/api/support/bundler"
 	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
 	logtypepb "google.golang.org/genproto/googleapis/logging/type"
+	"google.golang.org/protobuf/proto"
 )
 
 func TestLoggerCreation(t *testing.T) {
@@ -120,65 +120,6 @@
 	}
 }
 
-func TestToProtoStruct(t *testing.T) {
-	v := struct {
-		Foo string                 `json:"foo"`
-		Bar int                    `json:"bar,omitempty"`
-		Baz []float64              `json:"baz"`
-		Moo map[string]interface{} `json:"moo"`
-	}{
-		Foo: "foovalue",
-		Baz: []float64{1.1},
-		Moo: map[string]interface{}{
-			"a": 1,
-			"b": "two",
-			"c": true,
-		},
-	}
-
-	got, err := toProtoStruct(v)
-	if err != nil {
-		t.Fatal(err)
-	}
-	want := &structpb.Struct{
-		Fields: map[string]*structpb.Value{
-			"foo": {Kind: &structpb.Value_StringValue{StringValue: v.Foo}},
-			"baz": {Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{
-				{Kind: &structpb.Value_NumberValue{NumberValue: 1.1}},
-			}}}},
-			"moo": {Kind: &structpb.Value_StructValue{
-				StructValue: &structpb.Struct{
-					Fields: map[string]*structpb.Value{
-						"a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}},
-						"b": {Kind: &structpb.Value_StringValue{StringValue: "two"}},
-						"c": {Kind: &structpb.Value_BoolValue{BoolValue: true}},
-					},
-				},
-			}},
-		},
-	}
-	if !proto.Equal(got, want) {
-		t.Errorf("got  %+v\nwant %+v", got, want)
-	}
-
-	// Non-structs should fail to convert.
-	for v := range []interface{}{3, "foo", []int{1, 2, 3}} {
-		_, err := toProtoStruct(v)
-		if err == nil {
-			t.Errorf("%v: got nil, want error", v)
-		}
-	}
-
-	// Test fast path.
-	got, err = toProtoStruct(want)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if got != want {
-		t.Error("got and want should be identical, but are not")
-	}
-}
-
 func TestToLogEntryPayload(t *testing.T) {
 	for _, test := range []struct {
 		in         interface{}