Skip to content

Serialize additional dictionary shapes as KeyValue lists - #7679

Merged
martincostello merged 6 commits into
open-telemetry:mainfrom
slang25:slang25/otel-logrecord-kvlist-support
Aug 28, 2026
Merged

martincostello merged 6 commits into
open-telemetry:mainfrom
slang25:slang25/otel-logrecord-kvlist-support

Conversation

@slang25

@slang25 slang25 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #7678

Changes

  • TagWriter: pulled the depth-guarded kvlist path from Add support for serializing KeyValue lists #7015 into TryWriteKvListTagWithinDepthLimit, then added two cases, IEnumerable<KeyValuePair<string, string?>> (e.g. Dictionary<string, string>) and non-generic IDictionary (e.g. Dictionary<string, int>, Hashtable). Both sit after the Array case so the existing type checks are untouched
  • ZipkinTagWriter: kvlist values are now a JSON object embedded in a string, the same treatment arrays already get, rather than Convert.ToString() output
  • Unit tests for the new shapes (including Hashtable with non-string keys and the depth-limit fallback) and CHANGELOG entries

Benchmark before/after (ProtobufOtlpLogSerializerBenchmarks, net9.0, Apple M-series, --job short) shows no measurable change. The new checks only run for values that previously fell through to Convert.ToString().

AttributeCount KvListAttributeCount Before After Allocated (both)
4 0 133.5 ns 143.2 ns –
4 2 222.2 ns 227.7 ns 48 B
4 4 254.9 ns 254.3 ns 48 B
4 8 344.3 ns 343.8 ns 48 B
8 0 230.4 ns 230.0 ns –
8 2 314.5 ns 319.8 ns 48 B
8 4 350.9 ns 350.3 ns 48 B
8 8 452.0 ns 447.5 ns 48 B
16 0 430.7 ns 421.8 ns –
16 2 520.9 ns 532.3 ns 48 B
16 4 573.3 ns 557.5 ns 48 B
16 8 654.3 ns 677.2 ns 48 B

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

🤖 Generated with Claude Code

@slang25
slang25 requested a review from a team as a code owner August 22, 2026 10:52
@github-actions

Copy link
Copy Markdown
Contributor

Welcome, contributor! Thank you for your contribution to opentelemetry-dotnet.

Important reminders:

  • Read our Contributing Guidelines.
  • Sign the CLA if you haven't already.
  • Follow the OpenTelemetry Generative AI policy: disclose any AI use in your contribution, and communicate (PR descriptions, review replies) in your own words rather than AI-generated text.
  • Give reviewers at least a few days before pinging them for feedback.
  • If you need help with general setup, development process, or contributor etiquette, ask in #opentelemetry-new-contributors.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

@github-actions github-actions Bot added pkg:OpenTelemetry.Exporter.Console Issues related to OpenTelemetry.Exporter.Console NuGet package pkg:OpenTelemetry.Exporter.OpenTelemetryProtocol Issues related to OpenTelemetry.Exporter.OpenTelemetryProtocol NuGet package pkg:OpenTelemetry.Exporter.Zipkin Issues related to OpenTelemetry.Exporter.Zipkin NuGet package labels Aug 22, 2026
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Pull request dashboard status

Merged · refreshed 2026-08-28 16:11 UTC

Status above doesn't look right?
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@slang25
slang25 force-pushed the slang25/otel-logrecord-kvlist-support branch from d48a27b to 1cdcc82 Compare August 22, 2026 10:59
@slang25

slang25 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

/easycla

@martincostello martincostello left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some minor comments.

Comment thread src/OpenTelemetry.Exporter.Console/CHANGELOG.md Outdated
Comment thread src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md
Comment thread src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md Outdated
Comment thread src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagWriter.cs Outdated
Comment thread src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagWriter.cs Outdated
Comment thread src/Shared/TagWriter/TagWriter.cs Outdated
Comment thread src/Shared/TagWriter/TagWriter.cs Outdated
Comment thread test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinTagWriterTests.cs Outdated
@codecov

codecov Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.52%. Comparing base (0bb4087) to head (805c172).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #7679      +/-   ##
==========================================
- Coverage   91.55%   91.52%   -0.03%     
==========================================
  Files         321      321              
  Lines       17982    17988       +6     
==========================================
+ Hits        16463    16464       +1     
- Misses       1519     1524       +5     
Flag Coverage Δ
unittests-Project-Experimental 91.61% <100.00%> (-0.03%) ⬇️
unittests-Project-Stable 91.62% <100.00%> (+0.02%) ⬆️
unittests-Solution 91.62% <100.00%> (-0.01%) ⬇️
unittests-UnstableCoreLibraries-Experimental 51.11% <ø> (-0.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
....Exporter.Zipkin/Implementation/ZipkinTagWriter.cs 73.52% <100.00%> (+23.52%) ⬆️

... and 6 files with indirect coverage changes

Simplify the CHANGELOG entries and drop the explanatory comments called
out in review.
@slang25
slang25 force-pushed the slang25/otel-logrecord-kvlist-support branch from 23cdf7c to 23f7785 Compare August 22, 2026 13:50
@martincostello

Copy link
Copy Markdown
Member

Do you have an example of the after to compare to the before in #7678 just to visualise the difference?

@martincostello

Copy link
Copy Markdown
Member

Also while you're touching the file, this area might also be a candidate for similar optimisations as in #7681 when iterating the KV pairs.

@thomhurst

Copy link
Copy Markdown
Contributor

Could you extend even further to support ReadOnlySpan<KeyValuePair<string, object?>> for less allocating?

…d-kvlist-support

# Conflicts:
#	src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md
@github-actions github-actions Bot added perf Performance related pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package labels Aug 25, 2026
@slang25

slang25 commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor Author

@martincostello good shout on the #7681-style optimisation — I had a go at it. I've pushed a commit that swaps the foreach over the KV pairs for specialised struct enumerators, so the common shapes (Dictionary<string, object?>, List<...>, arrays, the string dictionaries, and so on) enumerate without allocating an enumerator on the heap. On the Zipkin side the nested writer is now pooled per depth level rather than newing up a MemoryStream/Utf8JsonWriter for every kvlist. I've added a couple of benchmarks for the kvlist shapes and brought the branch up to date with main while I was in there.

The honest caveat is it grows the PR a fair bit — there's a new IKeyValueListEnumerator with a struct per shape, and WriteKvListTag becomes generic over it. So if that feels like a step too far for what started out as "support a few more dictionary shapes", I'm very happy to revert it back to the more conservative change and do the allocation work as a separate follow-up. No strong opinion from me either way 🙂

@thomhurst the ReadOnlySpan<KeyValuePair<string, object?>> idea is a nice one too — Edit: I originally said it'd slot in as just another enumerator shape, but having had a proper look I was wrong there, it can't be done at all. ReadOnlySpan<T> is a ref struct, and attribute values travel through the SDK as object? (Activity.SetTag(string, object?), LogRecord.Attributes and friends), so a span can never be boxed into that parameter — the compiler rejects it at the call site, meaning there's no code path we could add that a span would ever reach.

The nearest shape that does compile is ReadOnlyMemory<KeyValuePair<string, object?>>, but boxing it to get it into object? costs 32 B, exactly what the array enumerator it would replace costs, so it's a wash (and it would need new public surface). The good news is the struct enumerators here already capture the win — arrays, List<...> and the common dictionary shapes now enumerate without allocating at all.

@martincostello

Copy link
Copy Markdown
Member

The honest caveat is it grows the PR a fair bit

Yeah, I think this is a lot bigger a change than I expected (compared to ~70 LoC in #7681). Let's revert that for now, and you can resubmit post merge if you're interested. I would however avoid changes to Zipkin where possible as it's deprecated and will be removed at the end of this year. Small changes to the console exporter are fine, but there's no need for that to eek every ounce of allocation out of it.

Also still waiting on #7679 (comment).

@github-actions github-actions Bot removed the pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package label Aug 26, 2026
…d-kvlist-support

# Conflicts:
#	src/Shared/TagWriter/TagWriter.cs
@slang25

slang25 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Example of before and after:

logger.LogInformation("Processed order {Order} with labels {Labels}",
    new Dictionary<string, object?> { ["id"] = "ord_123", ["total"] = 42.5 },
    new Dictionary<string, string> { ["region"] = "eu", ["env"] = "prod" });

Above Order already worked from the 1.18, however this is what happened for Labels:

Before

{ "key": "Labels", "value": { "stringValue": "System.Collections.Generic.Dictionary`2[System.String,System.String]" } }

After

{
  "key": "Labels",
  "value": {
    "kvlistValue": {
      "values": [
        { "key": "region", "value": { "stringValue": "eu" } },
        { "key": "env",    "value": { "stringValue": "prod" } }
      ]
    }
  }
}

@martincostello

Copy link
Copy Markdown
Member

Cool thanks - do the changes here also resolve the example as described in #6035?

This was referenced Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf Performance related pkg:OpenTelemetry.Exporter.Console Issues related to OpenTelemetry.Exporter.Console NuGet package pkg:OpenTelemetry.Exporter.OpenTelemetryProtocol Issues related to OpenTelemetry.Exporter.OpenTelemetryProtocol NuGet package pkg:OpenTelemetry.Exporter.Zipkin Issues related to OpenTelemetry.Exporter.Zipkin NuGet package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature request] Dictionary<string, string> and friends still serialize as type names rather than kvlist

3 participants