-
Notifications
You must be signed in to change notification settings - Fork 407
Add .NET 11 preview telemetry skill and local-dev (Termux/glibc) docs #1036
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
Open
qapdex-maker
wants to merge
8
commits into
dotnet:main
Choose a base branch
from
qapdex-maker:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a0eb862
docs: add local-dev constraint (glibc/.NET 11 preview) and lightweigh…
qapdex-maker 5dd44af
docs: document running .NET 11 preview inside ubuntu-termux (PRoot)
qapdex-maker 36f3bfc
tests: add eval.yaml + CODEOWNERS for dotnet11/lightweight-telemetry
qapdex-maker cd90705
tests: strengthen dotnet11/lightweight-telemetry eval to 10 stimuli
qapdex-maker 667bb05
Merge branch 'dotnet:main' into main
qapdex-maker 0ac100e
Strengthen dotnet11/lightweight-telemetry skill and eval
qapdex-maker 90894e7
Merge branch 'dotnet:main' into main
qapdex-maker f8224ba
Merge branch 'dotnet:main' into main
qapdex-maker 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| # Local development on non-glibc hosts | ||
|
|
||
| This repository pins a **.NET 11 preview SDK** in the root `global.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "sdk": { | ||
| "version": "11.0.100-preview.3.26207.106", | ||
| "rollForward": "latestMajor" | ||
| }, | ||
| "test": { "runner": "Microsoft.Testing.Platform" } | ||
| } | ||
| ``` | ||
|
|
||
| The official .NET SDK builds distributed from <https://dotnet.microsoft.com> and the | ||
| `dotnet-install.sh` script are **glibc** binaries. They will not run on platforms | ||
| whose system loader is not glibc. This document records the verified constraint and | ||
| the supported ways to develop locally. | ||
|
|
||
| ## Verified constraint | ||
|
|
||
| The following was confirmed on a Termux / Android (aarch64) host: | ||
|
|
||
| | Check | Result | | ||
| |-------|--------| | ||
| | `dotnet` on PATH before install | not found | | ||
| | Install `.NET 11 preview` via `dotnet-install.sh --version 11.0.100-preview.3.26207.106` | "Installation finished successfully" | | ||
| | Run `./.dotnet/dotnet --version` | `cannot execute: required file not found` | | ||
| | ELF interpreter requested by the SDK muxer | `/lib/ld-linux-aarch64.so.1` (glibc) | | ||
| | glibc `libc.so.6` present on device | **none** | | ||
| | System loader present | `/system/bin/linker64` (Android Bionic) | | ||
| | System `dotnet-sdk-10.0` from the distro package | **works** (it is a Bionic build: interpreter `/system/bin/linker64`) | | ||
|
|
||
| Conclusion: the SDK tarballs from dotnet.net are unusable on Bionic-only hosts | ||
| (Termux on Android, some minimal containers). `patchelf` does not help because the | ||
| glibc loader itself is absent. This is a platform ABI limit, not a configuration | ||
| problem in `global.json`. | ||
|
|
||
| ## Recommended local development environments | ||
|
|
||
| Use a host that provides glibc. In priority order: | ||
|
|
||
| ### 1. GitHub Codespaces / GitHub Actions (already used by CI) | ||
| The repository's workflows (for example `skill-validator.yml`) use | ||
| `actions/setup-dotnet@v5` with `global-json-file: global.json`, which installs the | ||
| exact pinned preview on `ubuntu-latest`, `windows-latest`, and `macos-latest`. | ||
| This is the source of truth for a green build. | ||
|
|
||
| ### 2. Docker (Linux, glibc) | ||
| ```bash | ||
| docker run --rm -it -v "$PWD":/repo -w /repo mcr.microsoft.com/dotnet/sdk:11.0-preview | ||
| dotnet build eng/skill-validator/SkillValidator.slnx | ||
| ``` | ||
| The Microsoft `dotnet/sdk` images are glibc-based and resolve the pinned preview. | ||
|
|
||
| ### 3. WSL 2 on Windows | ||
| ```bash | ||
| wsl -d Ubuntu | ||
| sudo apt update && sudo apt install -y dotnet-sdk-11.0 # or use dotnet-install.sh | ||
| cd /mnt/c/path/to/repo | ||
| dotnet build | ||
| ``` | ||
| WSL2 runs a real glibc Linux kernel/userspace, so the preview SDK works. | ||
|
|
||
| ### 4. A glibc Linux VM or remote host | ||
| Any x64/arm64 Linux with glibc (Ubuntu, Fedora, Debian) can run | ||
| `dotnet-install.sh --version 11.0.100-preview.3.26207.106` and build the repo. | ||
|
|
||
| ## What does NOT work (do not waste time) | ||
|
|
||
| - Installing the preview SDK under Termux / Android and expecting it to run. | ||
| - `patchelf`-ing the SDK to the Bionic loader — the glibc runtime is missing. | ||
| - `rollForward` tweaks in `global.json` — they only change SDK *version* selection, | ||
| not the ABI. They cannot make a glibc binary run on Bionic. | ||
|
|
||
| ## Why the .NET 10 distro package is not enough here | ||
|
|
||
| `dotnet-sdk-10.0` installs and builds `net10.0` projects (for example the | ||
| `foundry-agent-webapp` backend), but it does **not** satisfy this repository's | ||
| `global.json`, which requires the `.NET 11 preview`. The preview is only published | ||
| as a glibc SDK, so this repo cannot be built on a Bionic-only host regardless of | ||
| which SDK version is installed. | ||
|
|
||
| ## Running the .NET 11 preview inside `ubuntu-termux` | ||
|
|
||
| The companion repo [qapdex-maker/ubuntu-termux](https://github.com/qapdex-maker/ubuntu-termux) | ||
| boots a real **glibc** Ubuntu 24.04 guest via PRoot on an Android/Termux host. | ||
| Because the guest is glibc (not Bionic), the .NET 11 preview SDK runs there — | ||
| this is the practical way to build this repository's `net11.0` projects on a phone. | ||
|
|
||
| Verified steps (arm64, Ubuntu 24.04 guest): | ||
|
|
||
| ```bash | ||
| # 1. In Termux, install + launch the guest (see that repo's README) | ||
| git clone https://github.com/qapdex-maker/ubuntu-termux.git | ||
| cd ubuntu-termux && ./install.sh -y | ||
| ./startubuntu.sh | ||
|
|
||
| # 2. Inside the guest, install the preview SDK. | ||
| # The minimal rootfs has no curl/wget, so fetch the tarball from the | ||
| # Termux host (it is bind-mounted at /data/data/com.termux) and extract. | ||
| mkdir -p /root/dotnet | ||
| tar -xzf /data/data/com.termux/files/home/ubuntu-termux/dotnet-sdk.tar.gz -C /root/dotnet | ||
| /root/dotnet/dotnet --version # -> 11.0.100-preview.3.26207.106 | ||
| ``` | ||
|
|
||
| ### PRoot-specific runtime workaround (required) | ||
|
|
||
| Under PRoot the .NET runtime tries to reserve ~256 GiB of virtual address space, | ||
| which PRoot blocks, so `dotnet` aborts with | ||
| `GC: Reserving 274877906944 bytes ... failed` / `0x8007000E`. Fix it with: | ||
|
|
||
| ```bash | ||
| export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 # no libicu needed | ||
| export DOTNET_GCHeapHardLimit=134217728 # 128 MiB hard GC limit | ||
| ulimit -v 8388608 # cap virtual memory at 8 GiB | ||
| ``` | ||
|
|
||
| With these set, the runtime starts, builds, and runs normally on arm64 PRoot. | ||
|
|
||
| ### Worked example: the `lightweight-telemetry` sample | ||
|
|
||
| The skill `plugins/dotnet11/skills/lightweight-telemetry/` ships a runnable | ||
| sample. Inside the guest: | ||
|
|
||
| ```bash | ||
| export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1 \ | ||
| DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \ | ||
| DOTNET_GCHeapHardLimit=134217728 | ||
| ulimit -v 8388608 | ||
| cp -r /data/data/com.termux/files/home/github/repo/dotnet-skills/plugins/dotnet11/skills/lightweight-telemetry/sample /root/telemetry-sample | ||
| cd /root/telemetry-sample | ||
| /root/dotnet/dotnet build -c Release # Build succeeded, 0 warnings/0 errors | ||
| /root/dotnet/dotnet run -c Release --no-build | ||
| ``` | ||
|
|
||
| Verified output (structured telemetry as designed): | ||
|
|
||
| ```json | ||
| {"meter":"MyTool","instrument":"tool.runs","unit":"runs","value":"1","timestamp":"2026-08-23T01:26:12.0213552+00:00"} | ||
| {"meter":"MyTool","instrument":"tool.duration.ms","unit":"ms","value":"135.9351","timestamp":"2026-08-23T01:26:12.0991198+00:00"} | ||
| ``` | ||
|
|
||
| ## See also | ||
|
|
||
| - Repository website / dashboard: <https://dotnet.github.io/skills/> | ||
| - Skill authoring guide: <https://agentskills.io> | ||
| - `CONTRIBUTING.md` for how to add or change a plugin. | ||
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,34 @@ | ||
| # Visual manual — slides (authoring deferred) | ||
|
|
||
| This folder is reserved for the **visual manual / slide deck** for the .NET Skills | ||
| repository. Content authoring is deferred to a later pass (owner: repo maintainer). | ||
|
|
||
| ## Goal of the deck | ||
|
|
||
| A short, human-friendly companion to the written docs that explains: | ||
|
|
||
| 1. **What this repo is** — the .NET team's curated Agent Skills & custom agents | ||
| (see `README.md` and <https://dotnet.github.io/skills/>). | ||
| 2. **The plugin map** — one slide per plugin family | ||
| (dotnet, dotnet-ai, dotnet-msbuild, dotnet-aspnetcore, dotnet11, ...). | ||
| 3. **Local dev constraint** — why the .NET 11 preview only builds on glibc hosts | ||
| (Termux/Android is Bionic) and the supported paths (Codespaces, Docker, | ||
| WSL2, glibc VM). Source of truth: `docs/LOCAL-DEVELOPMENT.md`. | ||
| 4. **Lightweight telemetry add-on** — the `lightweight-telemetry` skill | ||
| (`plugins/dotnet11/skills/lightweight-telemetry/`): what it measures and how | ||
| to run the sample. | ||
| 5. **Green CI** — the `skill-validator.yml` build/test matrix and the dashboard | ||
| at <https://dotnet.github.io/skills/>. | ||
|
|
||
| ## Suggested format | ||
|
|
||
| - 8–12 slides. | ||
| - Diagrams: dark theme, architecture/flow style (see repo `docs/design/`). | ||
| - Keep code snippets minimal; link to the skill files for full source. | ||
|
|
||
| ## Status | ||
|
|
||
| - [ ] Outline approved | ||
| - [ ] Slides drafted | ||
| - [ ] Reviewed against `docs/LOCAL-DEVELOPMENT.md` and the telemetry skill | ||
| - [ ] Published / linked from README |
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,158 @@ | ||
| --- | ||
| name: lightweight-telemetry | ||
| description: Emit structured metrics from a .NET 11 console app, CLI, or build tool using the built-in System.Diagnostics.Metrics API with no OpenTelemetry, APM, or collector dependency. Use when asked to "report how long it took", "count how many times it ran", expose queue depth or a running total, pick between a counter, gauge, and histogram, split one metric by a tag/dimension, keep the measurement path cheap when nothing is listening, or print readings as JSON lines from a short-lived process. Do not use for distributed tracing across services (use configuring-opentelemetry-dotnet), cloud ingestion into Application Insights or Azure Monitor, or shipping log lines to Seq/Elasticsearch. | ||
| license: MIT | ||
| --- | ||
|
|
||
| # Lightweight telemetry in .NET 11 | ||
|
|
||
| A minimal, dependency-free way to expose operational metrics from a CLI or tool. | ||
| No OpenTelemetry SDK, no external collector required — metrics are written to the | ||
| console as structured lines and can be scraped or redirected. | ||
|
|
||
| ## When to use | ||
|
|
||
| - A build tool, CLI, or local agent needs to report timing/counts. | ||
| - You want structured telemetry without an APM vendor SDK. | ||
| - The host may be resource-constrained (no background collector). | ||
|
|
||
| ## When not to use | ||
|
|
||
| - You need distributed tracing across services → use the | ||
| `configuring-opentelemetry-dotnet` skill instead. | ||
| - You need cloud ingestion (Application Insights) → use the vendor SDK. | ||
|
|
||
| ## Pick the instrument first | ||
|
|
||
| The instrument type is the decision that is most often wrong, and it is not | ||
| recoverable downstream — a consumer cannot turn a gauge back into a rate. | ||
|
|
||
| | The value is | Use | Never use | Because | | ||
| |---|---|---|---| | ||
| | A total that only grows (bytes processed, runs) | `CreateCounter<T>` | a gauge | the consumer derives the rate from the increasing total; a gauge that resets destroys it | | ||
| | The value right now (queue depth, open handles) | `CreateGauge<T>` | a counter | a cumulative sum misrepresents a level that goes down again | | ||
| | A per-operation duration or size you want percentiles for | `CreateHistogram<T>` | a counter | summing durations loses the distribution | | ||
| | A level you can only sample when asked | `CreateObservableGauge<T>` | recording in a hot loop | the callback runs at collection time | | ||
|
|
||
| Always pass the unit and description — put the unit in the **metadata**, not only | ||
| in a `.ms` name suffix, or a consumer cannot tell seconds from milliseconds: | ||
|
|
||
| ```csharp | ||
| meter.CreateHistogram<double>("tool.step.duration", "ms", "Duration per build step"); | ||
| ``` | ||
|
|
||
| ## Split a metric by a dimension, not by name | ||
|
|
||
| One instrument plus a tag, never one instrument per value: | ||
|
|
||
| ```csharp | ||
| stepDuration.Record(elapsedMs, new TagList { { "step", "restore" } }); | ||
| ``` | ||
|
|
||
| Tag **values** must come from a bounded set (step names, status codes). Never tag | ||
| with a user id, path, or timestamp — each distinct value is a separate time | ||
| series downstream. | ||
|
|
||
| ## Keep the hot path cheap | ||
|
|
||
| `Record`/`Add` are cheap, but building the tags and formatting values is not. | ||
| Guard the expensive part when nothing is collecting: | ||
|
|
||
| ```csharp | ||
| if (stepDuration.Enabled) // false when no listener is attached | ||
| stepDuration.Record(elapsedMs, new TagList { { "step", step } }); | ||
| ``` | ||
|
|
||
| Use `TagList` (a struct) rather than allocating a `KeyValuePair[]` per iteration. | ||
|
|
||
| ## Lifetime: set up the listener before the first measurement | ||
|
|
||
| A `MeterListener` only sees measurements recorded **after** `Start()`. In a | ||
| short-lived CLI this is the difference between output and silence: | ||
|
|
||
| ```csharp | ||
| var listener = BuildListener(meter); // Start() called inside | ||
| // ... all recording happens after this point ... | ||
| listener.RecordObservableInstruments(); // pull observable gauges once before exit | ||
| listener.Dispose(); | ||
| meter.Dispose(); | ||
| ``` | ||
|
|
||
| Verified on .NET 10 (`System.Diagnostics.Metrics` is unchanged for these APIs on | ||
| net11.0): a measurement recorded before `listener.Start()` produces **no** output | ||
| line, one recorded after it produces exactly one. Observable instruments emit | ||
| nothing at all unless `RecordObservableInstruments()` is called, so a process that | ||
| exits without it reports nothing for them. | ||
|
|
||
| ## The pattern | ||
|
|
||
| Use `System.Diagnostics.Metrics.Meter` to define a counter and a histogram, drive | ||
| time measurement with `TimeProvider.System`, and flush a snapshot on exit. | ||
|
|
||
| ```csharp | ||
| using System.Diagnostics; | ||
| using System.Diagnostics.Metrics; | ||
|
|
||
| var meter = new Meter("MyTool", "1.0.0"); // stable name = metric identity | ||
| var runs = meter.CreateCounter<int>("tool.runs", "{run}", "Number of executions"); | ||
| var duration = meter.CreateHistogram<double>("tool.step.duration", "ms", "Duration per step"); | ||
|
|
||
| using var listener = new MetricListener(meter); // BEFORE the first measurement | ||
|
|
||
| var clock = TimeProvider.System; // injectable, testable clock | ||
| var start = clock.GetTimestamp(); | ||
|
|
||
| // ... work ... | ||
|
|
||
| if (duration.Enabled) // skip tag building when idle | ||
| duration.Record(clock.GetElapsedTime(start).TotalMilliseconds, | ||
| new TagList { { "step", "compile" } }); | ||
| runs.Add(1); | ||
|
|
||
| listener.Flush(); // pull observables before exit | ||
| ``` | ||
|
|
||
| Substitute a test `TimeProvider` (e.g. `Microsoft.Extensions.Time.Testing.FakeTimeProvider`) | ||
| to assert on recorded durations without sleeping. | ||
|
|
||
| ## Sample (runnable) | ||
|
|
||
| See `sample/Program.cs` and `sample/telemetry.csproj`. Build and run: | ||
|
|
||
| ```bash | ||
| dotnet run --project sample | ||
| ``` | ||
|
|
||
| It prints one JSON line per metric reading, e.g.: | ||
|
|
||
| ```json | ||
| {"meter":"MyTool","instrument":"tool.step.duration","unit":"ms","description":"Duration per step","value":58.6,"tags":{"step":"compile"},"timestamp":"2026-08-29T18:32:07+00:00"} | ||
| ``` | ||
|
|
||
| ## Running the sample inside `ubuntu-termux` (PRoot) | ||
|
|
||
| This skill is verified to run inside the glibc Ubuntu 24.04 guest of | ||
| [qapdex-maker/ubuntu-termux](https://github.com/qapdex-maker/ubuntu-termux) on an | ||
| Android/Termux host — the practical way to execute `net11.0` code on a phone. | ||
| PRoot blocks .NET's default ~256 GiB virtual-address reservation, so set: | ||
|
|
||
| ```bash | ||
| export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 # no libicu in minimal rootfs | ||
| export DOTNET_GCHeapHardLimit=134217728 # 128 MiB hard GC limit | ||
| ulimit -v 8388608 # cap virtual memory at 8 GiB | ||
| ``` | ||
|
|
||
| Then `dotnet build -c Release` (succeeded with 0 warnings/0 errors) and | ||
| `dotnet run -c Release --no-build` produce the designed structured output. See | ||
| `docs/LOCAL-DEVELOPMENT.md` for the full walkthrough. | ||
|
|
||
| ## Notes | ||
|
|
||
| - `Meter`/`Counter`/`Histogram` are built into `System.Diagnostics.DiagnosticSource` | ||
| (no extra NuGet package for the API itself). | ||
| - For production scraping, attach an `IMetricsListener` or export to OTLP; this | ||
| skill intentionally stays at the smallest useful surface. | ||
| - Keep the meter name stable — it becomes the metric namespace downstream. The | ||
| meter *version* string is safe to bump; the name is not. | ||
| - One instrument + a tag beats one instrument per value, but keep tag values | ||
| bounded — unbounded values (ids, paths) create a time series each. |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should all be part of the repo's readme/contributing docs. If something is missing there, please feel free to suggest a change there.