1212using OpenTelemetry . Exporter . Geneva . Transports ;
1313using OpenTelemetry . Internal ;
1414using OpenTelemetry . Logs ;
15+ using OpenTelemetry . Resources ;
1516
1617namespace OpenTelemetry . Exporter . Geneva . MsgPack ;
1718
@@ -33,26 +34,33 @@ internal sealed class MsgPackLogExporter : MsgPackExporter, IDisposable
3334
3435#if NET
3536 private readonly FrozenSet < string > ? customFields ;
36- private readonly FrozenDictionary < string , object > ? prepopulatedFields ;
3737#else
3838 private readonly HashSet < string > ? customFields ;
39- private readonly Dictionary < string , object > ? prepopulatedFields ;
4039#endif
4140
4241 private readonly ExceptionStackExportMode exportExceptionStack ;
43- private readonly List < string > ? prepopulatedFieldKeys ;
4442 private readonly byte [ ] bufferEpilogue ;
4543 private readonly IDataTransport dataTransport ;
44+ private readonly Func < Resource > resourceProvider ;
45+
46+ // These are values that are always added to the body as dedicated fields
47+ private readonly Dictionary < string , object > prepopulatedFields ;
48+
49+ // These are values that are always added to env_properties
50+ private readonly Dictionary < string , object > propertiesEntries ;
4651 private readonly int stringFieldSizeLimitCharCount ; // the maximum string size limit for MsgPack strings
4752
4853 // This is used for Scopes
4954 private readonly ThreadLocal < SerializationDataForScopes > serializationData = new ( ) ;
5055
5156 private bool isDisposed ;
5257
53- public MsgPackLogExporter ( GenevaExporterOptions options )
58+ public MsgPackLogExporter ( GenevaExporterOptions options , Func < Resource > resourceProvider )
5459 {
5560 Guard . ThrowIfNull ( options ) ;
61+ Guard . ThrowIfNull ( resourceProvider ) ;
62+
63+ this . resourceProvider = resourceProvider ;
5664
5765 this . tableNameSerializer = new ( options , defaultTableName : "Log" ) ;
5866 this . exportExceptionStack = options . ExceptionStackExportMode ;
@@ -88,21 +96,17 @@ public MsgPackLogExporter(GenevaExporterOptions options)
8896 }
8997
9098 this . stringFieldSizeLimitCharCount = connectionStringBuilder . PrivatePreviewLogMessagePackStringSizeLimit ;
99+
100+ this . propertiesEntries = [ ] ;
101+
102+ this . prepopulatedFields = new Dictionary < string , object > ( options . PrepopulatedFields . Count , StringComparer . Ordinal ) ;
103+
91104 if ( options . PrepopulatedFields != null )
92105 {
93- this . prepopulatedFieldKeys = [ ] ;
94- var tempPrepopulatedFields = new Dictionary < string , object > ( options . PrepopulatedFields . Count , StringComparer . Ordinal ) ;
95106 foreach ( var kv in options . PrepopulatedFields )
96107 {
97- tempPrepopulatedFields [ kv . Key ] = kv . Value ;
98- this . prepopulatedFieldKeys . Add ( kv . Key ) ;
108+ this . prepopulatedFields [ kv . Key ] = kv . Value ;
99109 }
100-
101- #if NET
102- this . prepopulatedFields = tempPrepopulatedFields . ToFrozenDictionary ( StringComparer . Ordinal ) ;
103- #else
104- this . prepopulatedFields = tempPrepopulatedFields ;
105- #endif
106110 }
107111
108112 // TODO: Validate custom fields (reserved name? etc).
@@ -174,27 +178,67 @@ public void Dispose()
174178 this . isDisposed = true ;
175179 }
176180
181+ internal void AddResourceAttributesToPrepopulated ( )
182+ {
183+ // This function needs to be idempotent
184+
185+ foreach ( var entry in this . resourceProvider ( ) . Attributes )
186+ {
187+ string key = entry . Key ;
188+ bool isDedicatedField = false ;
189+ if ( entry . Value is string )
190+ {
191+ switch ( key )
192+ {
193+ case "service.name" :
194+ key = Schema . V40 . PartA . Extensions . Cloud . Role ;
195+ isDedicatedField = true ;
196+ break ;
197+ case "service.instanceId" :
198+ key = Schema . V40 . PartA . Extensions . Cloud . RoleInstance ;
199+ isDedicatedField = true ;
200+ break ;
201+ }
202+ }
203+
204+ if ( isDedicatedField || this . customFields == null || this . customFields . Contains ( key ) )
205+ {
206+ if ( ! this . prepopulatedFields . ContainsKey ( key ) )
207+ {
208+ this . prepopulatedFields . Add ( key , entry . Value ) ;
209+ }
210+ }
211+ else
212+ {
213+ if ( ! this . propertiesEntries . ContainsKey ( key ) )
214+ {
215+ this . propertiesEntries . Add ( key , entry . Value ) ;
216+ }
217+ }
218+ }
219+ }
220+
177221 internal ArraySegment < byte > SerializeLogRecord ( LogRecord logRecord )
178222 {
179223 // `LogRecord.State` and `LogRecord.StateValues` were marked Obsolete in https://github.com/open-telemetry/opentelemetry-dotnet/pull/4334
180224#pragma warning disable 0618
181- IReadOnlyList < KeyValuePair < string , object ? > > ? listKvp ;
225+ IReadOnlyList < KeyValuePair < string , object ? > > ? logFields ;
182226 if ( logRecord . StateValues != null )
183227 {
184- listKvp = logRecord . StateValues ;
228+ logFields = logRecord . StateValues ;
185229 }
186230 else
187231 {
188232 // Attempt to see if State could be ROL_KVP.
189- listKvp = logRecord . State as IReadOnlyList < KeyValuePair < string , object ? > > ;
233+ logFields = logRecord . State as IReadOnlyList < KeyValuePair < string , object ? > > ?? [ ] ;
190234 }
191235#pragma warning restore 0618
192236
193237 var buffer = Buffer . Value ??= new byte [ BUFFER_SIZE ] ; // TODO: handle OOM
194238
195239 /* Fluentd Forward Mode:
196240 [
197- "Log",
241+ "Log", // (or category name)
198242 [
199243 [ <timestamp>, { "env_ver": "4.0", ... } ]
200244 ],
@@ -227,15 +271,20 @@ internal ArraySegment<byte> SerializeLogRecord(LogRecord logRecord)
227271 ushort cntFields = 0 ;
228272 var idxMapSizePatch = cursor - 2 ;
229273
230- if ( this . prepopulatedFieldKeys != null )
274+ this . AddResourceAttributesToPrepopulated ( ) ;
275+
276+ foreach ( var entry in this . prepopulatedFields )
231277 {
232- for ( var i = 0 ; i < this . prepopulatedFieldKeys . Count ; i ++ )
278+ // A prepopulated entry should not be added if the same key exists in the log,
279+ // and customFields configuration would make it a dedicated field.
280+ if ( ( this . customFields == null || this . customFields . Contains ( entry . Key ) )
281+ && logFields . Any ( kvp => kvp . Key == entry . Key ) )
233282 {
234- var key = this . prepopulatedFieldKeys [ i ] ;
235- var value = this . prepopulatedFields ! [ key ] ;
236- cursor = AddPartAField ( buffer , cursor , key , value ) ;
237- cntFields += 1 ;
283+ continue ;
238284 }
285+
286+ cursor = AddPartAField ( buffer , cursor , entry . Key , entry . Value ) ;
287+ cntFields += 1 ;
239288 }
240289
241290 // Part A - core envelope
@@ -295,10 +344,8 @@ internal ArraySegment<byte> SerializeLogRecord(LogRecord logRecord)
295344 var hasEnvProperties = false ;
296345 var bodyPopulated = false ;
297346 var namePopulated = false ;
298- for ( var i = 0 ; i < listKvp ? . Count ; i ++ )
347+ foreach ( var entry in logFields )
299348 {
300- var entry = listKvp [ i ] ;
301-
302349 // Iteration #1 - Get those fields which become dedicated columns
303350 // i.e all Part B fields and opt-in Part C fields.
304351 if ( entry . Key == "{OriginalFormat}" )
@@ -366,27 +413,44 @@ internal ArraySegment<byte> SerializeLogRecord(LogRecord logRecord)
366413 cursor = dataForScopes . Cursor ;
367414 cntFields = dataForScopes . FieldsCount ;
368415
369- if ( hasEnvProperties )
416+ if ( hasEnvProperties || this . propertiesEntries . Count > 0 )
370417 {
371- // Iteration #2 - Get all "other" fields and collapse them into single field
372- // named "env_properties".
418+ // Anything that's not a dedicated field gets put into a part C field called "env_properties".
373419 ushort envPropertiesCount = 0 ;
374420 cursor = MessagePackSerializer . SerializeAsciiString ( buffer , cursor , "env_properties" ) ;
375421 cursor = MessagePackSerializer . WriteMapHeader ( buffer , cursor , ushort . MaxValue ) ;
376422 var idxMapSizeEnvPropertiesPatch = cursor - 2 ;
377- for ( var i = 0 ; i < listKvp ! . Count ; i ++ )
423+
424+ if ( hasEnvProperties )
378425 {
379- var entry = listKvp [ i ] ;
380- if ( entry . Key == "{OriginalFormat}" || this . customFields ! . Contains ( entry . Key ) )
426+ foreach ( var entry in logFields )
381427 {
382- continue ;
428+ if ( entry . Key == "{OriginalFormat}" || this . customFields ! . Contains ( entry . Key ) )
429+ {
430+ continue ;
431+ }
432+ else
433+ {
434+ cursor = MessagePackSerializer . SerializeUnicodeString ( buffer , cursor , entry . Key , this . stringFieldSizeLimitCharCount ) ;
435+ cursor = MessagePackSerializer . Serialize ( buffer , cursor , entry . Value ) ;
436+ envPropertiesCount += 1 ;
437+ }
383438 }
384- else
439+ }
440+
441+ foreach ( var entry in this . propertiesEntries )
442+ {
443+ // A prepopulated env_properties entry should not be added if the same key exists in the log,
444+ // and lack of customFields configuration would place it in env_properties.
445+ if ( this . customFields != null && ! this . customFields . Contains ( entry . Key )
446+ && logFields . Any ( kvp => kvp . Key == entry . Key ) )
385447 {
386- cursor = MessagePackSerializer . SerializeUnicodeString ( buffer , cursor , entry . Key , this . stringFieldSizeLimitCharCount ) ;
387- cursor = MessagePackSerializer . Serialize ( buffer , cursor , entry . Value ) ;
388- envPropertiesCount += 1 ;
448+ continue ;
389449 }
450+
451+ cursor = MessagePackSerializer . SerializeUnicodeString ( buffer , cursor , entry . Key ) ;
452+ cursor = MessagePackSerializer . Serialize ( buffer , cursor , entry . Value ) ;
453+ envPropertiesCount += 1 ;
390454 }
391455
392456 // Prepare state for scopes
0 commit comments