Repository: open-telemetry/opentelemetry-go (go)

Issue:
log: Add Record.Clone
This is of type `go.opentelemetry.io/otel/log.Record` not `go.opentelemetry.io/otel/sdk/log.Record`

_Originally posted by @Mojachieee in https://github.com/open-telemetry/opentelemetry-go/pull/6968#discussion_r2194485774_

Implement `Record.Clone` in the same way as

https://github.com/open-telemetry/opentelemetry-go/blob/afd476363f2bc3b2f9ff8168a16400959c7f07a0/sdk/log/record.go#L402-L408
            

Pull request:
 log: Add Record.Clone
Fixes #6986

This PR adds a `Clone()` method to the `log.Record` type.

The `Clone` method returns a copy of the record with no shared state, allowing both the original and the clone to be modified independently. 

This functionality mirrors the existing `sdk/log.Record.Clone()` behavior and includes a corresponding unit test (`TestRecordClone`) to ensure correctness.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 64d91096..80054532 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
   - `RPCGRPCResponseMetadata`
 - Add `ErrorType` attribute helper function to the `go.opentelmetry.io/otel/semconv/v1.34.0` package. (#6962)
 - Add `WithAllowKeyDuplication` in `go.opentelemetry.io/otel/sdk/log` which can be used to disable deduplication for log records. (#6968)
+- Add `Clone` method to `Record` in `go.opentelemetry.io/otel/log` that returns a copy of the record with no shared state. (#7001)
 
 ### Changed
 
diff --git a/log/record.go b/log/record.go
index 4d2f32d0..adde7a0d 100644
--- a/log/record.go
+++ b/log/record.go
@@ -142,3 +142,11 @@ func (r *Record) AddAttributes(attrs ...KeyValue) {
 func (r *Record) AttributesLen() int {
 	return r.nFront + len(r.back)
 }
+
+// Clone returns a copy of the record with no shared state.
+// The original record and the clone can both be modified without interfering with each other.
+func (r *Record) Clone() Record {
+	res := *r
+	res.back = slices.Clone(r.back)
+	return res
+}


Test patch:
diff --git a/log/record_test.go b/log/record_test.go
index b3f145fe..7e44bbfc 100644
--- a/log/record_test.go
+++ b/log/record_test.go
@@ -160,3 +160,64 @@ func TestRecordAllocationLimits(t *testing.T) {
 	// Convince the linter these values are used.
 	_, _, _, _, _, _ = tStamp, sev, text, body, n, attr
 }
+
+func TestRecordClone(t *testing.T) {
+	now0 := time.Now()
+	sev0 := log.SeverityInfo
+	text0 := "text"
+	val0 := log.BoolValue(true)
+	attr0 := log.Bool("0", true)
+
+	r0 := log.Record{}
+	r0.SetTimestamp(now0)
+	r0.SetObservedTimestamp(now0)
+	r0.SetSeverity(sev0)
+	r0.SetSeverityText(text0)
+	r0.SetBody(val0)
+	r0.AddAttributes(attr0)
+
+	// Clone and modify the clone
+	now1 := now0.Add(time.Second)
+	sev1 := log.SeverityDebug
+	text1 := "string"
+	val1 := log.IntValue(1)
+	attr1 := log.Int64("1", 2)
+
+	r1 := r0.Clone()
+	r1.SetTimestamp(now1)
+	r1.SetObservedTimestamp(now1)
+	r1.SetSeverity(sev1)
+	r1.SetSeverityText(text1)
+	r1.SetBody(val1)
+	r1.AddAttributes(attr1)
+
+	//  Assertions on original record (r0)
+	assert.Equal(t, now0, r0.Timestamp())
+	assert.Equal(t, now0, r0.ObservedTimestamp())
+	assert.Equal(t, sev0, r0.Severity())
+	assert.Equal(t, text0, r0.SeverityText())
+	assert.True(t, val0.Equal(r0.Body()))
+
+	var r0Attrs []log.KeyValue
+	r0.WalkAttributes(func(kv log.KeyValue) bool {
+		r0Attrs = append(r0Attrs, kv)
+		return true
+	})
+	assert.Contains(t, r0Attrs, attr0)
+	assert.NotContains(t, r0Attrs, attr1)
+
+	//  Assertions on cloned record (r1)
+	assert.Equal(t, now1, r1.Timestamp())
+	assert.Equal(t, now1, r1.ObservedTimestamp())
+	assert.Equal(t, sev1, r1.Severity())
+	assert.Equal(t, text1, r1.SeverityText())
+	assert.True(t, val1.Equal(r1.Body()))
+
+	var r1Attrs []log.KeyValue
+	r1.WalkAttributes(func(kv log.KeyValue) bool {
+		r1Attrs = append(r1Attrs, kv)
+		return true
+	})
+	assert.Contains(t, r1Attrs, attr0)
+	assert.Contains(t, r1Attrs, attr1)
+}