Repository: jaegertracing/jaeger
Pull request #6946: [ES] Make `NestedTags` and `FieldTags` distinction at `CoreSpanWriter` level
Author: login: Manik2708
Labels: name: storage/elasticsearch
name: v2
name: changelog:refactoring
description: Internal code refactoring without functional changes
Created: 2025-03-30T13:58:03Z | Merged: 2025-04-27T17:32:53Z

## Which problem is this PR solving?
- Fixes a part of: #6458 

## Description of the changes
- `TagDotReplacement` which is responsible for nested and field tags distinction was earlier at writer level (of model and OTLP) which doesn't make sense as DB layer should manipulate whatever is optimal for the database, therfore now it is a part of `CoreSpanWriter`

## How was this change tested?
- Unit Tests

## Checklist
- [x] I have read https://github.com/jaegertracing/jaeger/blob/master/CONTRIBUTING_GUIDELINES.md
- [x] I have signed all commits
- [x] I have added unit tests for the new functionality
- [x] I have run lint and test steps successfully
  - for `jaeger`: `make lint test`
  - for `jaeger-ui`: `npm run lint` and `npm run test`



Diff:
diff --git a/internal/storage/v1/elasticsearch/spanstore/from_domain.go b/internal/storage/v1/elasticsearch/spanstore/from_domain.go
index d64c51deffc..8c71472b2d3 100644
--- a/internal/storage/v1/elasticsearch/spanstore/from_domain.go
+++ b/internal/storage/v1/elasticsearch/spanstore/from_domain.go
@@ -12,29 +12,24 @@ import (
 )
 
 // NewFromDomain creates FromDomain used to convert model span to db span
-func NewFromDomain(allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) FromDomain {
-	tags := map[string]bool{}
-	for _, k := range tagKeysAsFields {
-		tags[k] = true
-	}
-	return FromDomain{allTagsAsFields: allTagsAsObject, tagKeysAsFields: tags, tagDotReplacement: tagDotReplacement}
+func NewFromDomain() FromDomain {
+	return FromDomain{}
 }
 
 // FromDomain is used to convert model span to db span
-type FromDomain struct {
-	allTagsAsFields   bool
-	tagKeysAsFields   map[string]bool
-	tagDotReplacement string
-}
+type FromDomain struct{}
 
 // FromDomainEmbedProcess converts model.Span into json.Span format.
 // This format includes a ParentSpanID and an embedded Process.
 func (fd FromDomain) FromDomainEmbedProcess(span *model.Span) *dbmodel.Span {
-	return fd.convertSpanEmbedProcess(span)
+	s := fd.convertSpanInternal(span)
+	s.Process = fd.convertProcess(span.Process)
+	s.References = fd.convertReferences(span)
+	return &s
 }
 
 func (fd FromDomain) convertSpanInternal(span *model.Span) dbmodel.Span {
-	tags, tagsMap := fd.convertKeyValuesString(span.Tags)
+	tags := fd.convertKeyValues(span.Tags)
 	return dbmodel.Span{
 		TraceID:         dbmodel.TraceID(span.TraceID.String()),
 		SpanID:          dbmodel.SpanID(span.SpanID.String()),
@@ -44,18 +39,10 @@ func (fd FromDomain) convertSpanInternal(span *model.Span) dbmodel.Span {
 		StartTimeMillis: model.TimeAsEpochMicroseconds(span.StartTime) / 1000,
 		Duration:        model.DurationAsMicroseconds(span.Duration),
 		Tags:            tags,
-		Tag:             tagsMap,
 		Logs:            fd.convertLogs(span.Logs),
 	}
 }
 
-func (fd FromDomain) convertSpanEmbedProcess(span *model.Span) *dbmodel.Span {
-	s := fd.convertSpanInternal(span)
-	s.Process = fd.convertProcess(span.Process)
-	s.References = fd.convertReferences(span)
-	return &s
-}
-
 func (fd FromDomain) convertReferences(span *model.Span) []dbmodel.Reference {
 	out := make([]dbmodel.Reference, 0, len(span.References))
 	for _, ref := range span.References {
@@ -75,31 +62,23 @@ func (FromDomain) convertRefType(refType model.SpanRefType) dbmodel.ReferenceTyp
 	return dbmodel.ChildOf
 }
 
-func (fd FromDomain) convertKeyValuesString(keyValues model.KeyValues) ([]dbmodel.KeyValue, map[string]any) {
-	var tagsMap map[string]any
-	var kvs []dbmodel.KeyValue
-	for _, kv := range keyValues {
-		if kv.GetVType() != model.BinaryType && (fd.allTagsAsFields || fd.tagKeysAsFields[kv.Key]) {
-			if tagsMap == nil {
-				tagsMap = map[string]any{}
-			}
-			tagsMap[strings.ReplaceAll(kv.Key, ".", fd.tagDotReplacement)] = kv.Value()
-		} else {
-			kvs = append(kvs, convertKeyValue(kv))
-		}
+func (fd FromDomain) convertKeyValues(keyValues model.KeyValues) []dbmodel.KeyValue {
+	kvs := make([]dbmodel.KeyValue, 0, len(keyValues))
+	for i := range keyValues {
+		kvs = append(kvs, fd.convertKeyValue(keyValues[i]))
 	}
 	if kvs == nil {
 		kvs = make([]dbmodel.KeyValue, 0)
 	}
-	return kvs, tagsMap
+	return kvs
 }
 
-func (FromDomain) convertLogs(logs []model.Log) []dbmodel.Log {
+func (fd FromDomain) convertLogs(logs []model.Log) []dbmodel.Log {
 	out := make([]dbmodel.Log, len(logs))
 	for i, log := range logs {
 		var kvs []dbmodel.KeyValue
 		for _, kv := range log.Fields {
-			kvs = append(kvs, convertKeyValue(kv))
+			kvs = append(kvs, fd.convertKeyValue(kv))
 		}
 		out[i] = dbmodel.Log{
 			Timestamp: model.TimeAsEpochMicroseconds(log.Timestamp),
@@ -110,15 +89,14 @@ func (FromDomain) convertLogs(logs []model.Log) []dbmodel.Log {
 }
 
 func (fd FromDomain) convertProcess(process *model.Process) dbmodel.Process {
-	tags, tagsMap := fd.convertKeyValuesString(process.Tags)
+	tags := fd.convertKeyValues(process.Tags)
 	return dbmodel.Process{
 		ServiceName: process.ServiceName,
 		Tags:        tags,
-		Tag:         tagsMap,
 	}
 }
 
-func convertKeyValue(kv model.KeyValue) dbmodel.KeyValue {
+func (FromDomain) convertKeyValue(kv model.KeyValue) dbmodel.KeyValue {
 	outKv := dbmodel.KeyValue{
 		Key:  kv.Key,
 		Type: dbmodel.ValueType(strings.ToLower(kv.VType.String())),
diff --git a/internal/storage/v1/elasticsearch/spanstore/from_domain_test.go b/internal/storage/v1/elasticsearch/spanstore/from_domain_test.go
index 81343d08aba..00e72bb2e96 100644
--- a/internal/storage/v1/elasticsearch/spanstore/from_domain_test.go
+++ b/internal/storage/v1/elasticsearch/spanstore/from_domain_test.go
@@ -29,7 +29,7 @@ func TestFromDomainEmbedProcess(t *testing.T) {
 
 			var span model.Span
 			require.NoError(t, jsonpb.Unmarshal(bytes.NewReader(domainStr), &span))
-			converter := NewFromDomain(false, nil, ":")
+			converter := NewFromDomain()
 			embeddedSpan := converter.FromDomainEmbedProcess(&span)
 
 			testJSONEncoding(t, i, jsonStr, embeddedSpan)
@@ -68,34 +68,12 @@ func testJSONEncoding(t *testing.T, i int, expectedStr []byte, object any) {
 func TestEmptyTags(t *testing.T) {
 	tags := make([]model.KeyValue, 0)
 	span := model.Span{Tags: tags, Process: &model.Process{Tags: tags}}
-	converter := NewFromDomain(false, nil, ":")
+	converter := NewFromDomain()
 	dbSpan := converter.FromDomainEmbedProcess(&span)
 	assert.Empty(t, dbSpan.Tags)
 	assert.Empty(t, dbSpan.Tag)
 }
 
-func TestTagMap(t *testing.T) {
-	tags := []model.KeyValue{
-		model.String("foo", "foo"),
-		model.Bool("a", true),
-		model.Int64("b.b", 1),
-	}
-	span := model.Span{Tags: tags, Process: &model.Process{Tags: tags}}
-	converter := NewFromDomain(false, []string{"a", "b.b", "b*"}, ":")
-	dbSpan := converter.FromDomainEmbedProcess(&span)
-
-	assert.Len(t, dbSpan.Tags, 1)
-	assert.Equal(t, "foo", dbSpan.Tags[0].Key)
-	assert.Len(t, dbSpan.Process.Tags, 1)
-	assert.Equal(t, "foo", dbSpan.Process.Tags[0].Key)
-
-	tagsMap := map[string]any{}
-	tagsMap["a"] = true
-	tagsMap["b:b"] = int64(1)
-	assert.Equal(t, tagsMap, dbSpan.Tag)
-	assert.Equal(t, tagsMap, dbSpan.Process.Tag)
-}
-
 func TestConvertKeyValueValue(t *testing.T) {
 	longString := `Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues
 	Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues Bender Bending Rodrigues
@@ -135,62 +113,9 @@ func TestConvertKeyValueValue(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(fmt.Sprintf("%s:%s", test.expected.Type, test.expected.Key), func(t *testing.T) {
-			actual := convertKeyValue(test.kv)
+			converter := NewFromDomain()
+			actual := converter.convertKeyValue(test.kv)
 			assert.Equal(t, test.expected, actual)
 		})
 	}
 }
-
-func TestNewSpanTags(t *testing.T) {
-	testCases := []struct {
-		spanConverter FromDomain
-		expected      dbmodel.Span
-		name          string
-	}{
-		{
-			spanConverter: NewFromDomain(true, []string{}, ""),
-			expected: dbmodel.Span{
-				Tag: map[string]any{"foo": "bar"}, Tags: []dbmodel.KeyValue{},
-				Process: dbmodel.Process{Tag: map[string]any{"bar": "baz"}, Tags: []dbmodel.KeyValue{}},
-			},
-			name: "allTagsAsFields",
-		},
-		{
-			spanConverter: NewFromDomain(false, []string{"foo", "bar", "rere"}, ""),
-			expected: dbmodel.Span{
-				Tag: map[string]any{"foo": "bar"}, Tags: []dbmodel.KeyValue{},
-				Process: dbmodel.Process{Tag: map[string]any{"bar": "baz"}, Tags: []dbmodel.KeyValue{}},
-			},
-			name: "definedTagNames",
-		},
-		{
-			spanConverter: NewFromDomain(false, []string{}, ""),
-			expected: dbmodel.Span{
-				Tags: []dbmodel.KeyValue{{
-					Key:   "foo",
-					Type:  dbmodel.StringType,
-					Value: "bar",
-				}},
-				Process: dbmodel.Process{Tags: []dbmodel.KeyValue{{
-					Key:   "bar",
-					Type:  dbmodel.StringType,
-					Value: "baz",
-				}}},
-			},
-			name: "noAllTagsAsFields",
-		},
-	}
-	for _, test := range testCases {
-		t.Run(test.name, func(t *testing.T) {
-			s := &model.Span{
-				Tags:    []model.KeyValue{{Key: "foo", VStr: "bar"}},
-				Process: &model.Process{Tags: []model.KeyValue{{Key: "bar", VStr: "baz"}}},
-			}
-			mSpan := test.spanConverter.FromDomainEmbedProcess(s)
-			assert.Equal(t, test.expected.Tag, mSpan.Tag)
-			assert.Equal(t, test.expected.Tags, mSpan.Tags)
-			assert.Equal(t, test.expected.Process.Tag, mSpan.Process.Tag)
-			assert.Equal(t, test.expected.Process.Tags, mSpan.Process.Tags)
-		})
-	}
-}
diff --git a/internal/storage/v1/elasticsearch/spanstore/writer.go b/internal/storage/v1/elasticsearch/spanstore/writer.go
index 8884572a4ef..8b444c10a5d 100644
--- a/internal/storage/v1/elasticsearch/spanstore/writer.go
+++ b/internal/storage/v1/elasticsearch/spanstore/writer.go
@@ -7,6 +7,7 @@ package spanstore
 import (
 	"context"
 	"fmt"
+	"strings"
 	"time"
 
 	"go.uber.org/zap"
@@ -32,8 +33,11 @@ type SpanWriter struct {
 	client func() es.Client
 	logger *zap.Logger
 	// indexCache       cache.Cache
-	serviceWriter    serviceWriter
-	spanServiceIndex spanAndServiceIndexFn
+	serviceWriter     serviceWriter
+	spanServiceIndex  spanAndServiceIndexFn
+	allTagsAsFields   bool
+	tagDotReplacement string
+	tagKeysAsFields   map[string]bool
 }
 
 // CoreSpanWriter is a DB-Level abstraction which directly deals with database level operations
@@ -78,12 +82,20 @@ func NewSpanWriter(p SpanWriterParams) *SpanWriter {
 		}
 	}
 
+	tags := map[string]bool{}
+	for _, k := range p.TagKeysAsFields {
+		tags[k] = true
+	}
+
 	serviceOperationStorage := NewServiceOperationStorage(p.Client, p.Logger, serviceCacheTTL)
 	return &SpanWriter{
-		client:           p.Client,
-		logger:           p.Logger,
-		serviceWriter:    serviceOperationStorage.Write,
-		spanServiceIndex: getSpanAndServiceIndexFn(p, writeAliasSuffix),
+		client:            p.Client,
+		logger:            p.Logger,
+		serviceWriter:     serviceOperationStorage.Write,
+		spanServiceIndex:  getSpanAndServiceIndexFn(p, writeAliasSuffix),
+		tagKeysAsFields:   tags,
+		allTagsAsFields:   p.AllTagsAsFields,
+		tagDotReplacement: p.TagDotReplacement,
 	}
 }
 
@@ -120,6 +132,7 @@ func getSpanAndServiceIndexFn(p SpanWriterParams, writeAlias string) spanAndServ
 
 // WriteSpan writes a span and its corresponding service:operation in ElasticSearch
 func (s *SpanWriter) WriteSpan(spanStartTime time.Time, span *dbmodel.Span) {
+	s.convertNestedTagsToFieldTags(span)
 	spanIndexName, serviceIndexName := s.spanServiceIndex(spanStartTime)
 	if serviceIndexName != "" {
 		s.writeService(serviceIndexName, span)
@@ -128,6 +141,15 @@ func (s *SpanWriter) WriteSpan(spanStartTime time.Time, span *dbmodel.Span) {
 	s.logger.Debug("Wrote span to ES index", zap.String("index", spanIndexName))
 }
 
+func (s *SpanWriter) convertNestedTagsToFieldTags(span *dbmodel.Span) {
+	processNestedTags, processFieldTags := s.splitElevatedTags(span.Process.Tags)
+	span.Process.Tags = processNestedTags
+	span.Process.Tag = processFieldTags
+	nestedTags, fieldTags := s.splitElevatedTags(span.Tags)
+	span.Tags = nestedTags
+	span.Tag = fieldTags
+}
+
 // Close closes SpanWriter
 func (s *SpanWriter) Close() error {
 	return s.client().Close()
@@ -148,3 +170,25 @@ func (s *SpanWriter) writeService(indexName string, jsonSpan *dbmodel.Span) {
 func (s *SpanWriter) writeSpan(indexName string, jsonSpan *dbmodel.Span) {
 	s.client().Index().Index(indexName).Type(spanType).BodyJson(&jsonSpan).Add()
 }
+
+func (s *SpanWriter) splitElevatedTags(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
+	if !s.allTagsAsFields && len(s.tagKeysAsFields) == 0 {
+		return keyValues, nil
+	}
+	var tagsMap map[string]any
+	var kvs []dbmodel.KeyValue
+	for _, kv := range keyValues {
+		if kv.Type != dbmodel.BinaryType && (s.allTagsAsFields || s.tagKeysAsFields[kv.Key]) {
+			if tagsMap == nil {
+				tagsMap = map[string]any{}
+			}
+			tagsMap[strings.ReplaceAll(kv.Key, ".", s.tagDotReplacement)] = kv.Value
+		} else {
+			kvs = append(kvs, kv)
+		}
+	}
+	if kvs == nil {
+		kvs = make([]dbmodel.KeyValue, 0)
+	}
+	return kvs, tagsMap
+}
diff --git a/internal/storage/v1/elasticsearch/spanstore/writer_test.go b/internal/storage/v1/elasticsearch/spanstore/writer_test.go
index e8f23b01aea..2ad3bebcab2 100644
--- a/internal/storage/v1/elasticsearch/spanstore/writer_test.go
+++ b/internal/storage/v1/elasticsearch/spanstore/writer_test.go
@@ -414,6 +414,111 @@ func TestSpanWriterParamsTTL(t *testing.T) {
 	}
 }
 
+func TestTagMap(t *testing.T) {
+	tags := []dbmodel.KeyValue{
+		{
+			Key:   "foo",
+			Value: "foo",
+			Type:  dbmodel.StringType,
+		},
+		{
+			Key:   "a",
+			Value: true,
+			Type:  dbmodel.BoolType,
+		},
+		{
+			Key:   "b.b",
+			Value: int64(1),
+			Type:  dbmodel.Int64Type,
+		},
+	}
+	dbSpan := dbmodel.Span{Tags: tags, Process: dbmodel.Process{Tags: tags}}
+	converter := NewSpanWriter(SpanWriterParams{
+		AllTagsAsFields:   false,
+		TagKeysAsFields:   []string{"a", "b.b", "b*"},
+		TagDotReplacement: ":",
+	})
+	converter.convertNestedTagsToFieldTags(&dbSpan)
+
+	assert.Len(t, dbSpan.Tags, 1)
+	assert.Equal(t, "foo", dbSpan.Tags[0].Key)
+	assert.Len(t, dbSpan.Process.Tags, 1)
+	assert.Equal(t, "foo", dbSpan.Process.Tags[0].Key)
+
+	tagsMap := map[string]any{}
+	tagsMap["a"] = true
+	tagsMap["b:b"] = int64(1)
+	assert.Equal(t, tagsMap, dbSpan.Tag)
+	assert.Equal(t, tagsMap, dbSpan.Process.Tag)
+}
+
+func TestNewSpanTags(t *testing.T) {
+	testCases := []struct {
+		params   SpanWriterParams
+		expected dbmodel.Span
+		name     string
+	}{
+		{
+			params: SpanWriterParams{
+				AllTagsAsFields:   true,
+				TagKeysAsFields:   []string{},
+				TagDotReplacement: "",
+			},
+			expected: dbmodel.Span{
+				Tag: map[string]any{"foo": "bar"}, Tags: []dbmodel.KeyValue{},
+				Process: dbmodel.Process{Tag: map[string]any{"bar": "baz"}, Tags: []dbmodel.KeyValue{}},
+			},
+			name: "allTagsAsFields",
+		},
+		{
+			params: SpanWriterParams{
+				AllTagsAsFields:   false,
+				TagKeysAsFields:   []string{"foo", "bar", "rere"},
+				TagDotReplacement: "",
+			},
+			expected: dbmodel.Span{
+				Tag: map[string]any{"foo": "bar"}, Tags: []dbmodel.KeyValue{},
+				Process: dbmodel.Process{Tag: map[string]any{"bar": "baz"}, Tags: []dbmodel.KeyValue{}},
+			},
+			name: "definedTagNames",
+		},
+		{
+			params: SpanWriterParams{
+				AllTagsAsFields:   false,
+				TagKeysAsFields:   []string{},
+				TagDotReplacement: "",
+			},
+			expected: dbmodel.Span{
+				Tags: []dbmodel.KeyValue{{
+					Key:   "foo",
+					Type:  dbmodel.StringType,
+					Value: "bar",
+				}},
+				Process: dbmodel.Process{Tags: []dbmodel.KeyValue{{
+					Key:   "bar",
+					Type:  dbmodel.StringType,
+					Value: "baz",
+				}}},
+			},
+			name: "noAllTagsAsFields",
+		},
+	}
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			mSpan := &dbmodel.Span{
+				Tags:    []dbmodel.KeyValue{{Key: "foo", Value: "bar", Type: dbmodel.StringType}},
+				Process: dbmodel.Process{Tags: []dbmodel.KeyValue{{Key: "bar", Value: "baz", Type: dbmodel.StringType}}},
+			}
+			writer := NewSpanWriter(test.params)
+			writer.convertNestedTagsToFieldTags(mSpan)
+			assert.Equal(t, test.expected.Tag, mSpan.Tag)
+			assert.Equal(t, test.expected.Tags, mSpan.Tags)
+			assert.Equal(t, test.expected.Process.Tag, mSpan.Process.Tag)
+			assert.Equal(t, test.expected.Process.Tags, mSpan.Process.Tags)
+		})
+	}
+}
+
 // stringMatcher can match a string argument when it contains a specific substring q
 func stringMatcher(q string) any {
 	matchFunc := func(s string) bool {
diff --git a/internal/storage/v1/elasticsearch/spanstore/writerv1.go b/internal/storage/v1/elasticsearch/spanstore/writerv1.go
index b4b1072e340..0c0b90971d4 100644
--- a/internal/storage/v1/elasticsearch/spanstore/writerv1.go
+++ b/internal/storage/v1/elasticsearch/spanstore/writerv1.go
@@ -11,15 +11,13 @@ import (
 )
 
 type SpanWriterV1 struct {
-	spanWriter    CoreSpanWriter
-	spanConverter FromDomain
+	spanWriter CoreSpanWriter
 }
 
 // NewSpanWriterV1 returns the SpanWriterV1 for use
 func NewSpanWriterV1(p SpanWriterParams) *SpanWriterV1 {
 	return &SpanWriterV1{
-		spanWriter:    NewSpanWriter(p),
-		spanConverter: NewFromDomain(p.AllTagsAsFields, p.TagKeysAsFields, p.TagDotReplacement),
+		spanWriter: NewSpanWriter(p),
 	}
 }
 
@@ -30,7 +28,8 @@ func (s *SpanWriterV1) CreateTemplates(spanTemplate, serviceTemplate string, ind
 
 // WriteSpan writes a span and its corresponding service:operation in ElasticSearch
 func (s *SpanWriterV1) WriteSpan(_ context.Context, span *model.Span) error {
-	jsonSpan := s.spanConverter.FromDomainEmbedProcess(span)
+	converter := NewFromDomain()
+	jsonSpan := converter.FromDomainEmbedProcess(span)
 	s.spanWriter.WriteSpan(span.StartTime, jsonSpan)
 	return nil
 }
diff --git a/internal/storage/v1/elasticsearch/spanstore/writerv1_test.go b/internal/storage/v1/elasticsearch/spanstore/writerv1_test.go
index 07ca359f656..c225b9209a8 100644
--- a/internal/storage/v1/elasticsearch/spanstore/writerv1_test.go
+++ b/internal/storage/v1/elasticsearch/spanstore/writerv1_test.go
@@ -19,8 +19,8 @@ func TestSpanWriterV1_WriteSpan(t *testing.T) {
 		Tags:    []model.KeyValue{{Key: "foo", VStr: "bar"}},
 		Process: &model.Process{Tags: []model.KeyValue{{Key: "bar", VStr: "baz"}}},
 	}
-	converter := NewFromDomain(true, []string{}, "-")
-	writerV1 := &SpanWriterV1{spanWriter: coreWriter, spanConverter: converter}
+	writerV1 := &SpanWriterV1{spanWriter: coreWriter}
+	converter := NewFromDomain()
 	coreWriter.On("WriteSpan", s.StartTime, converter.FromDomainEmbedProcess(s))
 	err := writerV1.WriteSpan(context.Background(), s)
 	require.NoError(t, err)


Reviews:
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T14:07:29Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T14:08:46Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T14:09:47Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T14:10:02Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-30T16:08:40Z
user: login: yurishkuro
body: this is the example where snapshot testing would be very helpful, maybe spend a day to build those tests first.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-30T16:10:02Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T09:35:32Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T15:36:55Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T15:38:06Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T15:47:36Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T21:17:46Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T21:19:03Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-06T20:06:23Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-06T20:07:31Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T20:14:27Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-06T20:19:57Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-06T20:21:54Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T20:36:39Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-06T20:48:28Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T21:09:57Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T17:43:46Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T19:36:28Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T04:50:49Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T14:07:50Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T14:19:52Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T14:23:16Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T14:38:11Z
user: login: yurishkuro
body: overall lgtm, but many comments on the code organization / cleanliness. Also, we need to make a follow-up change by preservice the types of elevated tags, e.g. we can add another top-level map called `TagType` such that reading tags back will not incur data loss due to float/int confusion.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T16:49:42Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T17:33:19Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T17:41:28Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T17:43:17Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T17:45:51Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T18:37:58Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T18:38:43Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T22:07:33Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T02:04:40Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T16:45:44Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T13:25:19Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T13:27:49Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T13:29:12Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:30:57Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:36:07Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:37:26Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T13:41:02Z
user: login: Manik2708
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:47:37Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T14:18:59Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T14:20:56Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T14:35:14Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T16:26:24Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T23:14:03Z
user: login: yurishkuro
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-22T23:16:28Z
user: login: yurishkuro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T14:00:56Z
user: login: yurishkuro
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-27T17:23:44Z

Review comments:
diff_hunk: @@ -45,143 +61,91 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+func (t ToDBModel) resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
 	resource := resourceSpans.Resource()
 	scopeSpans := resourceSpans.ScopeSpans()
 
 	if scopeSpans.Len() == 0 {
 		return []dbmodel.Span{}
 	}
 
-	process := resourceToDbProcess(resource)
+	process := t.resourceToDbProcess(resource)
 
 	// Approximate the number of the spans as the number of the spans in the first
 	// instrumentation library info.
 	dbSpans := make([]dbmodel.Span, 0, scopeSpans.At(0).Spans().Len())
 
 	for _, scopeSpan := range scopeSpans.All() {
 		for _, span := range scopeSpan.Spans().All() {
-			dbSpan := spanToDbSpan(span, scopeSpan.Scope(), process)
+			dbSpan := t.spanToDbSpan(span, scopeSpan.Scope(), process)
 			dbSpans = append(dbSpans, dbSpan)
 		}
 	}
 
 	return dbSpans
 }
 
-func resourceToDbProcess(resource pcommon.Resource) dbmodel.Process {
+func (t ToDBModel) resourceToDbProcess(resource pcommon.Resource) dbmodel.Process {
 	process := dbmodel.Process{}
 	attrs := resource.Attributes()
 	if attrs.Len() == 0 {
 		process.ServiceName = noServiceName
 		return process
 	}
-	tags := make([]dbmodel.KeyValue, 0, attrs.Len())
+	tagAppender := newTagAppender(t.allTagsAsFields, t.tagKeysAsFields, t.tagDotReplacement)
 	for key, attr := range attrs.All() {
 		if key == conventions.AttributeServiceName {
 			process.ServiceName = attr.AsString()
 			continue
 		}
-		tags = append(tags, attributeToDbTag(key, attr))
+		tagAppender.appendTag(key, attr)
 	}
+	tags, tagMap := tagAppender.getTags()
 	process.Tags = tags
+	process.Tag = tagMap
 	return process
 }
 
 func appendTagsFromAttributes(dest []dbmodel.KeyValue, attrs pcommon.Map) []dbmodel.KeyValue {
 	for key, attr := range attrs.All() {
-		dest = append(dest, attributeToDbTag(key, attr))
+		dest = append(dest, dbmodel.KeyValue{
+			Key:   key,
+			Type:  attributeToDbType(attr.Type()),
+			Value: attr.AsString(),
+		})
 	}
 	return dest
 }
 
-func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: Moved to `tagAppender.go`
created_at: 2025-03-30T14:07:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,143 +61,91 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+func (t ToDBModel) resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
 	resource := resourceSpans.Resource()
 	scopeSpans := resourceSpans.ScopeSpans()
 
 	if scopeSpans.Len() == 0 {
 		return []dbmodel.Span{}
 	}
 
-	process := resourceToDbProcess(resource)
+	process := t.resourceToDbProcess(resource)
 
 	// Approximate the number of the spans as the number of the spans in the first
 	// instrumentation library info.
 	dbSpans := make([]dbmodel.Span, 0, scopeSpans.At(0).Spans().Len())
 
 	for _, scopeSpan := range scopeSpans.All() {
 		for _, span := range scopeSpan.Spans().All() {
-			dbSpan := spanToDbSpan(span, scopeSpan.Scope(), process)
+			dbSpan := t.spanToDbSpan(span, scopeSpan.Scope(), process)
 			dbSpans = append(dbSpans, dbSpan)
 		}
 	}
 
 	return dbSpans
 }
 
-func resourceToDbProcess(resource pcommon.Resource) dbmodel.Process {
+func (t ToDBModel) resourceToDbProcess(resource pcommon.Resource) dbmodel.Process {
 	process := dbmodel.Process{}
 	attrs := resource.Attributes()
 	if attrs.Len() == 0 {
 		process.ServiceName = noServiceName
 		return process
 	}
-	tags := make([]dbmodel.KeyValue, 0, attrs.Len())
+	tagAppender := newTagAppender(t.allTagsAsFields, t.tagKeysAsFields, t.tagDotReplacement)
 	for key, attr := range attrs.All() {
 		if key == conventions.AttributeServiceName {
 			process.ServiceName = attr.AsString()
 			continue
 		}
-		tags = append(tags, attributeToDbTag(key, attr))
+		tagAppender.appendTag(key, attr)
 	}
+	tags, tagMap := tagAppender.getTags()
 	process.Tags = tags
+	process.Tag = tagMap
 	return process
 }
 
 func appendTagsFromAttributes(dest []dbmodel.KeyValue, attrs pcommon.Map) []dbmodel.KeyValue {
 	for key, attr := range attrs.All() {
-		dest = append(dest, attributeToDbTag(key, attr))
+		dest = append(dest, dbmodel.KeyValue{
+			Key:   key,
+			Type:  attributeToDbType(attr.Type()),
+			Value: attr.AsString(),
+		})
 	}
 	return dest
 }
 
-func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
-	// TODO why are all values being converted to strings?
-	tag := dbmodel.KeyValue{Key: key, Value: attr.AsString()}
-	switch attr.Type() {
-	case pcommon.ValueTypeStr:
-		tag.Type = dbmodel.StringType
-	case pcommon.ValueTypeInt:
-		tag.Type = dbmodel.Int64Type
-	case pcommon.ValueTypeBool:
-		tag.Type = dbmodel.BoolType
-	case pcommon.ValueTypeDouble:
-		tag.Type = dbmodel.Float64Type
-	case pcommon.ValueTypeBytes:
-		tag.Type = dbmodel.BinaryType
-	case pcommon.ValueTypeMap, pcommon.ValueTypeSlice:
-		tag.Type = dbmodel.StringType
-	}
-	return tag
-}
-
-func spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
+func (t ToDBModel) spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
 	traceID := dbmodel.TraceID(span.TraceID().String())
 	parentSpanID := dbmodel.SpanID(span.ParentSpanID().String())
 	startTime := span.StartTimestamp().AsTime()
+	tags, tagMap := t.getDbSpanTags(span, libraryTags)
 	return dbmodel.Span{
 		TraceID:       traceID,
 		SpanID:        dbmodel.SpanID(span.SpanID().String()),
 		OperationName: span.Name(),
 		References:    linksToDbSpanRefs(span.Links(), parentSpanID, traceID),
 		StartTime:     model.TimeAsEpochMicroseconds(startTime),
 		Duration:      model.DurationAsMicroseconds(span.EndTimestamp().AsTime().Sub(startTime)),
-		Tags:          getDbSpanTags(span, libraryTags),
+		Tags:          tags,
+		Tag:           tagMap,
 		Logs:          spanEventsToDbSpanLogs(span.Events()),
 		Process:       process,
 	}
 }
 
-func getDbSpanTags(span ptrace.Span, scope pcommon.InstrumentationScope) []dbmodel.KeyValue {
-	var spanKindTag, statusCodeTag, statusMsgTag dbmodel.KeyValue
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: The reason to do this is a trial to clean the code a bit! Also to embed `tagDotReplacement`
created_at: 2025-03-30T14:08:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -255,96 +219,6 @@ func spanEventsToDbSpanLogs(events ptrace.SpanEventSlice) []dbmodel.Log {
 	return logs
 }
 
-func getTagFromSpanKind(spanKind ptrace.SpanKind) (dbmodel.KeyValue, bool) {
-	var tagStr string
-	switch spanKind {
-	case ptrace.SpanKindClient:
-		tagStr = string(model.SpanKindClient)
-	case ptrace.SpanKindServer:
-		tagStr = string(model.SpanKindServer)
-	case ptrace.SpanKindProducer:
-		tagStr = string(model.SpanKindProducer)
-	case ptrace.SpanKindConsumer:
-		tagStr = string(model.SpanKindConsumer)
-	case ptrace.SpanKindInternal:
-		tagStr = string(model.SpanKindInternal)
-	default:
-		return dbmodel.KeyValue{}, false
-	}
-
-	return dbmodel.KeyValue{
-		Key:   model.SpanKindKey,
-		Type:  dbmodel.StringType,
-		Value: tagStr,
-	}, true
-}
-
-func getTagFromStatusCode(statusCode ptrace.StatusCode) (dbmodel.KeyValue, bool) {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: This from L282-L347 is a part of tag appender now!
created_at: 2025-03-30T14:09:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -255,96 +219,6 @@ func spanEventsToDbSpanLogs(events ptrace.SpanEventSlice) []dbmodel.Log {
 	return logs
 }
 
-func getTagFromSpanKind(spanKind ptrace.SpanKind) (dbmodel.KeyValue, bool) {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: This is also moved `tagAppender.go`
created_at: 2025-03-30T14:10:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestore
+
+import (
+	"strings"
+
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/ptrace"
+	conventions "go.opentelemetry.io/collector/semconv/v1.16.0"
+
+	"github.com/jaegertracing/jaeger-idl/model/v1"
+	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
+)
+
+type tagAppender struct {
path: internal/storage/v2/elasticsearch/tracestore/tag_appender.go
user: login: yurishkuro
body: add a comment explaining what this aims to solve
created_at: 2025-03-30T16:08:40Z
author_association: MEMBER
diff_hunk: @@ -275,11 +151,55 @@ func TestToDbModel_Fixtures(t *testing.T) {
 	td, err := FromDBModel([]dbmodel.Span{span})
 	require.NoError(t, err)
 	testTraces(t, tracesStr, td)
-	spans := ToDBModel(td)
+	toDb := NewToDBModel(false, nil, ".")
+	spans := toDb.ConvertToDBModel(td)
 	assert.Len(t, spans, 1)
 	testSpans(t, spansStr, spans[0])
 }
 
+func TestToDbModel_DotReplacement_TagKeysAsFields(t *testing.T) {
+	tests := []struct {
+		name            string
+		allTagsAsFields bool
+		tagKeysAsFields []string
+	}{
+		{
+			name:            "allTagsAsObject=true",
+			allTagsAsFields: true,
+		},
+		{
+			name:            "allTagsAsObject=false",
+			allTagsAsFields: false,
+			tagKeysAsFields: []string{
+				"blob",
+				"error",
+				"otel.scope.name",
+				"otel.scope.version",
+				"peer.ipv4",
+				"peer.service",
+				"temperature",
+				"sdk.version.1",
+				"sdk.version.2",
+				"w3c.tracestate",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tracesData := loadTraces(t, 1)
+			unmarshaller := ptrace.JSONUnmarshaler{}
+			expectedTd, err := unmarshaller.UnmarshalTraces(tracesData)
+			require.NoError(t, err)
+			dotReplacement := "#"
+			toDb := NewToDBModel(tt.allTagsAsFields, tt.tagKeysAsFields, dotReplacement)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel_test.go
user: login: Manik2708
body: Currently I have skipped roundtrip because `tagDotReplacement` in `FromDBModel` will be implemented in next PR!
created_at: 2025-04-04T09:35:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,11 +151,55 @@ func TestToDbModel_Fixtures(t *testing.T) {
 	td, err := FromDBModel([]dbmodel.Span{span})
 	require.NoError(t, err)
 	testTraces(t, tracesStr, td)
-	spans := ToDBModel(td)
+	toDb := NewToDBModel(false, nil, ".")
+	spans := toDb.ConvertToDBModel(td)
 	assert.Len(t, spans, 1)
 	testSpans(t, spansStr, spans[0])
 }
 
+func TestToDbModel_DotReplacement_TagKeysAsFields(t *testing.T) {
+	tests := []struct {
+		name            string
+		allTagsAsFields bool
+		tagKeysAsFields []string
+	}{
+		{
+			name:            "allTagsAsObject=true",
+			allTagsAsFields: true,
+		},
+		{
+			name:            "allTagsAsObject=false",
+			allTagsAsFields: false,
+			tagKeysAsFields: []string{
+				"blob",
+				"error",
+				"otel.scope.name",
+				"otel.scope.version",
+				"peer.ipv4",
+				"peer.service",
+				"temperature",
+				"sdk.version.1",
+				"sdk.version.2",
+				"w3c.tracestate",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tracesData := loadTraces(t, 1)
+			unmarshaller := ptrace.JSONUnmarshaler{}
+			expectedTd, err := unmarshaller.UnmarshalTraces(tracesData)
+			require.NoError(t, err)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel_test.go
user: login: yurishkuro
body: if you have a function called `loadTraces` why doesn't it unmarshal and return ptrace model directly?
created_at: 2025-04-05T15:36:54Z
author_association: MEMBER
diff_hunk: @@ -333,7 +269,8 @@ func BenchmarkInternalTracesToDbSpans(b *testing.B) {
 
 	b.ResetTimer()
 	for n := 0; n < b.N; n++ {
-		batches := ToDBModel(td)
+		toDb := NewToDBModel(false, nil, ".")
+		batches := toDb.ConvertToDBModel(td)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel_test.go
user: login: yurishkuro
body: this isn't a useful separation. You can hide these details inside ToDBModel()
created_at: 2025-04-05T15:38:06Z
author_association: MEMBER
diff_hunk: @@ -333,7 +269,8 @@ func BenchmarkInternalTracesToDbSpans(b *testing.B) {
 
 	b.ResetTimer()
 	for n := 0; n < b.N; n++ {
-		batches := ToDBModel(td)
+		toDb := NewToDBModel(false, nil, ".")
+		batches := toDb.ConvertToDBModel(td)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel_test.go
user: login: Manik2708
body: Does that mean removing class and passing these three as parameters to `ToDBModel()`?
created_at: 2025-04-05T15:47:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,11 +151,55 @@ func TestToDbModel_Fixtures(t *testing.T) {
 	td, err := FromDBModel([]dbmodel.Span{span})
 	require.NoError(t, err)
 	testTraces(t, tracesStr, td)
-	spans := ToDBModel(td)
+	toDb := NewToDBModel(false, nil, ".")
+	spans := toDb.ConvertToDBModel(td)
 	assert.Len(t, spans, 1)
 	testSpans(t, spansStr, spans[0])
 }
 
+func TestToDbModel_DotReplacement_TagKeysAsFields(t *testing.T) {
+	tests := []struct {
+		name            string
+		allTagsAsFields bool
+		tagKeysAsFields []string
+	}{
+		{
+			name:            "allTagsAsObject=true",
+			allTagsAsFields: true,
+		},
+		{
+			name:            "allTagsAsObject=false",
+			allTagsAsFields: false,
+			tagKeysAsFields: []string{
+				"blob",
+				"error",
+				"otel.scope.name",
+				"otel.scope.version",
+				"peer.ipv4",
+				"peer.service",
+				"temperature",
+				"sdk.version.1",
+				"sdk.version.2",
+				"w3c.tracestate",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tracesData := loadTraces(t, 1)
+			unmarshaller := ptrace.JSONUnmarshaler{}
+			expectedTd, err := unmarshaller.UnmarshalTraces(tracesData)
+			require.NoError(t, err)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel_test.go
user: login: Manik2708
body: I tried doing this but I think it is better to return `[]bytes` rather than marshalling because we are asserting bytes not the objects. Asserting objects needs more complexity as we need to pass a copy of spans to to `FromModel` because it might manipulate the span (in fact it is manupilating). So `loadTraces` is a part of `loadFixtures`, for which we need byte data not the objects. 
created_at: 2025-04-05T21:17:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,9 +25,34 @@ const (
 	tagError         = "error"
 )
 
-// ToDBModel translates internal trace data into the DB Spans.
+// ToDBModel is used to convert OTLP Traces to db spans
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: Is this what we expect?
created_at: 2025-04-05T21:19:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,9 +25,34 @@ const (
 	tagError         = "error"
 )
 
-// ToDBModel translates internal trace data into the DB Spans.
+// ToDBModel is used to convert OTLP Traces to db spans
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: > tagDotReplacement

why is this a parameter? It seems to me like an unnecessary complexity that has no business value - we need to escape dots in ES field names, and we're doing it in a certain way, there's no reason for that certain way to be customizable.
created_at: 2025-04-06T20:06:23Z
author_association: MEMBER
diff_hunk: @@ -19,53 +20,17 @@ import (
 	"go.opentelemetry.io/collector/pdata/ptrace"
 	conventions "go.opentelemetry.io/collector/semconv/v1.9.0"
 
-	"github.com/jaegertracing/jaeger-idl/model/v1"
 	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
 )
 
-func TestGetTagFromStatusCode(t *testing.T) {
-	tests := []struct {
-		name string
-		code ptrace.StatusCode
-		tag  dbmodel.KeyValue
-	}{
-		{
-			name: "ok",
-			code: ptrace.StatusCodeOk,
-			tag: dbmodel.KeyValue{
-				Key:   conventions.OtelStatusCode,
-				Type:  dbmodel.StringType,
-				Value: statusOk,
-			},
-		},
-
-		{
-			name: "error",
-			code: ptrace.StatusCodeError,
-			tag: dbmodel.KeyValue{
-				Key:   tagError,
-				Type:  dbmodel.BoolType,
-				Value: "true",
-			},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			got, ok := getTagFromStatusCode(test.code)
-			assert.True(t, ok)
-			assert.Equal(t, test.tag, got)
-		})
-	}
-}
-
 func TestEmptyAttributes(t *testing.T) {
 	traces := ptrace.NewTraces()
 	spans := traces.ResourceSpans().AppendEmpty()
 	scopeSpans := spans.ScopeSpans().AppendEmpty()
 	spanScope := scopeSpans.Scope()
 	span := scopeSpans.Spans().AppendEmpty()
-	modelSpan := spanToDbSpan(span, spanScope, dbmodel.Process{})
+	toDb := newToDBModel(false, nil, ".")
+	modelSpan := toDb.spanToDbSpan(span, spanScope, dbmodel.Process{})
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel_test.go
user: login: yurishkuro
body: why is this calling private methods instead of the main entry point?
created_at: 2025-04-06T20:07:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestore
+
+import (
+	"encoding/hex"
+	"strings"
+
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/ptrace"
+	conventions "go.opentelemetry.io/collector/semconv/v1.16.0"
+
+	"github.com/jaegertracing/jaeger-idl/model/v1"
+	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
+)
+
+// tagAppender append tags to dbmodel KeyValue slice and tagsMap by replacing dots with tagDotReplacement
path: internal/storage/v2/elasticsearch/tracestore/tag_appender.go
user: login: yurishkuro
body: first, this comment is practically useless, it tells what the actual implementation code is doing (which I can see by looking at the code) instead of telling what business function it performs and why.

second, the struct is doing way more than that, which is why I asked you to write a definition in the comment. Its primary objective is to convert tags into top-level object fields in ES span, so that they get indexed more efficiently (that is what the definition is supposed to say). This objective has nothing to do with understanding all the different flavors of the tags like span kind, status code, etc. - all of that is meant to be handled by the transformer. In the v1 code the whole concept of "tag appender" used to be like a single function (it wasn't even a struct), so why is it necessary to mix it up with so much unrelated functionality?
created_at: 2025-04-06T20:14:15Z
author_association: MEMBER
diff_hunk: @@ -27,9 +25,34 @@ const (
 	tagError         = "error"
 )
 
-// ToDBModel translates internal trace data into the DB Spans.
+// ToDBModel is used to convert OTLP Traces to db spans
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: I am trying to keep the `SpanWriterParams` and the config same, because `TagDotReplacement` is a part of es configuration, I think if we want to remove it from here, then we have to remove it from configuration also because then it doesn't make sense to keep it in config and then not using it anywhere.
created_at: 2025-04-06T20:14:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+{
path: internal/storage/v2/elasticsearch/tracestore/fixtures/es_02.json
user: login: yurishkuro
body: the convention in the fixtures is that es_01 corresponds to otel_01, es_02 to otel_02, etc. Don't break this convention by introducing arbitrary naming scheme. If you have two different flavors of ES span (e.g. one with materialized fields another without) then they should still be named es_01_{suffix}
created_at: 2025-04-06T20:15:53Z
author_association: MEMBER
diff_hunk: @@ -27,9 +25,34 @@ const (
 	tagError         = "error"
 )
 
-// ToDBModel translates internal trace data into the DB Spans.
+// ToDBModel is used to convert OTLP Traces to db spans
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
+	tags := map[string]bool{}
+	for _, k := range tagKeysAsFields {
+		tags[k] = true
+	}
+	toDb := newToDBModel(allTagsAsObject, tags, tagDotReplacement)
+	return toDb.convertToDBModel(td)
+}
+
+func newToDBModel(allTagsAsObject bool, tagKeysAsFields map[string]bool, tagDotReplacement string) toDBModel {
+	return toDBModel{
+		allTagsAsFields:   allTagsAsObject,
+		tagKeysAsFields:   tagKeysAsFields,
+		tagDotReplacement: tagDotReplacement,
+	}
+}
+
+// toDBModel is used to convert OTLP traces to db spans
+type toDBModel struct {
+	allTagsAsFields   bool
+	tagKeysAsFields   map[string]bool
+	tagDotReplacement string
+}
+
+// convertToDBModel translates internal trace data into the DB Spans.
 // Returns slice of translated DB Spans and error if translation failed.
-func ToDBModel(td ptrace.Traces) []dbmodel.Span {
+func (t toDBModel) convertToDBModel(td ptrace.Traces) []dbmodel.Span {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: ```suggestion
func (t *toDBModel) convertToDBModel(td ptrace.Traces) []dbmodel.Span {
```
created_at: 2025-04-06T20:17:00Z
author_association: MEMBER
diff_hunk: @@ -27,9 +25,34 @@ const (
 	tagError         = "error"
 )
 
-// ToDBModel translates internal trace data into the DB Spans.
+// ToDBModel is used to convert OTLP Traces to db spans
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
+	tags := map[string]bool{}
+	for _, k := range tagKeysAsFields {
+		tags[k] = true
+	}
+	toDb := newToDBModel(allTagsAsObject, tags, tagDotReplacement)
+	return toDb.convertToDBModel(td)
+}
+
+func newToDBModel(allTagsAsObject bool, tagKeysAsFields map[string]bool, tagDotReplacement string) toDBModel {
+	return toDBModel{
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: ```suggestion
func newToDBModel(allTagsAsObject bool, tagKeysAsFields map[string]bool, tagDotReplacement string) *toDBModel {
	return &toDBModel{
```
created_at: 2025-04-06T20:17:18Z
author_association: MEMBER
diff_hunk: @@ -45,143 +61,91 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+func (t ToDBModel) resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
 	resource := resourceSpans.Resource()
 	scopeSpans := resourceSpans.ScopeSpans()
 
 	if scopeSpans.Len() == 0 {
 		return []dbmodel.Span{}
 	}
 
-	process := resourceToDbProcess(resource)
+	process := t.resourceToDbProcess(resource)
 
 	// Approximate the number of the spans as the number of the spans in the first
 	// instrumentation library info.
 	dbSpans := make([]dbmodel.Span, 0, scopeSpans.At(0).Spans().Len())
 
 	for _, scopeSpan := range scopeSpans.All() {
 		for _, span := range scopeSpan.Spans().All() {
-			dbSpan := spanToDbSpan(span, scopeSpan.Scope(), process)
+			dbSpan := t.spanToDbSpan(span, scopeSpan.Scope(), process)
 			dbSpans = append(dbSpans, dbSpan)
 		}
 	}
 
 	return dbSpans
 }
 
-func resourceToDbProcess(resource pcommon.Resource) dbmodel.Process {
+func (t ToDBModel) resourceToDbProcess(resource pcommon.Resource) dbmodel.Process {
 	process := dbmodel.Process{}
 	attrs := resource.Attributes()
 	if attrs.Len() == 0 {
 		process.ServiceName = noServiceName
 		return process
 	}
-	tags := make([]dbmodel.KeyValue, 0, attrs.Len())
+	tagAppender := newTagAppender(t.allTagsAsFields, t.tagKeysAsFields, t.tagDotReplacement)
 	for key, attr := range attrs.All() {
 		if key == conventions.AttributeServiceName {
 			process.ServiceName = attr.AsString()
 			continue
 		}
-		tags = append(tags, attributeToDbTag(key, attr))
+		tagAppender.appendTag(key, attr)
 	}
+	tags, tagMap := tagAppender.getTags()
 	process.Tags = tags
+	process.Tag = tagMap
 	return process
 }
 
 func appendTagsFromAttributes(dest []dbmodel.KeyValue, attrs pcommon.Map) []dbmodel.KeyValue {
 	for key, attr := range attrs.All() {
-		dest = append(dest, attributeToDbTag(key, attr))
+		dest = append(dest, dbmodel.KeyValue{
+			Key:   key,
+			Type:  attributeToDbType(attr.Type()),
+			Value: attr.AsString(),
+		})
 	}
 	return dest
 }
 
-func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: why?
created_at: 2025-04-06T20:18:14Z
author_association: MEMBER
diff_hunk: @@ -255,96 +219,6 @@ func spanEventsToDbSpanLogs(events ptrace.SpanEventSlice) []dbmodel.Log {
 	return logs
 }
 
-func getTagFromSpanKind(spanKind ptrace.SpanKind) (dbmodel.KeyValue, bool) {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: none of these changes are necessary or beneficial. Tag appender (bad name) should have just a single function - take a collection of already converted tags (converted in this translator) and materialize all/some of them to the top-level fields.
created_at: 2025-04-06T20:19:53Z
author_association: MEMBER
diff_hunk: @@ -27,9 +25,34 @@ const (
 	tagError         = "error"
 )
 
-// ToDBModel translates internal trace data into the DB Spans.
+// ToDBModel is used to convert OTLP Traces to db spans
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: (sigh) ok. This was a stupid decision made long time ago. Not everything needs to be user-customizable, it's an anti-pattern. But keep it for now.
created_at: 2025-04-06T20:21:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestore
+
+import (
+	"encoding/hex"
+	"strings"
+
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/ptrace"
+	conventions "go.opentelemetry.io/collector/semconv/v1.16.0"
+
+	"github.com/jaegertracing/jaeger-idl/model/v1"
+	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
+)
+
+// tagAppender append tags to dbmodel KeyValue slice and tagsMap by replacing dots with tagDotReplacement
path: internal/storage/v2/elasticsearch/tracestore/tag_appender.go
user: login: Manik2708
body: Got your point on mixing span kind, status code etc but the difference in v1 and v2 is that there is more conversion logic present in v2. I also thought initially to create a method of `toDBModel` but then we had to pass `map[string]any` and `[]dbmodel.KeyValue` everytime when we call the method along with key and value. This is what is happening in v1 but in v2 we had to use this method more frequently, take an example: 
```
tag := make(map[string]any)
tags := make([]dbmodel.KeyValue)
kindStr := convertToStringSpanKind(span.Kind())
if kindStr != "" {
  t.appendTag(model.SpanKindKey, pcommon.NewStrValue(kindStr), tag, tags)
}
// Similar with other tags
return tag, tags
```
To reduce this redundant passing of parameter, I wrapped it inside a struct. We could move to the v1 way also, would require your suggestion here!
created_at: 2025-04-06T20:36:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestore
+
+import (
+	"encoding/hex"
+	"strings"
+
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/ptrace"
+	conventions "go.opentelemetry.io/collector/semconv/v1.16.0"
+
+	"github.com/jaegertracing/jaeger-idl/model/v1"
+	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
+)
+
+// tagAppender append tags to dbmodel KeyValue slice and tagsMap by replacing dots with tagDotReplacement
path: internal/storage/v2/elasticsearch/tracestore/tag_appender.go
user: login: yurishkuro
body: Code that serves different problems needs to be kept separately. You already have implementation of translating tags / attributes between OTLP and DB, which is part of the overall converter - logical organization, why change that? Separately there's an additional capability that can materialize nested tags into top-level tags in ES object, for improved indexing in ES. That's a separate functionality that doesn't need to know _how_ the tags are translated between OTLP and DB, it just needs to apply after that translation and materialize some of the tags
created_at: 2025-04-06T20:48:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package tracestore
+
+import (
+	"encoding/hex"
+	"strings"
+
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/ptrace"
+	conventions "go.opentelemetry.io/collector/semconv/v1.16.0"
+
+	"github.com/jaegertracing/jaeger-idl/model/v1"
+	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
+)
+
+// tagAppender append tags to dbmodel KeyValue slice and tagsMap by replacing dots with tagDotReplacement
path: internal/storage/v2/elasticsearch/tracestore/tag_appender.go
user: login: Manik2708
body: We can seperate the tag conversion and materialization once the confusion stated in https://github.com/jaegertracing/jaeger/pull/6946#issuecomment-2781666003 is resolved! If we have to move by accepting `any` value (which might lead to "1234" instead of 1234 when spans are converted to OTEL traces) then we can do it but if not (means we have to follow the v1 constraint) then we have to differentiate that is we should be materializing along with conversion otherwise every value in tag map will be a string or we have to do an exra conversion to convert them to their original type!
created_at: 2025-04-06T21:09:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+{
+  "resourceSpans": [
+    {
+      "resource": {
+        "attributes": [
+          {
+            "key": "sdk.version",
+            "value": {
+              "bytesValue": "AAAwOQ=="
+            }
+          },
+          {
+            "key": "sdk.version.1",
+            "value": {
+              "stringValue": "1.2.2"
+            }
+          },
+          {
+            "key": "service.name",
+            "value": {
+              "stringValue": "service-x"
+            }
+          }
+        ]
+      },
+      "scopeSpans": [
+        {
+          "scope": {
+            "name": "testing-library",
+            "version": "1.1.1"
+          },
+          "spans": [
+            {
+              "traceId": "00000000000000010000000000000000",
+              "spanId": "0000000000000002",
+              "parentSpanId": "0000000000000003",
+              "flags": 1,
+              "name": "test-general-conversion",
+              "startTimeUnixNano": "1485467191639875000",
+              "endTimeUnixNano": "1485467191639880000",
+              "attributes": [
+                {
+                  "key": "blob",
+                  "value": {
+                    "bytesValue": "AAAwOQ=="
+                  }
+                },
+                {
+                  "key": "peer.ipv4",
+                  "value": {
+                    "doubleValue": 23456
path: internal/storage/v2/elasticsearch/tracestore/fixtures/otel_traces_02.json
user: login: Manik2708
body: I can't fix this! When it is a slice, I could check whether the type is `int64` or `float64` but here in map I can't find a way to check whether the number is float or int (unmarshaller converts every number to float64)
created_at: 2025-04-11T17:43:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: how does this not lead to lossy conversion? You said that when reading a number into JSON it always comes back as either float or json.Number. So if the original input tag is FLOAT with value `42.0`, the logic below may return either {INT, 42} or {FLOAT, 42}.
created_at: 2025-04-11T19:36:28Z
author_association: MEMBER
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: Can't understand! The problem is: while decoding, go json decoder converts all the numbers to `float64` and while fetching results from ES, numbers are fetched as in form of `json.Number` (this I got to know from integration tests). So if input value tag value is FLOAT, it will fetched as float in both cases (decoding or ES). How it can return INT?
created_at: 2025-04-12T04:50:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: What if it was INT in the original trace data?you just said it will be returned as float in the reverse fromdb translation, that's a data loss. Do we live with it or can we preserve the type?
created_at: 2025-04-12T14:07:50Z
author_association: MEMBER
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: I don't think we can move to 100% data type protection. I feel this same was happening even in v1 as I couldn't find any snapshot test which is validating this! I thought of various ideas but all are leading to data loss in one form or another. One of the example is: I thought of saving integers in form of string and then convert it back to int but the the problem is: What if it was actually saved as string ("1234" not 1234)? It will still be converted to 1234 that is string type is lost! The problem is: We will need to somehow save the type in map as this same problem was fixed in the previous PR by validating type from `ValueType`
created_at: 2025-04-12T14:19:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: Also can this case be ever possible when we are unmarshaling the span but that span is not from ES, I think never because this issue is happening only when we are manually decoding span from fixtures as ES gives number in form of `json.Number` which is handled correctly!
created_at: 2025-04-12T14:23:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: Ok, got it! In the reader the spans are unmarshalled by telling the decoder explicitly to handle numbers as in `json.Number` as in here https://github.com/jaegertracing/jaeger/blob/08be26536479102e1de736710a32ad0de3ca1be5/internal/storage/v1/elasticsearch/spanstore/reader.go#L267
We should do the same in tests also!
created_at: 2025-04-12T14:38:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,14 +100,18 @@ func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	} else {
 		attrs.EnsureCapacity(len(tags))
 	}
+	tags = f.mergeTagMapAndTagSlice(tags, tag)
 	dbTagsToAttributes(tags, attrs)
 }
 
-func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
+func (f *fromDBModel) dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
 	for i := range dbSpans {
 		span := &dbSpans[i]
+		tags := f.mergeTagMapAndTagSlice(span.Tags, span.Tag)
+		span.Tag = nil
+		span.Tags = tags
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: it is a very bad practice to modify input structure in a translation function. If we were doing it in another language like C++ the input would've been declared `const`.
created_at: 2025-04-13T16:17:04Z
author_association: MEMBER
diff_hunk: @@ -52,14 +100,18 @@ func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	} else {
 		attrs.EnsureCapacity(len(tags))
 	}
+	tags = f.mergeTagMapAndTagSlice(tags, tag)
 	dbTagsToAttributes(tags, attrs)
 }
 
-func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
+func (f *fromDBModel) dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
 	for i := range dbSpans {
 		span := &dbSpans[i]
+		tags := f.mergeTagMapAndTagSlice(span.Tags, span.Tag)
+		span.Tag = nil
+		span.Tags = tags
 		resourceSpan := resourceSpans.AppendEmpty()
-		dbProcessToResource(span.Process, resourceSpan.Resource())
+		f.dbProcessToResource(span.Process, resourceSpan.Resource())
 
 		scopeSpans := resourceSpan.ScopeSpans()
 		scopeSpan := scopeSpans.AppendEmpty()
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: > dbSpanToScope(span, scopeSpan)

this is misleading - you don't need the `span` object to populate a `scopeSpan`, you only need to have a combine map of tags.
created_at: 2025-04-13T16:18:03Z
author_association: MEMBER
diff_hunk: @@ -52,14 +100,18 @@ func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	} else {
 		attrs.EnsureCapacity(len(tags))
 	}
+	tags = f.mergeTagMapAndTagSlice(tags, tag)
 	dbTagsToAttributes(tags, attrs)
 }
 
-func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
+func (f *fromDBModel) dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
 	for i := range dbSpans {
 		span := &dbSpans[i]
+		tags := f.mergeTagMapAndTagSlice(span.Tags, span.Tag)
+		span.Tag = nil
+		span.Tags = tags
 		resourceSpan := resourceSpans.AppendEmpty()
-		dbProcessToResource(span.Process, resourceSpan.Resource())
+		f.dbProcessToResource(span.Process, resourceSpan.Resource())
 
 		scopeSpans := resourceSpan.ScopeSpans()
 		scopeSpan := scopeSpans.AppendEmpty()
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: ```suggestion
		scopeSpan := resourceSpan.ScopeSpans().AppendEmpty()
```
created_at: 2025-04-13T16:21:13Z
author_association: MEMBER
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
+	switch val := v.(type) {
+	case int64:
+		dTag.Type = dbmodel.Int64Type
+	case float64:
+		dTag.Type = dbmodel.Float64Type
+	case bool:
+		dTag.Type = dbmodel.BoolType
+	case string:
+		dTag.Type = dbmodel.StringType
+	case json.Number:
+		n, err := val.Int64()
+		if err == nil {
+			dTag.Type = dbmodel.Int64Type
+			dTag.Value = n
+		} else {
+			f, err := val.Float64()
+			if err == nil {
+				dTag.Type = dbmodel.Float64Type
+				dTag.Value = f
+			}
+		}
+	default:
+		dTag.Type = ""
+		dTag.Value = "invalid type"
+	}
+	return dTag
+}
+
+func (f *fromDBModel) mergeTagMapAndTagSlice(kvs []dbmodel.KeyValue, tagMap map[string]any) []dbmodel.KeyValue {
+	if tagMap == nil {
+		return kvs
+	}
+	convertedKvs := make([]dbmodel.KeyValue, len(kvs), len(kvs)+len(tagMap))
+	copy(convertedKvs, kvs)
+	for k, v := range tagMap {
+		convertedKvs = append(convertedKvs, f.convertTagField(k, v))
+	}
+	return convertedKvs
+}
+
+func (f *fromDBModel) dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	serviceName := process.ServiceName
 	tags := process.Tags
+	tag := process.Tag
 	if serviceName == "" && tags == nil {
 		return
 	}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: remove this
created_at: 2025-04-13T16:22:34Z
author_association: MEMBER
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
+	switch val := v.(type) {
+	case int64:
+		dTag.Type = dbmodel.Int64Type
+	case float64:
+		dTag.Type = dbmodel.Float64Type
+	case bool:
+		dTag.Type = dbmodel.BoolType
+	case string:
+		dTag.Type = dbmodel.StringType
+	case json.Number:
+		n, err := val.Int64()
+		if err == nil {
+			dTag.Type = dbmodel.Int64Type
+			dTag.Value = n
+		} else {
+			f, err := val.Float64()
+			if err == nil {
+				dTag.Type = dbmodel.Float64Type
+				dTag.Value = f
+			}
+		}
+	default:
+		dTag.Type = ""
+		dTag.Value = "invalid type"
+	}
+	return dTag
+}
+
+func (f *fromDBModel) mergeTagMapAndTagSlice(kvs []dbmodel.KeyValue, tagMap map[string]any) []dbmodel.KeyValue {
+	if tagMap == nil {
+		return kvs
+	}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: is it `nil` you are interested in or `len() == 0`?
created_at: 2025-04-13T16:23:32Z
author_association: MEMBER
diff_hunk: @@ -658,6 +683,89 @@ func getDbSpanIdFromByteArray(arr [8]byte) dbmodel.SpanID {
 	return dbmodel.SpanID(hex.EncodeToString(arr[:]))
 }
 
+func Test_MergeTagMapAndTagSlice(t *testing.T) {
+	tests := []struct {
+		name       string
+		tagMap     map[string]any
+		expectedKv dbmodel.KeyValue
+	}{
+		{
+			name: "int64",
+			tagMap: map[string]any{
+				"int64": int64(1),
+			},
+			expectedKv: dbmodel.KeyValue{
+				Key:   "int64",
+				Type:  dbmodel.Int64Type,
+				Value: int64(1),
+			},
+		},
+		{
+			name: "float64",
+			tagMap: map[string]any{
+				"float64": float64(1),
+			},
+			expectedKv: dbmodel.KeyValue{
+				Key:   "float64",
+				Type:  dbmodel.Float64Type,
+				Value: float64(1),
+			},
+		},
+		{
+			name: "bool",
+			tagMap: map[string]any{
+				"bool": true,
+			},
+			expectedKv: dbmodel.KeyValue{
+				Key:   "bool",
+				Type:  dbmodel.BoolType,
+				Value: true,
+			},
+		},
+		{
+			name: "binary",
+			tagMap: map[string]any{
+				"binary": []byte{0x01, 0x02, 0x03},
+			},
+			expectedKv: dbmodel.KeyValue{
+				Key:   "binary",
+				Type:  "",
+				Value: "invalid type",
+			},
+		},
+		{
+			name: "jsonNumber/int",
+			tagMap: map[string]any{
+				"json#number#int": json.Number("1"),
+			},
+			expectedKv: dbmodel.KeyValue{
+				Key:   "json.number.int",
+				Type:  dbmodel.Int64Type,
+				Value: int64(1),
+			},
+		},
+		{
+			name: "jsonNumber/float",
+			tagMap: map[string]any{
+				"json#number#float": json.Number("1.23"),
+			},
+			expectedKv: dbmodel.KeyValue{
+				Key:   "json.number.float",
+				Type:  dbmodel.Float64Type,
+				Value: 1.23,
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			fd := &fromDBModel{tagDotReplacement: "#"}
+			actual := fd.mergeTagMapAndTagSlice([]dbmodel.KeyValue{}, test.tagMap)
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel_test.go
user: login: yurishkuro
body: pass something as the first argument, not an empty slice
created_at: 2025-04-13T16:26:44Z
author_association: MEMBER
diff_hunk: @@ -29,7 +30,12 @@ const (
 
 // ToDBModel translates internal trace data into the DB Spans.
 // Returns slice of translated DB Spans and error if translation failed.
-func ToDBModel(td ptrace.Traces) []dbmodel.Span {
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
+	tags := map[string]bool{}
+	for _, k := range tagKeysAsFields {
+		tags[k] = true
+	}
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: this loop can be done inside newToDBModel()
created_at: 2025-04-13T16:28:29Z
author_association: MEMBER
diff_hunk: @@ -29,7 +30,12 @@ const (
 
 // ToDBModel translates internal trace data into the DB Spans.
 // Returns slice of translated DB Spans and error if translation failed.
-func ToDBModel(td ptrace.Traces) []dbmodel.Span {
+func ToDBModel(td ptrace.Traces, allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) []dbmodel.Span {
+	tags := map[string]bool{}
+	for _, k := range tagKeysAsFields {
+		tags[k] = true
+	}
+	toDB := newToDBModel(allTagsAsObject, tags, tagDotReplacement)
 	resourceSpans := td.ResourceSpans()
 
 	if resourceSpans.Len() == 0 {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: if you must do this, shouldn't it be done at the very beginning of the function? And the 2nd question - why must you do this check in the first place? Isn't zero slice going to produce the exact same result anyway? This looks like a shortcut that provides no benefits (and in prod it's very unlikely to have a zero-length input anyway, so it's not any sensible performance saver).
created_at: 2025-04-13T16:30:05Z
author_association: MEMBER
diff_hunk: @@ -48,31 +54,64 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+type toDBModel struct {
+	allTagsAsFields   bool
+	tagKeysAsFields   map[string]bool
+	tagDotReplacement string
+}
+
+func newToDBModel(allTagsAsFields bool, tagKeysAsFields map[string]bool, tagDotReplacement string) *toDBModel {
+	return &toDBModel{
+		allTagsAsFields:   allTagsAsFields,
+		tagKeysAsFields:   tagKeysAsFields,
+		tagDotReplacement: tagDotReplacement,
+	}
+}
+
+func (t *toDBModel) embedTagDotReplacement(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: what is the primary responsibility of this function? Try to write a sentence (in comment) and then choose a concise name that reflects that description.
created_at: 2025-04-13T16:33:30Z
author_association: MEMBER
diff_hunk: @@ -48,31 +54,64 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+type toDBModel struct {
+	allTagsAsFields   bool
+	tagKeysAsFields   map[string]bool
+	tagDotReplacement string
+}
+
+func newToDBModel(allTagsAsFields bool, tagKeysAsFields map[string]bool, tagDotReplacement string) *toDBModel {
+	return &toDBModel{
+		allTagsAsFields:   allTagsAsFields,
+		tagKeysAsFields:   tagKeysAsFields,
+		tagDotReplacement: tagDotReplacement,
+	}
+}
+
+func (t *toDBModel) embedTagDotReplacement(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
+	var tagsMap map[string]any
+	var kvs []dbmodel.KeyValue
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: ```suggestion
	kvs := []dbmodel.KeyValue{}
```
created_at: 2025-04-13T16:42:11Z
author_association: MEMBER
diff_hunk: @@ -48,31 +54,64 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+type toDBModel struct {
+	allTagsAsFields   bool
+	tagKeysAsFields   map[string]bool
+	tagDotReplacement string
+}
+
+func newToDBModel(allTagsAsFields bool, tagKeysAsFields map[string]bool, tagDotReplacement string) *toDBModel {
+	return &toDBModel{
+		allTagsAsFields:   allTagsAsFields,
+		tagKeysAsFields:   tagKeysAsFields,
+		tagDotReplacement: tagDotReplacement,
+	}
+}
+
+func (t *toDBModel) embedTagDotReplacement(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
+	var tagsMap map[string]any
+	var kvs []dbmodel.KeyValue
+	for _, kv := range keyValues {
+		if kv.Type != dbmodel.BinaryType && (t.allTagsAsFields || t.tagKeysAsFields[kv.Key]) {
+			if tagsMap == nil {
+				tagsMap = map[string]any{}
+			}
+			tagsMap[strings.ReplaceAll(kv.Key, ".", t.tagDotReplacement)] = kv.Value
+		} else {
+			kvs = append(kvs, kv)
+		}
+	}
+	if kvs == nil {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: why not just declare it as empty slice above?
created_at: 2025-04-13T16:42:35Z
author_association: MEMBER
diff_hunk: @@ -125,10 +166,12 @@ func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
 	return tag
 }
 
-func spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
+func (t *toDBModel) spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: ```suggestion
func (t *toDBModel) spanToDbSpan(span ptrace.Span, scopeTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
```
created_at: 2025-04-13T16:44:07Z
author_association: MEMBER
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: utility function should not be at the top of the file, it breaks the flow of logic
created_at: 2025-04-13T16:45:06Z
author_association: MEMBER
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
+	switch val := v.(type) {
+	case int64:
+		dTag.Type = dbmodel.Int64Type
+	case float64:
+		dTag.Type = dbmodel.Float64Type
+	case bool:
+		dTag.Type = dbmodel.BoolType
+	case string:
+		dTag.Type = dbmodel.StringType
+	case json.Number:
+		n, err := val.Int64()
+		if err == nil {
+			dTag.Type = dbmodel.Int64Type
+			dTag.Value = n
+		} else {
+			f, err := val.Float64()
+			if err == nil {
+				dTag.Type = dbmodel.Float64Type
+				dTag.Value = f
+			}
+		}
+	default:
+		dTag.Type = ""
+		dTag.Value = "invalid type"
+	}
+	return dTag
+}
+
+func (f *fromDBModel) mergeTagMapAndTagSlice(kvs []dbmodel.KeyValue, tagMap map[string]any) []dbmodel.KeyValue {
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: utility function should not be at the top of the file, it breaks the flow of logic
created_at: 2025-04-13T16:45:26Z
author_association: MEMBER
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
+	switch val := v.(type) {
+	case int64:
+		dTag.Type = dbmodel.Int64Type
+	case float64:
+		dTag.Type = dbmodel.Float64Type
+	case bool:
+		dTag.Type = dbmodel.BoolType
+	case string:
+		dTag.Type = dbmodel.StringType
+	case json.Number:
+		n, err := val.Int64()
+		if err == nil {
+			dTag.Type = dbmodel.Int64Type
+			dTag.Value = n
+		} else {
+			f, err := val.Float64()
+			if err == nil {
+				dTag.Type = dbmodel.Float64Type
+				dTag.Value = f
+			}
+		}
+	default:
+		dTag.Type = ""
+		dTag.Value = "invalid type"
+	}
+	return dTag
+}
+
+func (f *fromDBModel) mergeTagMapAndTagSlice(kvs []dbmodel.KeyValue, tagMap map[string]any) []dbmodel.KeyValue {
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: function name needs to reflect the business function, not restate its signature. What it's doing is merging nested and materialized tags.
```suggestion
func (f *fromDBModel) mergeNestedAndElevatedTags(kvs []dbmodel.KeyValue, tagMap map[string]any) []dbmodel.KeyValue {
```
created_at: 2025-04-13T16:47:44Z
author_association: MEMBER
diff_hunk: @@ -52,14 +100,18 @@ func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	} else {
 		attrs.EnsureCapacity(len(tags))
 	}
+	tags = f.mergeTagMapAndTagSlice(tags, tag)
 	dbTagsToAttributes(tags, attrs)
 }
 
-func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
+func (f *fromDBModel) dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
 	for i := range dbSpans {
 		span := &dbSpans[i]
+		tags := f.mergeTagMapAndTagSlice(span.Tags, span.Tag)
+		span.Tag = nil
+		span.Tags = tags
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: I also thought the same but my aim was not to change the structure a lot for `tagDotReplacement` as without modifying the span we had to pass the updated tags as a parameter into the subsequent method and only this updated tags should be used. How should we proceed with this? Should we change the overall structure? Also something similar is hapenning here: https://github.com/jaegertracing/jaeger/blob/f8c40ffb8d97ef26ca07a7598fe9058b3bd491d2/internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go#L449-L459 Should we change this also? (This is directly taken from OTEL code)
created_at: 2025-04-13T17:33:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,22 +25,70 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
+	switch val := v.(type) {
+	case int64:
+		dTag.Type = dbmodel.Int64Type
+	case float64:
+		dTag.Type = dbmodel.Float64Type
+	case bool:
+		dTag.Type = dbmodel.BoolType
+	case string:
+		dTag.Type = dbmodel.StringType
+	case json.Number:
+		n, err := val.Int64()
+		if err == nil {
+			dTag.Type = dbmodel.Int64Type
+			dTag.Value = n
+		} else {
+			f, err := val.Float64()
+			if err == nil {
+				dTag.Type = dbmodel.Float64Type
+				dTag.Value = f
+			}
+		}
+	default:
+		dTag.Type = ""
+		dTag.Value = "invalid type"
+	}
+	return dTag
+}
+
+func (f *fromDBModel) mergeTagMapAndTagSlice(kvs []dbmodel.KeyValue, tagMap map[string]any) []dbmodel.KeyValue {
+	if tagMap == nil {
+		return kvs
+	}
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: I think we should do both!
created_at: 2025-04-13T17:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,14 +100,18 @@ func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	} else {
 		attrs.EnsureCapacity(len(tags))
 	}
+	tags = f.mergeTagMapAndTagSlice(tags, tag)
 	dbTagsToAttributes(tags, attrs)
 }
 
-func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
+func (f *fromDBModel) dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
 	for i := range dbSpans {
 		span := &dbSpans[i]
+		tags := f.mergeTagMapAndTagSlice(span.Tags, span.Tag)
+		span.Tag = nil
+		span.Tags = tags
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: we, we need to change such that we don't modify the input. You can either find other ways to pass the tags, or you can clone the span and then modify the copy.
created_at: 2025-04-13T17:43:17Z
author_association: MEMBER
diff_hunk: @@ -52,14 +100,18 @@ func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	} else {
 		attrs.EnsureCapacity(len(tags))
 	}
+	tags = f.mergeTagMapAndTagSlice(tags, tag)
 	dbTagsToAttributes(tags, attrs)
 }
 
-func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
+func (f *fromDBModel) dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
 	for i := range dbSpans {
 		span := &dbSpans[i]
+		tags := f.mergeTagMapAndTagSlice(span.Tags, span.Tag)
+		span.Tag = nil
+		span.Tags = tags
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: As per me clonning will be better! Is there any performance issue which can rise? because then I might have to find ways to pass it down the other functions (updated tag)
created_at: 2025-04-13T17:45:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -437,25 +482,30 @@ func getTraceStateFromAttrs(attrs pcommon.Map) string {
 	return traceState
 }
 
-func dbSpanToScope(span *dbmodel.Span, scopeSpan ptrace.ScopeSpans) {
-	if libraryName, ok := getAndDeleteTag(span, conventions.AttributeOtelScopeName); ok {
+func dbSpanToScope(tags []dbmodel.KeyValue, scopeSpan ptrace.ScopeSpans) []dbmodel.KeyValue {
+	updatedTags := tags
+	if tagsWithoutScopeName, libraryName, ok := getAndDeleteTag(tags, conventions.AttributeOtelScopeName); ok {
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: Manik2708
body: Does this approach seem fine?
created_at: 2025-04-13T18:37:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,31 +46,66 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+type toDBModel struct {
+	allTagsAsFields   bool
+	tagKeysAsFields   map[string]bool
+	tagDotReplacement string
+}
+
+func newToDBModel(allTagsAsFields bool, tagKeysAsFields []string, tagDotReplacement string) *toDBModel {
+	tags := map[string]bool{}
+	for _, k := range tagKeysAsFields {
+		tags[k] = true
+	}
+	return &toDBModel{
+		allTagsAsFields:   allTagsAsFields,
+		tagKeysAsFields:   tags,
+		tagDotReplacement: tagDotReplacement,
+	}
+}
+
+// splitTagsToNestedAndElevatedTags split the tags into nested and elevated tags by replacing the dot in the keys of elevated tags by tagDotReplacement
+func (t *toDBModel) splitTagsToNestedAndElevatedTags(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: Does this name look fine?
created_at: 2025-04-13T18:38:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,56 +25,101 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	serviceName := process.ServiceName
 	tags := process.Tags
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: use functional names, `nestedTags` and `elevatedTags`
created_at: 2025-04-13T21:52:54Z
author_association: MEMBER
diff_hunk: @@ -25,56 +25,101 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	serviceName := process.ServiceName
 	tags := process.Tags
-	if serviceName == "" && tags == nil {
-		return
-	}
-
+	tag := process.Tag
 	attrs := resource.Attributes()
 	if serviceName != "" && serviceName != noServiceName {
 		attrs.EnsureCapacity(len(tags) + 1)
 		attrs.PutStr(conventions.AttributeServiceName, serviceName)
 	} else {
 		attrs.EnsureCapacity(len(tags))
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: ```suggestion
		attrs.EnsureCapacity(len(tags) + len(tag) + 1)
		attrs.PutStr(conventions.AttributeServiceName, serviceName)
	} else {
		attrs.EnsureCapacity(len(tags) + len(tag))
```
created_at: 2025-04-13T21:53:30Z
author_association: MEMBER
diff_hunk: @@ -25,56 +25,101 @@ import (
 var errType = errors.New("invalid type")
 
 // FromDBModel converts multiple ES DB Spans to internal traces
-func FromDBModel(spans []dbmodel.Span) (ptrace.Traces, error) {
+func FromDBModel(spans []dbmodel.Span, tagDotReplacement string) (ptrace.Traces, error) {
 	traceData := ptrace.NewTraces()
 	if len(spans) == 0 {
 		return traceData, nil
 	}
-
+	fromDb := &fromDBModel{tagDotReplacement: tagDotReplacement}
 	resourceSpans := traceData.ResourceSpans()
 	resourceSpans.EnsureCapacity(len(spans))
-	err := dbSpansToSpans(spans, resourceSpans)
+	err := fromDb.dbSpansToSpans(spans, resourceSpans)
 
 	return traceData, err
 }
 
-func dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
+type fromDBModel struct {
+	tagDotReplacement string
+}
+
+func (f *fromDBModel) dbProcessToResource(process dbmodel.Process, resource pcommon.Resource) {
 	serviceName := process.ServiceName
 	tags := process.Tags
-	if serviceName == "" && tags == nil {
-		return
-	}
-
+	tag := process.Tag
 	attrs := resource.Attributes()
 	if serviceName != "" && serviceName != noServiceName {
 		attrs.EnsureCapacity(len(tags) + 1)
 		attrs.PutStr(conventions.AttributeServiceName, serviceName)
 	} else {
 		attrs.EnsureCapacity(len(tags))
 	}
+	tags = f.mergeNestedAndElevatedTags(tags, tag)
 	dbTagsToAttributes(tags, attrs)
 }
 
-func dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
+func (f *fromDBModel) dbSpansToSpans(dbSpans []dbmodel.Span, resourceSpans ptrace.ResourceSpansSlice) error {
 	for i := range dbSpans {
 		span := &dbSpans[i]
+		tags := f.mergeNestedAndElevatedTags(span.Tags, span.Tag)
 		resourceSpan := resourceSpans.AppendEmpty()
-		dbProcessToResource(span.Process, resourceSpan.Resource())
+		f.dbProcessToResource(span.Process, resourceSpan.Resource())
 
-		scopeSpans := resourceSpan.ScopeSpans()
-		scopeSpan := scopeSpans.AppendEmpty()
-		dbSpanToScope(span, scopeSpan)
+		scopeSpan := resourceSpan.ScopeSpans().AppendEmpty()
+		tags = dbSpanToScope(tags, scopeSpan)
 
 		// TODO there should be no errors returned from translation from db model
-		err := dbSpanToSpan(span, scopeSpan.Spans().AppendEmpty())
+		err := dbSpanToSpan(span, tags, scopeSpan.Spans().AppendEmpty())
 		if err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func dbSpanToSpan(dbSpan *dbmodel.Span, span ptrace.Span) error {
+func (f *fromDBModel) convertTagField(k string, v any) dbmodel.KeyValue {
+	dKey := strings.ReplaceAll(k, f.tagDotReplacement, ".")
+	dTag := dbmodel.KeyValue{Key: dKey, Value: v}
+	switch val := v.(type) {
+	case int64:
+		dTag.Type = dbmodel.Int64Type
+	case float64:
+		dTag.Type = dbmodel.Float64Type
+	case bool:
+		dTag.Type = dbmodel.BoolType
+	case string:
+		dTag.Type = dbmodel.StringType
+	case json.Number:
+		n, err := val.Int64()
+		if err == nil {
+			dTag.Type = dbmodel.Int64Type
+			dTag.Value = n
+		} else {
+			f, err := val.Float64()
+			if err == nil {
+				dTag.Type = dbmodel.Float64Type
+				dTag.Value = f
+			}
+		}
+	default:
+		dTag.Type = ""
+		dTag.Value = "invalid type"
+	}
+	return dTag
+}
+
+func (f *fromDBModel) mergeNestedAndElevatedTags(kvs []dbmodel.KeyValue, tagMap map[string]any) []dbmodel.KeyValue {
+	if tagMap == nil || len(tagMap) == 0 {
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: ```suggestion
	if len(tagMap) == 0 {
```
created_at: 2025-04-13T21:55:46Z
author_association: MEMBER
diff_hunk: @@ -437,25 +482,30 @@ func getTraceStateFromAttrs(attrs pcommon.Map) string {
 	return traceState
 }
 
-func dbSpanToScope(span *dbmodel.Span, scopeSpan ptrace.ScopeSpans) {
-	if libraryName, ok := getAndDeleteTag(span, conventions.AttributeOtelScopeName); ok {
+func dbSpanToScope(tags []dbmodel.KeyValue, scopeSpan ptrace.ScopeSpans) []dbmodel.KeyValue {
path: internal/storage/v2/elasticsearch/tracestore/from_dbmodel.go
user: login: yurishkuro
body: function name doesn't match the signature
created_at: 2025-04-13T21:57:30Z
author_association: MEMBER
diff_hunk: @@ -48,31 +46,66 @@ func ToDBModel(td ptrace.Traces) []dbmodel.Span {
 	return batches
 }
 
-func resourceSpansToDbSpans(resourceSpans ptrace.ResourceSpans) []dbmodel.Span {
+type toDBModel struct {
+	allTagsAsFields   bool
+	tagKeysAsFields   map[string]bool
+	tagDotReplacement string
+}
+
+func newToDBModel(allTagsAsFields bool, tagKeysAsFields []string, tagDotReplacement string) *toDBModel {
+	tags := map[string]bool{}
+	for _, k := range tagKeysAsFields {
+		tags[k] = true
+	}
+	return &toDBModel{
+		allTagsAsFields:   allTagsAsFields,
+		tagKeysAsFields:   tags,
+		tagDotReplacement: tagDotReplacement,
+	}
+}
+
+// splitTagsToNestedAndElevatedTags split the tags into nested and elevated tags by replacing the dot in the keys of elevated tags by tagDotReplacement
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: > split ... by replacing the dot

this isn't the main functionality of the method, it's a minor detail.

```suggestion
// splitTagsToNestedAndElevatedTags splits the tags into nested and elevated tags
// according to `allTagsAsFields` and `tagKeysAsFields` settings. It also replaces
// dots in the elevated tag keys with a placeholder.
```
created_at: 2025-04-13T22:03:28Z
author_association: MEMBER
diff_hunk: @@ -87,7 +120,9 @@ func resourceToDbProcess(resource pcommon.Resource) dbmodel.Process {
 		}
 		tags = append(tags, attributeToDbTag(key, attr))
 	}
+	tags, tagMap := t.splitTagsToNestedAndElevatedTags(tags)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: ```suggestion
	nestedTags, elevatedTags := t.splitTagsToNestedAndElevatedTags(tags)
```
created_at: 2025-04-13T22:04:40Z
author_association: MEMBER
diff_hunk: @@ -125,10 +160,12 @@ func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
 	return tag
 }
 
-func spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
+func (t *toDBModel) spanToDbSpan(span ptrace.Span, scopeTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: ```suggestion
func (t *toDBModel) spanToDbSpan(span ptrace.Span, scope pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
```
created_at: 2025-04-13T22:05:19Z
author_association: MEMBER
diff_hunk: @@ -125,10 +160,12 @@ func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
 	return tag
 }
 
-func spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
+func (t *toDBModel) spanToDbSpan(span ptrace.Span, scopeTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
 	traceID := dbmodel.TraceID(span.TraceID().String())
 	parentSpanID := dbmodel.SpanID(span.ParentSpanID().String())
 	startTime := span.StartTimestamp().AsTime()
+	tags := getDbSpanTags(span, scopeTags)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: getDbSpanTags this function looks broken to me. Do you have a unit test where a Scope has another attribute besides scope name / version? How would you be able to perform lossless reverse transform if you dump all scope tags into span tags?

I would argue you need to have a `Span.Scope` similar to `Span.Process` in the dbSpan.
created_at: 2025-04-13T22:07:18Z
author_association: MEMBER
diff_hunk: @@ -125,10 +160,12 @@ func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
 	return tag
 }
 
-func spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
+func (t *toDBModel) spanToDbSpan(span ptrace.Span, scopeTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
 	traceID := dbmodel.TraceID(span.TraceID().String())
 	parentSpanID := dbmodel.SpanID(span.ParentSpanID().String())
 	startTime := span.StartTimestamp().AsTime()
+	tags := getDbSpanTags(span, scopeTags)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: Manik2708
body: How will that ensure backward compatibility? As searching is also made on tags, so what if query tags contain scope based tags, should we then search both as for now (for the old data not containing scope)?
created_at: 2025-04-14T02:04:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,10 +160,12 @@ func attributeToDbTag(key string, attr pcommon.Value) dbmodel.KeyValue {
 	return tag
 }
 
-func spanToDbSpan(span ptrace.Span, libraryTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
+func (t *toDBModel) spanToDbSpan(span ptrace.Span, scopeTags pcommon.InstrumentationScope, process dbmodel.Process) dbmodel.Span {
 	traceID := dbmodel.TraceID(span.TraceID().String())
 	parentSpanID := dbmodel.SpanID(span.ParentSpanID().String())
 	startTime := span.StartTimestamp().AsTime()
+	tags := getDbSpanTags(span, scopeTags)
path: internal/storage/v2/elasticsearch/tracestore/to_dbmodel.go
user: login: yurishkuro
body: This should work similar to how Process tags work. From the UI you don't specify which part of the span to search, so all tags are searched across Span and Span.Process, and in this new case across Span.Scope.
created_at: 2025-04-14T16:45:44Z
author_association: MEMBER
diff_hunk: @@ -148,3 +168,28 @@ func (s *SpanWriter) writeService(indexName string, jsonSpan *dbmodel.Span) {
 func (s *SpanWriter) writeSpan(indexName string, jsonSpan *dbmodel.Span) {
 	s.client().Index().Index(indexName).Type(spanType).BodyJson(&jsonSpan).Add()
 }
+
+func convertKeyValuesString(keyValues []dbmodel.KeyValue, allTagsAsFields bool, tagKeysAsFields map[string]bool, tagDotReplacement string) ([]dbmodel.KeyValue, map[string]any) {
+	var tagsMap map[string]any
+	var kvs []dbmodel.KeyValue
+	for _, kv := range keyValues {
+		if kv.Type != dbmodel.BinaryType && (allTagsAsFields || tagKeysAsFields[kv.Key]) {
+			if tagsMap == nil {
+				tagsMap = map[string]any{}
+			}
+			tagsMap[strings.ReplaceAll(kv.Key, ".", tagDotReplacement)] = kv.Value
+		} else {
+			kvs = append(kvs, kv)
+		}
+	}
+	if kvs == nil {
+		kvs = make([]dbmodel.KeyValue, 0)
+	}
+	return kvs, tagsMap
+}
+
+func getKeyValueConverter(p SpanWriterParams, tagKeysAsFields map[string]bool) keyValueConverter {
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: yurishkuro
body: what is the need for this indirection?
created_at: 2025-04-22T13:25:19Z
author_association: MEMBER
diff_hunk: @@ -11,30 +11,14 @@ import (
 	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
 )
 
-// NewFromDomain creates FromDomain used to convert model span to db span
-func NewFromDomain(allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) FromDomain {
-	tags := map[string]bool{}
-	for _, k := range tagKeysAsFields {
-		tags[k] = true
-	}
-	return FromDomain{allTagsAsFields: allTagsAsObject, tagKeysAsFields: tags, tagDotReplacement: tagDotReplacement}
-}
-
-// FromDomain is used to convert model span to db span
-type FromDomain struct {
-	allTagsAsFields   bool
-	tagKeysAsFields   map[string]bool
-	tagDotReplacement string
-}
-
 // FromDomainEmbedProcess converts model.Span into json.Span format.
 // This format includes a ParentSpanID and an embedded Process.
-func (fd FromDomain) FromDomainEmbedProcess(span *model.Span) *dbmodel.Span {
-	return fd.convertSpanEmbedProcess(span)
+func FromDomainEmbedProcess(span *model.Span) *dbmodel.Span {
+	return convertSpanEmbedProcess(span)
path: internal/storage/v1/elasticsearch/spanstore/from_domain.go
user: login: yurishkuro
body: does convertSpanEmbedProcess need to be a separate function? is it reused?
created_at: 2025-04-22T13:27:49Z
author_association: MEMBER
diff_hunk: @@ -62,6 +64,8 @@ type SpanWriterParams struct {
 	ServiceCacheTTL     time.Duration
 }
 
+type keyValueConverter func([]dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any)
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: yurishkuro
body: why is this not simply a method on the struct? I don't see any test substituting this function.
created_at: 2025-04-22T13:29:12Z
author_association: MEMBER
diff_hunk: @@ -148,3 +168,28 @@ func (s *SpanWriter) writeService(indexName string, jsonSpan *dbmodel.Span) {
 func (s *SpanWriter) writeSpan(indexName string, jsonSpan *dbmodel.Span) {
 	s.client().Index().Index(indexName).Type(spanType).BodyJson(&jsonSpan).Add()
 }
+
+func convertKeyValuesString(keyValues []dbmodel.KeyValue, allTagsAsFields bool, tagKeysAsFields map[string]bool, tagDotReplacement string) ([]dbmodel.KeyValue, map[string]any) {
+	var tagsMap map[string]any
+	var kvs []dbmodel.KeyValue
+	for _, kv := range keyValues {
+		if kv.Type != dbmodel.BinaryType && (allTagsAsFields || tagKeysAsFields[kv.Key]) {
+			if tagsMap == nil {
+				tagsMap = map[string]any{}
+			}
+			tagsMap[strings.ReplaceAll(kv.Key, ".", tagDotReplacement)] = kv.Value
+		} else {
+			kvs = append(kvs, kv)
+		}
+	}
+	if kvs == nil {
+		kvs = make([]dbmodel.KeyValue, 0)
+	}
+	return kvs, tagsMap
+}
+
+func getKeyValueConverter(p SpanWriterParams, tagKeysAsFields map[string]bool) keyValueConverter {
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: Manik2708
body: We need to access `TagDotReplacement`, `AllTagsKeysAsFields` and `TagKeysAsFields`  which can be accessed only by `SpanWriterParams` but they are not accessible inside `WriterSpan`, therefore this indirection is made.
created_at: 2025-04-22T13:30:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,30 +11,14 @@ import (
 	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
 )
 
-// NewFromDomain creates FromDomain used to convert model span to db span
-func NewFromDomain(allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) FromDomain {
-	tags := map[string]bool{}
-	for _, k := range tagKeysAsFields {
-		tags[k] = true
-	}
-	return FromDomain{allTagsAsFields: allTagsAsObject, tagKeysAsFields: tags, tagDotReplacement: tagDotReplacement}
-}
-
-// FromDomain is used to convert model span to db span
-type FromDomain struct {
-	allTagsAsFields   bool
-	tagKeysAsFields   map[string]bool
-	tagDotReplacement string
-}
-
 // FromDomainEmbedProcess converts model.Span into json.Span format.
 // This format includes a ParentSpanID and an embedded Process.
-func (fd FromDomain) FromDomainEmbedProcess(span *model.Span) *dbmodel.Span {
-	return fd.convertSpanEmbedProcess(span)
+func FromDomainEmbedProcess(span *model.Span) *dbmodel.Span {
+	return convertSpanEmbedProcess(span)
path: internal/storage/v1/elasticsearch/spanstore/from_domain.go
user: login: Manik2708
body: Hm, I just now checked, it's not used anywhere else, so can be removed
created_at: 2025-04-22T13:36:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,8 @@ type SpanWriterParams struct {
 	ServiceCacheTTL     time.Duration
 }
 
+type keyValueConverter func([]dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any)
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: Manik2708
body: For that we might need to make tagDotReplacement parameters a part of the writer, will that be fine?
created_at: 2025-04-22T13:37:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,8 @@ type SpanWriterParams struct {
 	ServiceCacheTTL     time.Duration
 }
 
+type keyValueConverter func([]dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any)
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: yurishkuro
body: it is already a parameter to the writer, you're initializing this function in the constructor `getKeyValueConverter(p, tags)`
created_at: 2025-04-22T13:41:02Z
author_association: MEMBER
diff_hunk: @@ -62,6 +64,8 @@ type SpanWriterParams struct {
 	ServiceCacheTTL     time.Duration
 }
 
+type keyValueConverter func([]dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any)
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: Manik2708
body: But we can't access the params outside of the constructer. They only variables which are part of the writer are: 
```
type SpanWriter struct {
	client func() es.Client
	logger *zap.Logger
	// indexCache       cache.Cache
	serviceWriter     serviceWriter
	spanServiceIndex  spanAndServiceIndexFn
}
```
So how will we access the tagDotReplacement params in a method of the struct? We might have to do this:
```
type SpanWriter struct {
	client func() es.Client
	logger *zap.Logger
	// indexCache       cache.Cache
	serviceWriter     serviceWriter
	spanServiceIndex  spanAndServiceIndexFn
        tagDotReplacement string // Similarly other parameters
}
```
created_at: 2025-04-22T13:47:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,8 @@ type SpanWriterParams struct {
 	ServiceCacheTTL     time.Duration
 }
 
+type keyValueConverter func([]dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any)
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: yurishkuro
body: So let's do that, why is it even a question?
created_at: 2025-04-22T14:18:59Z
author_association: MEMBER
diff_hunk: @@ -62,6 +64,8 @@ type SpanWriterParams struct {
 	ServiceCacheTTL     time.Duration
 }
 
+type keyValueConverter func([]dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any)
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: yurishkuro
body: You can capture the full params struct in the writer.
created_at: 2025-04-22T14:20:56Z
author_association: MEMBER
diff_hunk: @@ -148,3 +170,22 @@ func (s *SpanWriter) writeService(indexName string, jsonSpan *dbmodel.Span) {
 func (s *SpanWriter) writeSpan(indexName string, jsonSpan *dbmodel.Span) {
 	s.client().Index().Index(indexName).Type(spanType).BodyJson(&jsonSpan).Add()
 }
+
+func (s *SpanWriter) convertKeyValuesString(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: yurishkuro
body: ```suggestion
func (s *SpanWriter) splitElevatedTags(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
```
created_at: 2025-04-22T14:35:13Z
author_association: MEMBER
diff_hunk: @@ -11,30 +11,17 @@ import (
 	"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
 )
 
-// NewFromDomain creates FromDomain used to convert model span to db span
-func NewFromDomain(allTagsAsObject bool, tagKeysAsFields []string, tagDotReplacement string) FromDomain {
-	tags := map[string]bool{}
-	for _, k := range tagKeysAsFields {
-		tags[k] = true
-	}
-	return FromDomain{allTagsAsFields: allTagsAsObject, tagKeysAsFields: tags, tagDotReplacement: tagDotReplacement}
-}
-
-// FromDomain is used to convert model span to db span
-type FromDomain struct {
-	allTagsAsFields   bool
-	tagKeysAsFields   map[string]bool
-	tagDotReplacement string
-}
-
 // FromDomainEmbedProcess converts model.Span into json.Span format.
 // This format includes a ParentSpanID and an embedded Process.
-func (fd FromDomain) FromDomainEmbedProcess(span *model.Span) *dbmodel.Span {
-	return fd.convertSpanEmbedProcess(span)
+func FromDomainEmbedProcess(span *model.Span) *dbmodel.Span {
+	s := convertSpanInternal(span)
+	s.Process = convertProcess(span.Process)
+	s.References = convertReferences(span)
+	return &s
 }
 
-func (fd FromDomain) convertSpanInternal(span *model.Span) dbmodel.Span {
path: internal/storage/v1/elasticsearch/spanstore/from_domain.go
user: login: yurishkuro
body: why remove the struct? It gives namespace to functions
created_at: 2025-04-22T16:26:23Z
author_association: MEMBER
diff_hunk: @@ -75,31 +62,23 @@ func (FromDomain) convertRefType(refType model.SpanRefType) dbmodel.ReferenceTyp
 	return dbmodel.ChildOf
 }
 
-func (fd FromDomain) convertKeyValuesString(keyValues model.KeyValues) ([]dbmodel.KeyValue, map[string]any) {
-	var tagsMap map[string]any
+func (fd FromDomain) convertKeyValues(keyValues model.KeyValues) []dbmodel.KeyValue {
 	var kvs []dbmodel.KeyValue
 	for _, kv := range keyValues {
path: internal/storage/v1/elasticsearch/spanstore/from_domain.go
user: login: yurishkuro
body: the size is known upfront now
```suggestion
	kvs := make([]dbmodel.KeyValue, 0, len(keyValues))
	for i := range keyValues {
		kvs[i] = fd.convertKeyValue(keyValues[i])
```
created_at: 2025-04-22T23:14:03Z
author_association: MEMBER
diff_hunk: @@ -148,3 +170,22 @@ func (s *SpanWriter) writeService(indexName string, jsonSpan *dbmodel.Span) {
 func (s *SpanWriter) writeSpan(indexName string, jsonSpan *dbmodel.Span) {
 	s.client().Index().Index(indexName).Type(spanType).BodyJson(&jsonSpan).Add()
 }
+
+func (s *SpanWriter) splitElevatedTags(keyValues []dbmodel.KeyValue) ([]dbmodel.KeyValue, map[string]any) {
+	var tagsMap map[string]any
path: internal/storage/v1/elasticsearch/spanstore/writer.go
user: login: yurishkuro
body: nit: I think we should short circuit here
 ```suggestion
	if (!s.allTagsAsFields && len(s.tagKeysAsFields) == 0) {
		return
	}
	var tagsMap map[string]any
```
created_at: 2025-04-24T14:00:55Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-30T14:05:00Z
body: ## [Codecov](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.01%. Comparing base [(`e22fa5d`)](https://app.codecov.io/gh/jaegertracing/jaeger/commit/e22fa5d4fa0e08efbc2e2d4a1bfba055a1538c7a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) to head [(`19c4772`)](https://app.codecov.io/gh/jaegertracing/jaeger/commit/19c47722f91443a0fc8df753721bcb92f9d1c913?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing).
> Report is 7 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #6946   +/-   ##
=======================================
  Coverage   96.01%   96.01%           
=======================================
  Files         355      355           
  Lines       20993    21012   +19     
=======================================
+ Hits        20156    20175   +19     
  Misses        630      630           
  Partials      207      207           
```

| [Flag](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | Coverage Δ | |
|---|---|---|
| [badger_v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `9.94% <0.00%> (-0.02%)` | :arrow_down: |
| [badger_v2](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.06% <0.00%> (-0.01%)` | :arrow_down: |
| [cassandra-4.x-v1-manual](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `14.96% <0.00%> (-0.03%)` | :arrow_down: |
| [cassandra-4.x-v2-auto](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.05% <0.00%> (-0.01%)` | :arrow_down: |
| [cassandra-4.x-v2-manual](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.05% <0.00%> (-0.01%)` | :arrow_down: |
| [cassandra-5.x-v1-manual](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `14.96% <0.00%> (-0.03%)` | :arrow_down: |
| [cassandra-5.x-v2-auto](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.05% <0.00%> (-0.01%)` | :arrow_down: |
| [cassandra-5.x-v2-manual](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.05% <0.00%> (-0.01%)` | :arrow_down: |
| [elasticsearch-6.x-v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `20.05% <94.73%> (+0.12%)` | :arrow_up: |
| [elasticsearch-7.x-v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `20.13% <94.73%> (+0.12%)` | :arrow_up: |
| [elasticsearch-8.x-v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `20.30% <94.73%> (+0.12%)` | :arrow_up: |
| [elasticsearch-8.x-v2](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.06% <0.00%> (-0.01%)` | :arrow_down: |
| [grpc_v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `11.49% <0.00%> (-0.03%)` | :arrow_down: |
| [grpc_v2](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.06% <0.00%> (-0.01%)` | :arrow_down: |
| [kafka-3.x-v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `10.22% <0.00%> (-0.02%)` | :arrow_down: |
| [kafka-3.x-v2](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.06% <0.00%> (-0.01%)` | :arrow_down: |
| [memory_v2](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.06% <0.00%> (-0.01%)` | :arrow_down: |
| [opensearch-1.x-v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `20.18% <94.73%> (+0.12%)` | :arrow_up: |
| [opensearch-2.x-v1](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `20.18% <94.73%> (+0.12%)` | :arrow_up: |
| [opensearch-2.x-v2](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.06% <0.00%> (-0.01%)` | :arrow_down: |
| [query](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `2.06% <0.00%> (-0.01%)` | :arrow_down: |
| [tailsampling-processor](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `0.56% <0.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing) | `94.79% <98.24%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jaegertracing/jaeger/pull/6946?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jaegertracing).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: Manik2708
created_at: 2025-03-30T14:12:11Z
body: @yurishkuro This is a try to separate tag appending from the `ToDBModel` and embed `tagDotReplacement` in the `ToDBModel`. Please review!
author_association: CONTRIBUTOR
user: login: Manik2708
created_at: 2025-03-31T06:29:32Z
body: > this is the example where snapshot testing would be very helpful, maybe spend a day to build those tests first.

@yurishkuro I am able to generate or reuse the db spans fixtures but I don't have any idea to serialize-deserialize ptrace from json fixtures. Is there anything provided by OTEL? Or we need to use txt fixtures which are used in [goldendataset](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/internal/coreinternal/goldendataset/testdata)
I have tested the already existing fixture in the following way:
```
func TestToDbModel_Fixtures(t *testing.T) {
	dbStr, err := os.ReadFile("../../../elasticsearch/dbmodel/fixtures/es_01.json")
	require.NoError(t, err)
	var span dbmodel.Span
	err = json.Unmarshal(dbStr, &span)
	require.NoError(t, err)
	td, err := FromDBModel([]dbmodel.Span{span})
	require.NoError(t, err)
	spans := ToDBModel(td)
	assert.Equal(t, span, spans[0])
}
```
author_association: CONTRIBUTOR
user: login: Manik2708
created_at: 2025-04-04T15:57:27Z
body: @yurishkuro Please review this PR, after this PR, similar modification will be done in `FromDBModel` and we will get rid of `es_01.json`
author_association: CONTRIBUTOR
user: login: Manik2708
created_at: 2025-04-06T19:33:27Z
body: @yurishkuro Can you please review this PR as it is a blocker for the next PR (embedding `tagDotReplacement` in `FromDBModel`)
author_association: CONTRIBUTOR
user: login: Manik2708
created_at: 2025-04-06T20:46:12Z
body: https://github.com/jaegertracing/jaeger/pull/6946#discussion_r2030257486 @yurishkuro There is a problem in this approach, if we will firstly convert all the tags to db tag and then materialize them to top-level fields then the value of dbmodel.KeyValue is always put as string (this is what is being done in v1) whereas in tag map it can be anything. For example when 25 is saved in `dbmodel.KeyValue` it is stored as "25" in tags but is stored as 25 in tag map (I did this here because we don't want to change the db level span in v2). If we will employ this approach then we will need to convert strings back to their values to put into tag map.
author_association: CONTRIBUTOR
user: login: yurishkuro
created_at: 2025-04-06T20:52:13Z
body: in the DB model we have `Tags []KeyValue` and 
```
type KeyValue struct {
        Key   string    `json:"key"`
        Type  ValueType `json:"type,omitempty"`
        Value any       `json:"value"`
}
```

The Value here is not string, similar to `Tag map[string]any`. So I don't see a discrepancy - materializing Tags -> Tag should be lossless.
author_association: MEMBER
user: login: Manik2708
created_at: 2025-04-06T21:01:24Z
body: > in the DB model we have `Tags []KeyValue` and
> 
> ```
> type KeyValue struct {
>         Key   string    `json:"key"`
>         Type  ValueType `json:"type,omitempty"`
>         Value any       `json:"value"`
> }
> ```
> 
> The Value here is not string, similar to `Tag map[string]any`. So I don't see a discrepancy - materializing Tags -> Tag should be lossless.

@yurishkuro That exactly what even I thought but in v1, every value is string in snapshot tests, also please see this: https://github.com/jaegertracing/jaeger/blob/81c9ed937573e64ed391189761fac7a52a6a668d/internal/storage/v1/elasticsearch/spanstore/from_domain.go#L125
Every value which is stored in db tag is string but in tag map, it is like this: https://github.com/jaegertracing/jaeger/blob/81c9ed937573e64ed391189761fac7a52a6a668d/internal/storage/v1/elasticsearch/spanstore/from_domain.go#L86
So how should we proceed in v2? As we have to think of backward-compatibilty also as when converting db spans back to OTEL traces, we have to think whether the value is string or any!
author_association: CONTRIBUTOR
user: login: yurishkuro
created_at: 2025-04-06T22:47:39Z
body: I don't know why v1 converter uses AsString(), I believe I even added a TODO asking about that specifically. But we don't have to blindly replicate v1 converter behavior, I think it should be capturing Value() regardless of where the tag is stored. It's especially important for numeric fields as storing raw value means ES queries can be made against such numeric field, e.g. computing some stats.

The only price we'd pay for using Value() is that the reverse translation (db->otlp) will have to be able to deal with the stored value being either a raw value or a string.

BTW, using Value() directly is not always correct because if the type is Binary it should be encoded somehow. There may be other limitations of using raw values, e.g. whole numbers in JS are limited to 53bits, so we already have some special handing for those when returning to UI to avoid losing precision.
author_association: MEMBER
user: login: Manik2708
created_at: 2025-04-11T17:48:49Z
body: @yurishkuro The diffs in `to_dbmodel.go` and `from_dbmodel.go` is less, most of the changes lie in tests and adding more snapshots which were necessary even if I would have considered only one file for this PR, so I changed both the files in the same PR. If this looks fine, then I will change the title of PR!
author_association: CONTRIBUTOR
user: login: Manik2708
created_at: 2025-04-12T15:28:43Z
body: @yurishkuro I just now discovered that reader always convert the numbers to `json.Number` by this line https://github.com/jaegertracing/jaeger/blob/08be26536479102e1de736710a32ad0de3ca1be5/internal/storage/v1/elasticsearch/spanstore/reader.go#L267
So it is never possible that numbers are directly decoded to float/int instead they are firstly decoded as `json.Number` and then to int/float. Snapshot tests are now testing this workflow also!
author_association: CONTRIBUTOR
user: login: Manik2708
created_at: 2025-04-13T13:40:31Z
body: @yurishkuro This will be the last PR before implementing the writer and reader. Please review this so that we could work on implementing the v2 factory!
author_association: CONTRIBUTOR
user: login: yurishkuro
created_at: 2025-04-13T16:12:50Z
body: > This will be the last PR before implementing the writer and reader. Please review this so that we could work on implementing the v2 factory!

why is this PR blocking you from working on reader/writer/factory? This is exactly the point of separation of responsibilities. The reader/writer/factory do not depend on the minor details of how conversion is done (what this PR is about). You need to learn to parallelize your work, not to block on reviews for a single PR.
author_association: MEMBER
user: login: Manik2708
created_at: 2025-04-13T16:46:22Z
body: > > This will be the last PR before implementing the writer and reader. Please review this so that we could work on implementing the v2 factory!
> 
> why is this PR blocking you from working on reader/writer/factory? This is exactly the point of separation of responsibilities. The reader/writer/factory do not depend on the minor details of how conversion is done (what this PR is about). You need to learn to parallelize your work, not to block on reviews for a single PR.

Actually the work is almost ready in my local repository (for reader and writer) but I was reluctant to raise a PR because this PR uses the `tagDotReplacement` which will remain useless in writer and reader. That was the only reason I was not raising the PR but gonna raise them in the meant time!
author_association: CONTRIBUTOR
user: login: yurishkuro
created_at: 2025-04-13T16:51:06Z
body: > Embed tagDotReplacement in ToDBModel

The PR title must reflect the change in business functionality, not an implementation detail. As an end user why do I care if you "embed" some helper?
author_association: MEMBER
user: login: Manik2708
created_at: 2025-04-24T13:58:10Z
body: @yurishkuro Is this PR good to go?
author_association: CONTRIBUTOR
user: login: Manik2708
created_at: 2025-04-27T07:51:32Z
body: @yurishkuro Is this PR good to go now?
author_association: CONTRIBUTOR