Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OpenTelemetry.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
<Folder Name="/examples/">
<Project Path="examples/AspNetCore/Examples.AspNetCore.csproj" />
<Project Path="examples/Console/Examples.Console.csproj" />
<Project Path="examples/EnvironmentVariables/Examples.EnvironmentVariables.csproj" />
<Project Path="examples/FSharp/Examples.FSharp.fsproj" />
<Project Path="examples/GrpcService/Examples.GrpcService.csproj" />
</Folder>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<NoWarn>$(NoWarn);CA2007</NoWarn>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultTargetFrameworkForExampleApps)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Api\OpenTelemetry.Api.csproj" />
</ItemGroup>

</Project>
295 changes: 295 additions & 0 deletions examples/EnvironmentVariables/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using OpenTelemetry;
using OpenTelemetry.Context.Propagation;

namespace Examples.EnvironmentVariables;

internal static class Program
{
private const string ActivitySourceName = "Examples.EnvironmentVariables";
private const string ChildModeArgument = "--child";

private static readonly ActivitySource ActivitySource = new(ActivitySourceName);
private static readonly TextMapPropagator Propagator = new CompositeTextMapPropagator(
[
new TraceContextPropagator(),
new BaggagePropagator(),
]);

public static async Task<int> Main(string[] args)
{
using var listener = CreateActivityListener();

return args.Contains(ChildModeArgument, StringComparer.Ordinal)
? RunAsChild()
: await RunAsParentAsync();
}

private static async Task<int> RunAsParentAsync()
{
Baggage.ClearBaggage();
Baggage.SetBaggage("tenant.id", "contoso");
Baggage.SetBaggage("user.id", "alice");

using var activity = ActivitySource.StartActivity("parent-process");
if (activity == null)
{
await Console.Error.WriteLineAsync("Failed to create the parent activity.");
return 1;
}

WriteProcessContext("Parent", activity, default, Baggage.Current);

var startInfo = CreateChildStartInfo();

CopyCurrentEnvironment(startInfo.Environment);

var context = new PropagationContext(activity.Context, Baggage.Current);
Propagator.Inject(context, startInfo.Environment, EnvironmentVariableCarrier.Set);

Console.WriteLine("[Parent] Injected environment variables:");
WritePropagationFields("Parent", startInfo.Environment);
Console.WriteLine();

using var child = Process.Start(startInfo);
if (child == null)
{
await Console.Error.WriteLineAsync("Failed to start the child process.");
return 1;
}

// See https://stackoverflow.com/a/16326426/1064169 and
// https://learn.microsoft.com/dotnet/api/system.diagnostics.processstartinfo.redirectstandardoutput.
using var outputTokenSource = new CancellationTokenSource();

#pragma warning disable CA2025
var readOutput = ReadOutputAsync(child, outputTokenSource.Token);
#pragma warning restore CA2025

await child.WaitForExitAsync();

(string childStandardError, string childStandardOutput) = await readOutput;

if (!string.IsNullOrEmpty(childStandardOutput))
{
Console.WriteLine();
Console.Write(childStandardOutput);
}

if (!string.IsNullOrEmpty(childStandardError))
{
await Console.Error.WriteAsync(childStandardError);
}

Console.WriteLine();
Console.WriteLine($"[Parent] Child process exited with code {child.ExitCode}.");

return child.ExitCode;

static async Task<(string Error, string Output)> ReadOutputAsync(
Process process,
CancellationToken cancellationToken)
{
var processErrors = ConsumeStreamAsync(process.StandardError, process.StartInfo.RedirectStandardError, cancellationToken);
var processOutput = ConsumeStreamAsync(process.StandardOutput, process.StartInfo.RedirectStandardOutput, cancellationToken);

await Task.WhenAll(processErrors, processOutput);

string error = string.Empty;
string output = string.Empty;

if (processErrors.Status == TaskStatus.RanToCompletion)
{
error = (await processErrors).ToString();
}

if (processOutput.Status == TaskStatus.RanToCompletion)
{
output = (await processOutput).ToString();
}

return (error, output);
}

static Task<StringBuilder> ConsumeStreamAsync(
StreamReader reader,
bool isRedirected,
CancellationToken cancellationToken)
{
return isRedirected ?
Task.Run(() => ProcessStream(reader, cancellationToken), cancellationToken) :
Task.FromResult(new StringBuilder(0));

static async Task<StringBuilder> ProcessStream(
StreamReader reader,
CancellationToken cancellationToken)
{
var builder = new StringBuilder();

try
{
builder.Append(await reader.ReadToEndAsync(cancellationToken));
}
catch (OperationCanceledException)
{
// Ignore
}

return builder;
}
}
}

private static int RunAsChild()
{
var carrier = EnvironmentVariableCarrier.Capture();
var parentContext = Propagator.Extract(default, carrier, EnvironmentVariableCarrier.Get);

Baggage.Current = parentContext.Baggage;

using var activity = ActivitySource.StartActivity(
"child-process",
ActivityKind.Internal,
parentContext.ActivityContext);

if (activity == null)
{
Console.Error.WriteLine("Failed to create the child activity.");
return 1;
}

Console.WriteLine(" [Child] Captured propagated environment variables:");
WritePropagationFields("Child", carrier);
Console.WriteLine();

WriteProcessContext("Child", activity, parentContext.ActivityContext, Baggage.Current);

return 0;
}

private static ActivityListener CreateActivityListener()
{
var listener = new ActivityListener
{
ShouldListenTo = static source => source.Name == ActivitySourceName,
Sample = static (ref _) => ActivitySamplingResult.AllDataAndRecorded,
SampleUsingParentId = static (ref _) => ActivitySamplingResult.AllDataAndRecorded,
};

ActivitySource.AddActivityListener(listener);

return listener;
}

private static ProcessStartInfo CreateChildStartInfo()
{
var processPath = Environment.ProcessPath
?? throw new InvalidOperationException("The current process path is unavailable.");

var entryAssemblyPath = Assembly.GetEntryAssembly()?.Location
?? throw new InvalidOperationException("The entry assembly path is unavailable.");

var fileName = Path.GetFileNameWithoutExtension(processPath);

var startInfo = new ProcessStartInfo(processPath)
{
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
};

if (string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase))
{
startInfo.ArgumentList.Add(entryAssemblyPath);
}

startInfo.ArgumentList.Add(ChildModeArgument);

return startInfo;
}

private static void CopyCurrentEnvironment(IDictionary<string, string?> environment)
{
foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables())
{
environment[(string)variable.Key] = variable.Value?.ToString();
}
}

private static void WriteProcessContext(
string role,
Activity activity,
ActivityContext parentContext,
Baggage baggage)
{
string indent = role is "Parent" ? string.Empty : " ";

Console.WriteLine($"{indent}[{role}] ProcessId: {Environment.ProcessId}");
Console.WriteLine($"{indent}[{role}] TraceId: {activity.TraceId}");
Console.WriteLine($"{indent}[{role}] SpanId: {activity.SpanId}");
Console.WriteLine($"{indent}[{role}] ParentSpanId: {FormatSpanId(activity.ParentSpanId)}");

if (parentContext != default)
{
Console.WriteLine($"{indent}[{role}] ExtractedParentTraceId: {parentContext.TraceId}");
Console.WriteLine($"{indent}[{role}] ExtractedParentSpanId: {FormatSpanId(parentContext.SpanId)}");
}

Console.WriteLine($"{indent}[{role}] Baggage: {FormatBaggage(baggage)}");

static string FormatSpanId(ActivitySpanId spanId)
{
return spanId == default ? "<none>" : spanId.ToString();
}

static string FormatBaggage(Baggage baggage)
{
if (baggage.Count == 0)
{
return "<empty>";
}

var builder = new StringBuilder();

foreach (var item in baggage.GetBaggage())
{
if (builder.Length > 0)
{
builder.Append(", ");
}

builder.Append(item.Key);
builder.Append('=');
builder.Append(item.Value);
}

return builder.ToString();
}
}

private static void WritePropagationFields<T>(string role, T carrier)
where T : IEnumerable<KeyValuePair<string, string?>>
{
if (Propagator.Fields is not { Count: > 0 } fields)
{
return;
}

string indent = role is "Parent" ? string.Empty : " ";

foreach (var field in fields)
{
var normalized = EnvironmentVariableCarrier.NormalizeKey(field);
var values = EnvironmentVariableCarrier.Get(carrier, field);
var value = values?.FirstOrDefault();

Console.WriteLine($"{indent}[{role}] {normalized}={value ?? "<not set>"}");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
OpenTelemetry.Context.Propagation.EnvironmentVariableCarrier
static OpenTelemetry.Context.Propagation.EnvironmentVariableCarrier.Capture() -> System.Collections.Generic.IReadOnlyDictionary<string!, string?>!
static OpenTelemetry.Context.Propagation.EnvironmentVariableCarrier.Capture(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string?>>! environmentVariables) -> System.Collections.Generic.IReadOnlyDictionary<string!, string?>!
static OpenTelemetry.Context.Propagation.EnvironmentVariableCarrier.Get<T>(T carrier, string! key) -> System.Collections.Generic.IEnumerable<string!>?
static OpenTelemetry.Context.Propagation.EnvironmentVariableCarrier.NormalizeKey(string! key) -> string!
static OpenTelemetry.Context.Propagation.EnvironmentVariableCarrier.Set<T>(T carrier, string! key, string! value) -> void
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Released 2026-Apr-21
from values when parsing the `baggage` header.
([#7009](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7009))

* Add support for using environment variables as context propagation carriers.
([#7174](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7174))

## 1.15.2

Released 2026-Apr-08
Expand Down
Loading
Loading