-
Notifications
You must be signed in to change notification settings - Fork 0
feat(tcg): publish the Telemetry Capability Graph as an OTel LogRecord (First-Light step 3) #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| using System.Globalization; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Extensions.Logging; | ||
| using OpenTelemetry; | ||
| using OpenTelemetry.Logs; | ||
| using Qyl.OpenTelemetry.AutoInstrumentation; | ||
| using Qyl.OpenTelemetry.AutoInstrumentation.Publishing; | ||
|
|
||
| // Proof: the Publishing package emits the binary's Telemetry Capability Graph as a real OTel | ||
| // LogRecord at host startup. We attach a processor (the OTel SDK's pipeline — exactly what an OTLP | ||
| // exporter would sit behind) and assert the captured record matches the binary's own TCG. | ||
| var capturer = new CapturingProcessor(); | ||
|
|
||
| var builder = Host.CreateApplicationBuilder(args); | ||
| builder.Logging.ClearProviders(); | ||
| builder.Logging.AddOpenTelemetry(options => | ||
| { | ||
| options.IncludeFormattedMessage = true; | ||
| options.AddProcessor(capturer); | ||
| }); | ||
| builder.Services.AddQylTelemetryCapabilityGraphPublisher(); | ||
|
|
||
| using (var host = builder.Build()) | ||
| { | ||
| await host.StartAsync(); | ||
| await host.StopAsync(); | ||
| } | ||
|
|
||
| if (!capturer.Found) | ||
| { | ||
| await Console.Error.WriteLineAsync("FAIL: no LogRecord carrying qyl.tcg.schema_version was emitted"); | ||
| return 1; | ||
| } | ||
|
|
||
| Console.WriteLine("event=" + capturer.EventName); | ||
| Console.WriteLine("schema_version=" + capturer.SchemaVersion); | ||
| Console.WriteLine("capability_count=" + capturer.CapabilityCount); | ||
| Console.WriteLine("body_is_tcg_json=" + (capturer.Body == QylTelemetryCapabilityGraph.Json).ToString(CultureInfo.InvariantCulture)); | ||
|
|
||
| var expectedCount = QylTelemetryCapabilityGraph.CapabilityCount.ToString(CultureInfo.InvariantCulture); | ||
| var ok = | ||
| capturer.EventName == "qyl.telemetry_capability_graph" && | ||
| capturer.SchemaVersion == QylTelemetryCapabilityGraph.SchemaVersion && | ||
| capturer.CapabilityCount == expectedCount && | ||
| capturer.Body == QylTelemetryCapabilityGraph.Json; | ||
|
|
||
| if (!ok) | ||
| { | ||
| await Console.Error.WriteLineAsync("FAIL: emitted LogRecord did not match the binary's TCG"); | ||
| return 1; | ||
| } | ||
|
|
||
| Console.WriteLine("tcg-publishing-ok"); | ||
| return 0; | ||
|
|
||
| // Copies the fields out at OnEnd (synchronously, during the log call) so LogRecord pooling cannot | ||
| // recycle them out from under the assertions above. | ||
| internal sealed class CapturingProcessor : BaseProcessor<LogRecord> | ||
| { | ||
| public bool Found { get; private set; } | ||
| public string? EventName { get; private set; } | ||
| public string? SchemaVersion { get; private set; } | ||
| public string? CapabilityCount { get; private set; } | ||
| public string? Body { get; private set; } | ||
|
|
||
| public override void OnEnd(LogRecord record) | ||
| { | ||
| if (record.Attributes is null) | ||
| return; | ||
|
|
||
| if (Attribute(record, "qyl.tcg.schema_version") is { } schemaVersion) | ||
| { | ||
| Found = true; | ||
| EventName = record.EventId.Name; | ||
| SchemaVersion = schemaVersion; | ||
| CapabilityCount = Attribute(record, "qyl.tcg.capability_count"); | ||
| Body = record.FormattedMessage ?? record.Body; | ||
| } | ||
| } | ||
|
|
||
| private static string? Attribute(LogRecord record, string key) | ||
| { | ||
| foreach (var attribute in record.Attributes!) | ||
| { | ||
| if (attribute.Key == key) | ||
| return attribute.Value switch | ||
| { | ||
| null => null, | ||
| IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), | ||
| _ => attribute.Value.ToString(), | ||
| }; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } |
20 changes: 20 additions & 0 deletions
20
demos/Qyl.RealTcgPublishingDemo/Qyl.RealTcgPublishingDemo.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <RootNamespace>Qyl.RealTcgPublishingDemo</RootNamespace> | ||
| <AssemblyName>Qyl.RealTcgPublishingDemo</AssemblyName> | ||
| <Description>Real proof that the qyl Publishing package emits the Telemetry Capability Graph as a true OpenTelemetry LogRecord at host startup.</Description> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Hosting" /> | ||
| <PackageReference Include="OpenTelemetry" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\Qyl.OpenTelemetry.AutoInstrumentation.Publishing\Qyl.OpenTelemetry.AutoInstrumentation.Publishing.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
src/Qyl.OpenTelemetry.AutoInstrumentation.Publishing/PublicAPI.Shipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| #nullable enable |
3 changes: 3 additions & 0 deletions
3
src/Qyl.OpenTelemetry.AutoInstrumentation.Publishing/PublicAPI.Unshipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| #nullable enable | ||
| Qyl.OpenTelemetry.AutoInstrumentation.Publishing.QylTelemetryCapabilityGraphPublishingExtensions | ||
| static Qyl.OpenTelemetry.AutoInstrumentation.Publishing.QylTelemetryCapabilityGraphPublishingExtensions.AddQylTelemetryCapabilityGraphPublisher(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! |
21 changes: 21 additions & 0 deletions
21
...ry.AutoInstrumentation.Publishing/Qyl.OpenTelemetry.AutoInstrumentation.Publishing.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <RootNamespace>Qyl.OpenTelemetry.AutoInstrumentation.Publishing</RootNamespace> | ||
| <AssemblyName>Qyl.OpenTelemetry.AutoInstrumentation.Publishing</AssemblyName> | ||
| <PackageId>Qyl.OpenTelemetry.AutoInstrumentation.Publishing</PackageId> | ||
| <Description>Publishes a qyl binary's Telemetry Capability Graph (TCG) as a single OpenTelemetry LogRecord at host startup. Opt-in via AddQylTelemetryCapabilityGraphPublisher(); emits through ILogger so the OTLP exporter stays app-owned and this package takes no OpenTelemetry SDK dependency.</Description> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> | ||
| <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> | ||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\Qyl.OpenTelemetry.AutoInstrumentation\Qyl.OpenTelemetry.AutoInstrumentation.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
70 changes: 70 additions & 0 deletions
70
...Qyl.OpenTelemetry.AutoInstrumentation.Publishing/QylTelemetryCapabilityGraphPublishing.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Qyl.OpenTelemetry.AutoInstrumentation.Publishing; | ||
|
|
||
| /// <summary> | ||
| /// Registers TCG publishing: emits this binary's Telemetry Capability Graph as a single | ||
| /// OpenTelemetry LogRecord at host startup (North Star pillar 3 — the open exchange channel). | ||
| /// </summary> | ||
| public static class QylTelemetryCapabilityGraphPublishingExtensions | ||
| { | ||
| /// <summary> | ||
| /// Publish the binary's Telemetry Capability Graph (TCG) once at host startup as an | ||
| /// <c>Information</c> log with event name <c>qyl.telemetry_capability_graph</c>: the TCG JSON is the | ||
| /// log body and <c>qyl.tcg.schema_version</c> / <c>qyl.tcg.capability_count</c> are attributes (see | ||
| /// <c>docs/TELEMETRY_CAPABILITY_GRAPH.md</c> → publication channel 2). When the app has OpenTelemetry | ||
| /// logging with an OTLP exporter configured, this becomes a true OTLP LogRecord. The exporter stays | ||
| /// app-owned — this package emits through <see cref="ILogger"/> and takes no OpenTelemetry SDK dependency. | ||
| /// </summary> | ||
| /// <param name="services">The service collection to register the publisher on.</param> | ||
| /// <returns>The same <paramref name="services"/> instance, for chaining.</returns> | ||
| public static IServiceCollection AddQylTelemetryCapabilityGraphPublisher(this IServiceCollection services) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(services); | ||
| services.AddHostedService<TelemetryCapabilityGraphPublisher>(); | ||
| return services; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Emits the binary's Telemetry Capability Graph exactly once, when the host starts. AOT/trim-clean: | ||
| /// the log state is a fixed attribute array and the body is a constant string — no reflection. | ||
| /// </summary> | ||
| internal sealed class TelemetryCapabilityGraphPublisher : IHostedService | ||
| { | ||
| private const string EventName = "qyl.telemetry_capability_graph"; | ||
| private static readonly EventId PublishEvent = new(0, EventName); | ||
|
|
||
| private readonly ILogger<TelemetryCapabilityGraphPublisher> _logger; | ||
|
|
||
| public TelemetryCapabilityGraphPublisher(ILogger<TelemetryCapabilityGraphPublisher> logger) | ||
| => _logger = logger; | ||
|
|
||
| public Task StartAsync(CancellationToken cancellationToken) | ||
| { | ||
| if (cancellationToken.IsCancellationRequested) | ||
| return Task.FromCanceled(cancellationToken); | ||
|
|
||
| var attributes = new[] | ||
| { | ||
| new KeyValuePair<string, object?>("qyl.tcg.schema_version", QylTelemetryCapabilityGraph.SchemaVersion), | ||
| new KeyValuePair<string, object?>("qyl.tcg.capability_count", QylTelemetryCapabilityGraph.CapabilityCount), | ||
| }; | ||
|
|
||
| // Body = the TCG JSON; attributes = schema version + capability count; event name above. The | ||
| // OpenTelemetry logging bridge maps the state's key/value pairs to LogRecord attributes and the | ||
| // formatter output to the LogRecord body. | ||
| _logger.Log( | ||
| LogLevel.Information, | ||
| PublishEvent, | ||
| attributes, | ||
| exception: null, | ||
| formatter: static (_, _) => QylTelemetryCapabilityGraph.Json); | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| #!/usr/bin/env python3 | ||
| from __future__ import annotations | ||
|
|
||
| import subprocess | ||
| from pathlib import Path | ||
|
|
||
| from verify_helpers import clean_env | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
| PROJECT = ROOT / "demos" / "Qyl.RealTcgPublishingDemo" / "Qyl.RealTcgPublishingDemo.csproj" | ||
|
|
||
|
|
||
| def fail(message: str) -> None: | ||
| raise SystemExit(message) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| env = clean_env() | ||
| try: | ||
| completed = subprocess.run( | ||
| ["dotnet", "run", "--project", str(PROJECT), "-c", "Release", "-v", "quiet"], | ||
| cwd=ROOT, | ||
| env=env, | ||
| capture_output=True, | ||
| text=True, | ||
| check=False, | ||
| timeout=600, | ||
| ) | ||
| except subprocess.TimeoutExpired as timed_out: | ||
| fail( | ||
| "tcg publishing demo timed out (the run-once host should emit the TCG and exit)\n" | ||
| f"timeout={timed_out.timeout}s\nstdout={timed_out.stdout}\nstderr={timed_out.stderr}" | ||
| ) | ||
| if completed.returncode != 0: | ||
| fail( | ||
| "tcg publishing demo failed\n" | ||
| f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}" | ||
| ) | ||
|
|
||
| stdout = completed.stdout | ||
| # The demo attaches an OTel log processor (where an OTLP exporter would sit) and asserts the | ||
| # emitted LogRecord matches the binary's own TCG before printing the marker below. | ||
| for required in ( | ||
| "event=qyl.telemetry_capability_graph", | ||
| "body_is_tcg_json=True", | ||
| "tcg-publishing-ok", | ||
| ): | ||
| if required not in stdout: | ||
| fail(f"tcg publishing demo missing expected output: {required!r}\nstdout={stdout}") | ||
|
|
||
| print("tcg-publishing-demo-ok") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.