-
Notifications
You must be signed in to change notification settings - Fork 171
feat(feature-flags): add agentless configuration keys, settings, and endpoint derivation #9040
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
pavlokhrebto
merged 31 commits into
master
from
pavlo.khrebto/EX-2703/ffe-config-and-endpoint
Aug 27, 2026
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
543c9b1
feat(feature-flags): add agentless configuration keys to supported-co…
pavlokhrebto 4753079
feat(feature-flags): add FeatureFlagsSettings with source resolution …
pavlokhrebto 25e168f
feat(feature-flags): add AgentlessEndpoint for CDN URL derivation
pavlokhrebto 9c3a236
test(feature-flags): add unit tests for FeatureFlagsSettings and Agen…
pavlokhrebto 95bf50b
fix(tests): correct AgentlessEndpointTests invalid URL expectations
pavlokhrebto 4e6232b
fix(feature-flags): redact agentless base URL telemetry and reject ma…
pavlokhrebto 7ee3cf3
fix: remove unnecessary null-forgiving operators in AgentlessEndpoint
pavlokhrebto 02a6ac6
fix: drop redundant HasWhitespace check, defer to Uri.TryCreate per r…
pavlokhrebto 7f72e7f
fix: reword credential comment and remove unnecessary null-forgiving …
pavlokhrebto fb32e7d
refactor(feature-flags): use config framework for source resolution a…
pavlokhrebto 2911d78
fix: mark DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL as…
pavlokhrebto 7af7bc5
refactor: pass site/env/apiKey via constructor instead of re-reading …
pavlokhrebto 8f406d9
[FeatureFlags] Resolve the configuration source in a single telemetry…
pavlokhrebto ada921d
[FeatureFlags] Make AgentlessEndpoint a class so a failed TryCreate y…
pavlokhrebto 8c4f3b3
Merge branch 'master' of github.com:DataDog/dd-trace-dotnet into pavl…
pavlokhrebto 154ef58
[FeatureFlags] Apply dd_env per request instead of baking it into the…
pavlokhrebto dbae9dc
[FeatureFlags] Normalize dd_env the same way span tags are normalized
pavlokhrebto 7c7d7e6
[FeatureFlags] Report an unrecognised configuration source as a parsi…
pavlokhrebto d6aafaf
Merge branch 'master' of github.com:DataDog/dd-trace-dotnet into pavl…
pavlokhrebto 2317ea8
[FeatureFlags] Fix double question mark in agentless request URI on .…
pavlokhrebto d868f5a
[FeatureFlags] Align the provider initialization timeout default with…
pavlokhrebto b2f012a
Fix comments
pavlokhrebto 97fdde3
[FeatureFlags] Add dd_env to the managed agentless endpoint only
pavlokhrebto eba74ae
[FeatureFlags] Name the offline source after the value that selects it
pavlokhrebto 1c063c3
[FeatureFlags] Address review comments on source resolution and endpo…
pavlokhrebto 85eacdb
FeatureFlags] Document the agentless base URL contract
pavlokhrebto 4149d7b
[FeatureFlags] Fail closed on an unrecognised configuration source
pavlokhrebto 3bfd025
[FeatureFlags] Reject a Datadog site that can redirect the managed en…
pavlokhrebto d8e1083
[FeatureFlags] Reject a Datadog site that can redirect the managed en…
pavlokhrebto aa8fc5b
[FeatureFlags] Separate whether Feature Flags run from where configur…
pavlokhrebto b3f5318
Merge branch 'master' of github.com:DataDog/dd-trace-dotnet into pavl…
pavlokhrebto 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
Some comments aren't visible on the classic Files Changed page.
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
186 changes: 186 additions & 0 deletions
186
tracer/src/Datadog.Trace/FeatureFlags/Agentless/AgentlessEndpoint.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,186 @@ | ||
| // <copyright file="AgentlessEndpoint.cs" company="Datadog"> | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. | ||
| // </copyright> | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using Datadog.Trace.Processors; | ||
| using Datadog.Trace.Util; | ||
|
|
||
| namespace Datadog.Trace.FeatureFlags.Agentless; | ||
|
|
||
| /// <summary> | ||
| /// The agentless endpoint, derived from the Datadog site or a custom base URL. A class rather | ||
| /// than a struct so that "no endpoint" is <c>null</c> instead of a default instance whose | ||
| /// non-nullable <see cref="Uri"/> is null; it is built once per process, so the allocation is free. | ||
| /// </summary> | ||
| internal sealed class AgentlessEndpoint | ||
| { | ||
| /// <summary> | ||
| /// Canonical rules-based server path, appended to the managed CDN host and to custom base | ||
| /// URLs that only supply an origin. | ||
| /// </summary> | ||
| internal const string DefaultPath = "/api/v2/feature-flagging/config/rules-based/server"; | ||
|
|
||
| /// <summary> | ||
| /// The prefix prepended to the site to form the managed CDN host. | ||
| /// </summary> | ||
| internal const string ManagedHostPrefix = "ufc-server.ff-cdn."; | ||
|
|
||
| /// <summary> | ||
| /// The query parameter carrying the environment to request configuration for. Added to the | ||
| /// managed endpoint only. | ||
| /// </summary> | ||
| internal const string EnvParameterName = "dd_env"; | ||
|
|
||
| private AgentlessEndpoint(Uri uri, bool isManaged) | ||
| { | ||
| Uri = uri; | ||
| IsManaged = isManaged; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the endpoint URI, without the environment. Use <see cref="BuildRequestUri"/> to get the | ||
| /// URI to request. | ||
| /// </summary> | ||
| public Uri Uri { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets a value indicating whether this is the endpoint derived from the site. The API key is | ||
| /// only sent there: a custom endpoint reports its own authentication failure rather than | ||
| /// having the credential leaked to it. | ||
| /// </summary> | ||
| public bool IsManaged { get; } | ||
|
pavlokhrebto marked this conversation as resolved.
|
||
|
|
||
| /// <summary> | ||
| /// Builds the endpoint. Without a custom <paramref name="baseUrl"/> the managed Datadog CDN | ||
| /// endpoint is derived from the (lowercased) site, so staging and government sites resolve | ||
| /// with no allowlist, and is the only endpoint <c>dd_env</c> is added to. A custom base URL that | ||
| /// is an origin receives the canonical path; one that carries a path is used verbatim. | ||
| /// <para> | ||
| /// The environment is not part of the endpoint: it can be changed in code while the application | ||
| /// runs, so it is applied per request by <see cref="BuildRequestUri"/> instead. | ||
| /// </para> | ||
| /// </summary> | ||
| /// <param name="site">The Datadog site, for example <c>datadoghq.com</c>.</param> | ||
| /// <param name="baseUrl">The configured endpoint override, or <c>null</c>.</param> | ||
| /// <param name="endpoint">The resulting endpoint, or <c>null</c> when none could be built.</param> | ||
| /// <param name="error">Why the configured base URL was rejected. Never contains the URL, which may carry credentials.</param> | ||
| /// <returns><c>true</c> when an endpoint could be built.</returns> | ||
| public static bool TryCreate(string? site, string? baseUrl, [NotNullWhen(true)] out AgentlessEndpoint? endpoint, out string? error) | ||
| { | ||
| endpoint = null; | ||
| error = null; | ||
|
|
||
| var configured = baseUrl?.Trim(); | ||
| if (StringUtil.IsNullOrEmpty(configured)) | ||
| { | ||
| var trimmedSite = site?.Trim(); | ||
| if (StringUtil.IsNullOrEmpty(trimmedSite)) | ||
| { | ||
| error = "No Datadog site is configured"; | ||
| return false; | ||
| } | ||
|
|
||
| // The site is concatenated into a host, so every character that can change what a URL means | ||
| // has to be rejected before that happens. "@" is the dangerous one: it would make the rest of | ||
| // the value the real host, and the API key would be sent there. "/", "?" and "#" would start a | ||
| // path, query or fragment, and ":" a port or a scheme. Uri.TryCreate accepts several of these, | ||
| // so it cannot be relied on to catch them. The other tracers reject the same set. | ||
| foreach (var character in trimmedSite) | ||
| { | ||
| if (char.IsWhiteSpace(character) || character is '/' or '?' or '#' or '@' or ':') | ||
| { | ||
| error = "The configured Datadog site is not valid"; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| var managedHost = ManagedHostPrefix + trimmedSite.ToLowerInvariant(); | ||
|
|
||
| if (!Uri.TryCreate($"https://{managedHost}{DefaultPath}", UriKind.Absolute, out var managedUri)) | ||
| { | ||
| error = "The configured Datadog site is not valid"; | ||
| return false; | ||
| } | ||
|
|
||
| endpoint = new AgentlessEndpoint(managedUri, isManaged: true); | ||
| return true; | ||
| } | ||
|
|
||
| // Uri parsing rejects whitespace inside a host, but accepts it in a path or query, so a | ||
| // URL carrying it there is malformed and has to be caught explicitly. | ||
| foreach (var character in configured) | ||
| { | ||
| if (char.IsWhiteSpace(character)) | ||
| { | ||
| error = "The configured Feature Flags agentless URL is not a valid URL"; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| if (!Uri.TryCreate(configured, UriKind.Absolute, out var custom) || StringUtil.IsNullOrEmpty(custom.Host)) | ||
| { | ||
| error = "The configured Feature Flags agentless URL is not a valid absolute URL"; | ||
| return false; | ||
| } | ||
|
|
||
| // http is accepted for a custom endpoint only: pointing at one is an operator decision. | ||
| if (custom.Scheme != Uri.UriSchemeHttps && custom.Scheme != Uri.UriSchemeHttp) | ||
| { | ||
| error = "The configured Feature Flags agentless URL must use HTTP or HTTPS"; | ||
| return false; | ||
| } | ||
|
|
||
| if (custom.AbsolutePath is "" or "/") | ||
| { | ||
| custom = new UriBuilder(custom) { Path = DefaultPath }.Uri; | ||
| } | ||
|
pavlokhrebto marked this conversation as resolved.
|
||
|
|
||
| endpoint = new AgentlessEndpoint(custom, isManaged: false); | ||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the URI to request configuration for <paramref name="env"/>. The environment is | ||
| /// added as a query parameter rather than baked into the endpoint, because it can change while | ||
| /// the application runs. | ||
| /// <para> | ||
| /// A configured base URL is opaque: it is requested exactly as the operator wrote it, since it | ||
| /// may hold credentials, routing, or a scope of its own. Only the managed endpoint carries | ||
| /// <c>dd_env</c>, which matches the other tracers. | ||
| /// </para> | ||
| /// </summary> | ||
| /// <param name="env">The current environment, or <c>null</c> when none is configured.</param> | ||
| /// <returns>The URI to request.</returns> | ||
| public Uri BuildRequestUri(string? env) | ||
| { | ||
| if (!IsManaged) | ||
| { | ||
| return Uri; | ||
| } | ||
|
|
||
| // Normalized the same way the tracer normalizes it before tagging spans, so that flag | ||
| // targeting and span tags agree on what the environment is. It also bounds the value at 200 | ||
| // characters, which keeps a misconfigured environment from producing an unusable URL. | ||
| // A value that normalizes to nothing is treated as no environment at all. | ||
| var normalized = TraceUtil.NormalizeTag(env); | ||
| if (StringUtil.IsNullOrEmpty(normalized)) | ||
| { | ||
| return Uri; | ||
| } | ||
|
|
||
| var parameter = EnvParameterName + "=" + Uri.EscapeDataString(normalized); | ||
| var builder = new UriBuilder(Uri); | ||
|
|
||
| // The getter returns the query with its leading "?", while the setter prepends one of its | ||
| // own on .NET Framework, so the existing query is trimmed before it is extended. A URL | ||
| // ending in a bare "?" reports a query of "?", which the length check treats as no query. | ||
| var existing = builder.Query; | ||
| builder.Query = existing.Length > 1 ? existing.TrimStart('?') + "&" + parameter : parameter; | ||
| return builder.Uri; | ||
| } | ||
| } | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.