From 0be82285ef41f0eb493935b327ac276d643fcf97 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 9 Sep 2026 23:22:10 +0200 Subject: [PATCH] Make Debug OTLP export opt-in via FSHARP_OTEL_EXPORT FSharpPackage unconditionally started an OTLP trace/metric exporter against http://127.0.0.1:4317 in Debug builds. Without a collector listening there, the metric exporter retries in the background for the whole session and Dispose blocks on a 5s ForceFlush, so closing the experimental VS instance stalls every time. otelExport now only starts the exporter when FSHARP_OTEL_EXPORT is set to an absolute http/https URI with a host - a value like "localhost:4317" parses as an absolute URI too but has no host, so it is treated the same as an empty or unparseable value and falls back to the default endpoint. The default VisualFSharpDebug launch profile clears the variable so a first F5 never surprises anyone with OTEL noise, even if it leaked in from an ambient shell environment; the new "OTEL Export" profile sets it to opt back in. Co-Authored-By: Claude Sonnet 5 --- DEVGUIDE.md | 2 ++ docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../Properties/launchSettings.json | 18 +++++++++++- .../src/FSharp.Editor/Common/DebugHelpers.fs | 29 ++++++++++++++++--- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/DEVGUIDE.md b/DEVGUIDE.md index 11995f91f80..0af0c55e85c 100644 --- a/DEVGUIDE.md +++ b/DEVGUIDE.md @@ -374,6 +374,8 @@ Then, use the **f5** or **ctrl+f5** keyboard shortcuts to test your tooling chan > [!TIP] > If the F# extension fails to load when you F5 (because the Roslyn version in the repo is ahead of the one in your Visual Studio), launch VS with `.\start-vs-VisualFSharpSln.ps1` instead of opening the solution directly. It builds the extension against the Roslyn your VS actually ships, so you don't need an internal/nightly VS or a local Roslyn build. Requires VS with Roslyn 5.10 or newer. +Debug builds export traces and metrics over OTLP only when `FSHARP_OTEL_EXPORT` is set, so the default `VisualFSharpDebug` profile stays quiet without a collector running. Pick the `OTEL Export` launch profile - it points `FSHARP_OTEL_EXPORT` at `http://127.0.0.1:4317` - to debug against Jaeger, Prometheus or another OTLP collector listening there. + Alternatively, you can do this entirely via the command line if you prefer that: ```shell diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..8749505bd8d 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -22,3 +22,4 @@ * Unused analyzers: disable in VS when file has errors ([PR #19892](https://github.com/dotnet/fsharp/pull/19892)) * Move to Roslyn's unified ExternalAccess library ([PR #20099](https://github.com/dotnet/fsharp/pull/20099)) * Remove trailing whitespace from source files. No functional change: whitespace inside string literals and inactive `#if` regions is preserved. ([PR #20355](https://github.com/dotnet/fsharp/pull/20355)) +* The Debug build no longer exports OpenTelemetry unless `FSHARP_OTEL_EXPORT` asks it to (the variable can also carry the collector endpoint), so the default `VisualFSharpDebug` launch profile no longer stalls or retries against a missing collector; the new `OTEL Export` profile turns it back on. ([PR #20468](https://github.com/dotnet/fsharp/pull/20468)) diff --git a/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json b/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json index 5731d86cdd6..7638dfe6832 100644 --- a/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json +++ b/vsintegration/Vsix/VisualFSharpFull/Properties/launchSettings.json @@ -11,7 +11,23 @@ "FSharp_Shim_Present": "true", "FSharpPreferAnyCpuTools": "true", "Fsc_NetFramework_ToolPath": "$(FSharpCompilerPathForDebuggingLocally)", - "Fsc_NetFramework_AnyCpu_ToolExe": "fscAnyCpu.exe" + "Fsc_NetFramework_AnyCpu_ToolExe": "fscAnyCpu.exe", + "FSHARP_OTEL_EXPORT": "" + } + }, + "OTEL Export": { + "commandName": "Executable", + "executablePath": "$(DevEnvDir)devenv.exe", + "commandLineArgs": "/rootsuffix $(VSSDKTargetPlatformRegRootSuffix) /log", + "environmentVariables": { + "FSharpCompilerPath": "$(FSharpCompilerPathForDebuggingLocally)", + "DisableAutoSetFscCompilerPath": "true", + "FSharpPreferNetFrameworkTools": "true", + "FSharp_Shim_Present": "true", + "FSharpPreferAnyCpuTools": "true", + "Fsc_NetFramework_ToolPath": "$(FSharpCompilerPathForDebuggingLocally)", + "Fsc_NetFramework_AnyCpu_ToolExe": "fscAnyCpu.exe", + "FSHARP_OTEL_EXPORT": "http://127.0.0.1:4317" } } } diff --git a/vsintegration/src/FSharp.Editor/Common/DebugHelpers.fs b/vsintegration/src/FSharp.Editor/Common/DebugHelpers.fs index fa8303b7e60..c0ec81b8e8a 100644 --- a/vsintegration/src/FSharp.Editor/Common/DebugHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/DebugHelpers.fs @@ -136,10 +136,26 @@ module FSharpServiceTelemetry = open OpenTelemetry.Trace open OpenTelemetry.Metrics - let otelExport () = - // On Windows forwarding localhost to wsl2 docker container sometimes does not work. Use IP address instead. - let otlpEndpoint = Uri("http://127.0.0.1:4317") - + // Nothing listening on the endpoint means an exporter retrying in the background and a 5s flush + // on shutdown, so exporting is opt-in: FSHARP_OTEL_EXPORT enables it and can carry the endpoint. + let private otelEndpoint = + match Environment.GetEnvironmentVariable "FSHARP_OTEL_EXPORT" with + | null + | "" -> ValueNone + | value -> + match Uri.TryCreate(value, UriKind.Absolute) with + | true, uri when + (uri.Scheme = Uri.UriSchemeHttp || uri.Scheme = Uri.UriSchemeHttps) + && not (String.IsNullOrEmpty uri.Host) + -> + ValueSome uri + // "localhost:4317" parses as an absolute URI too - scheme "localhost", no host - so an + // http(s) scheme and a host are required; anything else falls back to the default, same as + // an unparseable value. On Windows forwarding localhost to wsl2 docker sometimes does not + // work, so that default is an IP address rather than the name. + | _ -> ValueSome(Uri "http://127.0.0.1:4317") + + let private startOtelExport (otlpEndpoint: Uri) = let meterProvider = // Configure OpenTelemetry metrics. Metrics can be viewed in Prometheus or other compatible tools. OpenTelemetry.Sdk @@ -170,5 +186,10 @@ module FSharpServiceTelemetry = tracerProvider.Dispose() meterProvider.Dispose() + let otelExport () = + match otelEndpoint with + | ValueNone -> ignore + | ValueSome endpoint -> startOtelExport endpoint + let listenToAll () = listen "" #endif