Skip to content

Commit 2ac6628

Browse files
committed
doc update, format fix
1 parent b467188 commit 2ac6628

2 files changed

Lines changed: 241 additions & 1 deletion

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# Application Name Telemetry
2+
3+
> **Status:** Implemented · **Tracking issue:** [#3216](https://github.com/Azure/data-api-builder/issues/3216)
4+
5+
## Summary
6+
7+
Data API builder (DAB) embeds a compact, anonymous **usage-telemetry token** into the `Application Name` property of the connection strings it uses to reach SQL Server, Azure SQL, Azure SQL Data Warehouse (DWSQL), and PostgreSQL. Because `Application Name` is surfaced on the database side (for example in `sys.dm_exec_sessions.program_name`), this lets the team understand — **in aggregate and without any per-customer identifiers** — which DAB version is running and which features are enabled, using telemetry the database already collects.
8+
9+
The token has the shape:
10+
11+
```text
12+
dab_oss_<version>+<context>|<runtime>|<entity>+
13+
```
14+
15+
Example (an MSSQL pool, REST + GraphQL on, Static Web Apps auth):
16+
17+
```text
18+
dab_oss_2.0.0+XXSX|110000M1M000MMMMMWMM|100?111001110?+
19+
```
20+
21+
It is opt-out (`DAB_TELEMETRY_APPNAME_OPT_OUT=1`), carries no secrets or identifiers, and is purely additive to the existing `Application Name` value.
22+
23+
## Motivation
24+
25+
DAB ships as an open-source container that customers run anywhere. We have very little visibility into how it is configured or which features are exercised. The connection's `Application Name` is a standard, low-cost signal that the database side already records, so encoding a small feature fingerprint there gives us aggregate usage insight with:
26+
27+
- **no new endpoints, services, or network calls,**
28+
- **no per-customer data,** and
29+
- **a single, easy place to query** on the database side.
30+
31+
### Goals
32+
33+
- Encode the DAB **version** and a **feature fingerprint** of a deployment into `Application Name`.
34+
- Make the token **queryable and decodable** so the data can be aggregated and read back.
35+
- Be **safe by construction**: no secrets, no identifiers, easy to opt out of, and additive to any user-supplied `Application Name`.
36+
- Avoid changing **connection-pool behavior** (see [Pooling model](#pooling-model-why-some-fields-are-x)).
37+
38+
### Non-goals
39+
40+
- **Per-request** telemetry (which API was called, which entity, which role). Those facets are not knowable when a pooled connection opens and belong in DAB's request-level telemetry (OpenTelemetry / Application Insights), not the `Application Name`.
41+
- Telemetry for **MySQL** and **Cosmos DB**. MySQL does not get a payload, and Cosmos connection strings are left untouched.
42+
43+
## Background: `Application Name` and connection pooling
44+
45+
`Application Name` is a first-class keyword in both the SQL Server and Npgsql connection-string builders, and it is part of the **connection-pool key**. Two connection strings that differ only in `Application Name` produce two separate pools.
46+
47+
Two consequences shape the design:
48+
49+
1. The token is computed **once per data source at configuration load** and is constant for the lifetime of that data source, so embedding it does **not** create additional pools per request &mdash; every request to a data source reuses the same `Application Name`.
50+
2. Each data source already has its own connection string (its own pool), so the token is naturally emitted **per pool**.
51+
52+
DAB already appended a plain `dab_oss_<version>` user agent to the `Application Name`; this feature replaces that plain value with the richer, decodable token (while preserving any user-supplied `Application Name` as a comma-separated prefix).
53+
54+
## The token format
55+
56+
```text
57+
dab_oss_<version>+<context>|<runtime>|<entity>+
58+
```
59+
60+
- `dab_oss_` &mdash; a fixed marker (`ProductInfo.DAB_USER_AGENT_MARKER`) used to locate the token and to decode it.
61+
- `<version>` &mdash; the product version `Major.Minor.Patch` (from `ProductInfo.DAB_USER_AGENT`). The telemetry is always based on the product version, so it is independent of any host label (see [Hosted label](#hosted-label-dab_app_name_env)).
62+
- The payload is wrapped in `+ ... +` and split into **three `|`-delimited sections**: `context`, `runtime`, and `entity`.
63+
64+
> **Note on the issue's example.** The original issue's example string listed a fourth `general` segment, but the issue only defined three settings tables (Context, Runtime, Entity). `general` was never defined, so the implementation uses the three authoritative sections only.
65+
66+
Each position in a section is a single character drawn from a small alphabet. The shared sentinel values are:
67+
68+
| Char | Meaning |
69+
| --- | --- |
70+
| `0` | feature present and off / false |
71+
| `1` | feature present and on / true |
72+
| `M` | **missing** &mdash; the config section that would answer this is absent |
73+
| `X` | **not applicable** &mdash; not knowable when the pool opens (per-request fields) |
74+
| `?` | **not supported** &mdash; the concept is not yet modeled in DAB |
75+
76+
A few positions use field-specific letters instead (Source and Auth provider), described below.
77+
78+
### Context section (4 characters)
79+
80+
Identifies *what kind of connection* this is. Only `Source` is knowable when a pooled connection opens; the rest are per-request and therefore `X` (see [Pooling model](#pooling-model-why-some-fields-are-x)).
81+
82+
| Pos | Field | Encoding |
83+
| --- | --- | --- |
84+
| 1 | Protocol | always `X` (per-request: REST / GraphQL / MCP) |
85+
| 2 | Object | always `X` (per-request: table / view / stored-proc / document) |
86+
| 3 | Source | the database engine of this data source (see table) |
87+
| 4 | Role | always `X` (per-request: anonymous / authenticated / custom) |
88+
89+
**Source map:** `MSSQL -> S`, `DWSQL -> D`, `PostgreSQL -> P`, `MySQL -> M`, `Cosmos -> C`, and `X` when there is no live data source (for example the CLI, which has no open connection).
90+
91+
### Runtime section (20 characters)
92+
93+
A fingerprint of the **global** `runtime` configuration. Each position is `0` / `1` / `M` unless noted.
94+
95+
| Pos | Setting |
96+
| --- | --- |
97+
| 1 | `runtime.rest.enabled` |
98+
| 2 | `runtime.graphql.enabled` |
99+
| 3 | `runtime.mcp.enabled` |
100+
| 4 | `runtime.host.mode` (`0` = Development, `1` = Production, `M` = missing) |
101+
| 5 | `data-source-files` present (multi-database) |
102+
| 6 | `azure-key-vault` configured |
103+
| 7 | `runtime.health.enabled` |
104+
| 8 | `runtime.cache.enabled` |
105+
| 9 | `runtime.cache.level-2.enabled` |
106+
| 10 | data source uses on-behalf-of (OBO) auth |
107+
| 11 | auto-entities present |
108+
| 12 | `runtime.rest.request-body-strict` |
109+
| 13 | `runtime.graphql.multiple-mutations.create.enabled` |
110+
| 14 | `runtime.telemetry.open-telemetry.enabled` |
111+
| 15 | `runtime.telemetry.application-insights.enabled` |
112+
| 16 | `runtime.telemetry.azure-log-analytics.enabled` |
113+
| 17 | `runtime.telemetry.file.enabled` (file sink) |
114+
| 18 | `runtime.host.authentication.provider` (letter, see below) |
115+
| 19 | embeddings enabled |
116+
| 20 | embeddings endpoint configured |
117+
118+
**Auth provider letters (position 18):** `U` = Unauthenticated/Simulator-disabled, `S` = Simulator, `W` = StaticWebApps, `A` = AppService, `E` = EntraID / AzureAD, `C` = a custom JWT provider, `M` = no authentication section. The single-letter mapping is a DAB-chosen convention (the issue gave the alphabet without a legend); it is trivial to adjust because encode and decode share one table.
119+
120+
### Entity section (14 characters)
121+
122+
An "**is any entity using X?**" fingerprint computed across the (merged) entity set. Each position is `0` / `1` / `M`; positions 4 and 14 may also be `?` because they are not yet modeled.
123+
124+
| Pos | "Any entity &hellip;" |
125+
| --- | --- |
126+
| 1 | is a table |
127+
| 2 | is a view |
128+
| 3 | is a stored procedure |
129+
| 4 | is an MCP persisted document (`?` &mdash; not modeled) |
130+
| 5 | has caching enabled |
131+
| 6 | has REST enabled |
132+
| 7 | has GraphQL enabled |
133+
| 8 | exposes MCP DML tools |
134+
| 9 | exposes an MCP custom tool |
135+
| 10 | uses a custom role (not `anonymous` / `authenticated`) |
136+
| 11 | uses an item-level policy |
137+
| 12 | has a description |
138+
| 13 | has relationships |
139+
| 14 | uses parameter embedding (`?` &mdash; not modeled) |
140+
141+
`M` here means "no entities at all," distinguishing an empty deployment from one whose entities simply do not use a feature.
142+
143+
## Design
144+
145+
### Encoder / decoder
146+
147+
A single class, `ApplicationNameTelemetry` (in `Azure.DataApiBuilder.Config.Telemetry`), owns the format:
148+
149+
- `EncodeTelemetryString(config, liveDataSource)` produces the pure `dab_oss_<version>+...+` token. It is **independent of the opt-out switch and of any host label** &mdash; it always emits the full payload, which is why the CLI uses it for inspection.
150+
- `BuildApplicationNameSegment(config, liveDataSource)` produces what is actually embedded: it honors the opt-out switch and prepends any host label.
151+
- `Decode(applicationName)` turns a token back into human-readable lines and is tolerant of truncation, a missing trailing delimiter, an absent payload, and extra (newer) flags.
152+
153+
Encode and decode are driven by **one ordered list of settings per section** (`_contextSettings`, `_runtimeSettings`, `_entitySettings`). Each setting knows how to encode itself and how to describe a decoded character, so the two directions can never drift apart, and adding a flag is a one-line, append-only change.
154+
155+
### Where and when the token is embedded
156+
157+
The token is woven into connection strings at **configuration load time**, never per request.
158+
159+
- **File / standard load.** `RuntimeConfigLoader.TryParseConfig` post-processes the parsed config and, for every MSSQL / DWSQL / PostgreSQL data source, replaces the `Application Name` with the embedded token. A single public dispatcher, `GetConnectionStringWithApplicationName(connectionString, config, dataSource)`, selects the engine-specific builder (`SqlConnectionStringBuilder` vs `NpgsqlConnectionStringBuilder`); engines without telemetry support return the connection string unchanged.
160+
- **Hosted / late-config.** The `POST /configuration` endpoint supplies configuration after startup with environment-variable replacement disabled, which bypasses the file-load post-processing. `RuntimeConfigProvider.Initialize` therefore embeds the token itself for every data source after the config is materialized, so hosted deployments &mdash; exactly where the `dab_hosted` label matters most &mdash; are covered for both the single-connection-string and merged-config endpoint variants.
161+
162+
### Pooling model: why some fields are `X`
163+
164+
The `Application Name` is the pool key. If `Protocol`, `Object`, and `Role` were encoded per request, DAB would need a distinct pool per `(protocol, object, role)` combination (3 &times; 4 &times; 3 = 36 per data source, multiplied again per user under OBO), exploding the pool count and harming performance. We therefore adopt **Model A**: encode only what is fixed when the pool opens (`Source`) and emit `X` for the per-request facets. Those per-request dimensions, when needed, belong in DAB's request-level telemetry, not the connection's `Application Name`.
165+
166+
### Global telemetry per pool (multi-database)
167+
168+
In a multi-database deployment each data source is its own pool, so the token is embedded into each. We deliberately encode the **global** runtime and the **complete (merged) entity set** at every pool, rather than scoping the entity fingerprint to the entities of that specific data source. The decisive reason is that **the token carries no deployment-correlation identifier**, so the consumer cannot stitch per-pool slices back into one deployment. Encoding the global picture at every pool means **any single sampled connection is sufficient** to know the deployment's full feature profile &mdash; robust to sampling and to rarely-opened pools. (The `Source` character still differs per pool, so the engine mix is preserved.)
169+
170+
### Idempotency
171+
172+
Embedding is idempotent: the engine-specific helpers parse the existing `Application Name` and **skip** if it already contains the `dab_oss_` marker. This guarantees a value can never accumulate a duplicated payload (`...+...+,dab_oss_...+...+`) even if the embed path runs more than once (for example loader post-processing followed by the late-config provider). A user-supplied `Application Name` with no marker is preserved and the token is appended after a comma.
173+
174+
### Opt-out (`DAB_TELEMETRY_APPNAME_OPT_OUT`)
175+
176+
Setting `DAB_TELEMETRY_APPNAME_OPT_OUT=1` reduces the embedded value to **version only** (`dab_oss_<version>`, no payload). Any other value (or unset) leaves telemetry on. The marker is preserved even when opted out so the version remains decodable.
177+
178+
### Hosted label (`DAB_APP_NAME_ENV`)
179+
180+
When `DAB_APP_NAME_ENV` is set (DAB's hosted offering sets it to `dab_hosted`), its value is preserved as a **comma prefix**: `dab_hosted,dab_oss_<version>+...+`. Telemetry is always computed from the product version, so the host label **never suppresses** the token and the `dab_oss_` marker stays intact for decoding.
181+
182+
### CLI: `dab appname`
183+
184+
A new offline command supports inspection without a database:
185+
186+
- `dab appname --config <file>` parses the config and prints the token. Context is emitted as placeholders (no live connection), so the `Source` is `X`. This command performs **no validation and opens no connection** &mdash; it is a static inspection tool, and it intentionally always shows the full encoding regardless of the opt-out switch.
187+
- `dab appname --decode "<token>"` prints a human-readable legend, tolerant of truncation.
188+
- `-o, --output <file>` writes the result to a file instead of stdout.
189+
190+
### Diagnostic logging
191+
192+
When the token is computed, DAB emits a single Debug log of the **token only** (never the full connection string, which can contain secrets). Because the log level may not be known when connection strings are first computed, the entry is buffered in a shared `LogBuffer` and flushed once the logger is available (at startup, on hot reload, and on the hosted late-config path). The buffer is **bounded** (drop-oldest beyond a cap) so it cannot grow without limit if it is ever left undrained.
193+
194+
## Privacy and security
195+
196+
- The token contains **no connection-string contents, secrets, server names, database names, or customer identifiers** &mdash; only the DAB version and boolean/categorical feature flags.
197+
- It is **opt-out** via `DAB_TELEMETRY_APPNAME_OPT_OUT=1`.
198+
- The diagnostic log emits the **token**, never the connection string.
199+
200+
## Scope
201+
202+
| Engine | Telemetry token | Notes |
203+
| --- | --- | --- |
204+
| SQL Server / Azure SQL (`MSSQL`) | Yes (`Source = S`) | |
205+
| SQL Data Warehouse (`DWSQL`) | Yes (`Source = D`) | shares the SQL Server builder |
206+
| PostgreSQL | Yes (`Source = P`) | |
207+
| MySQL | No | connection string left unchanged |
208+
| Cosmos DB | No | connection string left unchanged |
209+
210+
## Decoding and observing in production
211+
212+
On the database side, the token appears as the session's program name. For SQL Server:
213+
214+
```sql
215+
SELECT program_name
216+
FROM sys.dm_exec_sessions
217+
WHERE program_name LIKE 'dab_oss%' OR program_name LIKE '%,dab_oss%';
218+
```
219+
220+
A captured token can be decoded back to a legend with `dab appname --decode "<token>"`.
221+
222+
## Testing
223+
224+
- **Encoder / decoder unit tests** for token shape, each section's flag mapping, the Source and auth-provider maps, opt-out, the host-label prefix, and round-trip / truncation-tolerant decoding.
225+
- **Connection-string injection tests** for MSSQL, DWSQL, and PostgreSQL (including the user-supplied `Application Name` prefix case), and the no-op cases for MySQL / Cosmos.
226+
- **Multi-database tests** asserting child data sources encode the global runtime and merged entities, and a heterogeneous (MSSQL + PostgreSQL) case asserting the per-pool `Source` character.
227+
- **Hosted / late-config tests** asserting telemetry is embedded through `RuntimeConfigProvider.Initialize` for the single-source and multi-database cases, plus end-to-end `/configuration` and `/configuration/v2` endpoint tests.
228+
- **Idempotency test** asserting a re-embed is a no-op (exactly one marker).
229+
- **`LogBuffer` tests** asserting the bounded drop-oldest behavior and flush-and-drain.
230+
- **CLI tests** for encode (to file and stdout), decode, the config-not-found error path, and opt-out independence.
231+
232+
## Extensibility
233+
234+
- **Adding a flag.** Append a `Setting` to the relevant section list; encode and decode update together. Sentinels (`M`, `?`, `X`) keep older decoders forward-compatible.
235+
- **Adding an engine.** Implement an engine-specific `Get...ConnectionStringWithApplicationName`, add it to the dispatcher's switch, and map the engine to a `Source` character. The injection sites (file load, hosted, multi-database) are already engine-agnostic.
236+
237+
238+
## References
239+
240+
- Tracking issue: [#3216](https://github.com/Azure/data-api-builder/issues/3216)
241+
- Key types: `ApplicationNameTelemetry` (`src/Config/Telemetry/`), `RuntimeConfigLoader` / `FileSystemRuntimeConfigLoader` (`src/Config/`), `RuntimeConfigProvider` (`src/Core/Configurations/`), `LogBuffer` (`src/Config/`), `AppNameOptions` (`src/Cli/Commands/`).

src/Service.Tests/UnitTests/LogBufferTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Linq;
76
using Azure.DataApiBuilder.Config;
87
using Microsoft.Extensions.Logging;
98
using Microsoft.VisualStudio.TestTools.UnitTesting;

0 commit comments

Comments
 (0)