Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,5 @@

/plugins/dotnet11/skills/system-text-json-net11/ @dotnet/skills-csharp-language-reviewers
/tests/dotnet11/system-text-json-net11/ @dotnet/skills-csharp-language-reviewers
/plugins/dotnet11/skills/lightweight-telemetry/ @dotnet/skills-csharp-language-reviewers
/tests/dotnet11/lightweight-telemetry/ @dotnet/skills-csharp-language-reviewers
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ $ skill-installer install https://github.com/dotnet/skills/tree/main/plugins/<pl

See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and how to add a new plugin.

### Local development

The repository pins a **.NET 11 preview SDK** in `global.json`. That SDK is only
published as a glibc build, so it does **not** run on Bionic-only hosts (for
example Termux on Android). Use GitHub Codespaces, Docker (`mcr.microsoft.com/dotnet/sdk:11.0-preview`),
WSL 2, or a glibc Linux VM. See [docs/LOCAL-DEVELOPMENT.md](docs/LOCAL-DEVELOPMENT.md)
for the full constraint and verified alternatives.

### Website & dashboard

- Repository website / accuracy dashboard: <https://dotnet.github.io/skills/>
- Agent Skills standard: <https://agentskills.io>

## License

See [LICENSE](LICENSE) for details.
148 changes: 148 additions & 0 deletions docs/LOCAL-DEVELOPMENT.md
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`:

Copy link
Copy Markdown
Collaborator

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.


```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.
34 changes: 34 additions & 0 deletions docs/slides/README.md
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
158 changes: 158 additions & 0 deletions plugins/dotnet11/skills/lightweight-telemetry/SKILL.md
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.
Loading