< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Langfuse.LangfuseEndpoints
Assembly: NexusLabs.Needlr.AgentFramework.Langfuse
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Langfuse/LangfuseEndpoints.cs
Line coverage
98%
Covered lines: 57
Uncovered lines: 1
Coverable lines: 58
Total lines: 137
Line coverage: 98.2%
Branch coverage
90%
Covered branches: 19
Total branches: 21
Branch coverage: 90.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_BaseUrl()100%11100%
get_TracesEndpoint()100%11100%
get_MetricsEndpoint()100%11100%
get_ScoresEndpoint()100%11100%
get_AuthorizationHeaderValue()100%11100%
get_Headers()100%11100%
Resolve(...)100%44100%
ResolveBaseUrl(...)100%1010100%
RegionBaseUrl(...)80%5587.5%
EnsureTrailingSlash(...)50%22100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Langfuse/LangfuseEndpoints.cs

#LineLine coverage
 1using System.Text;
 2
 3namespace NexusLabs.Needlr.AgentFramework.Langfuse;
 4
 5/// <summary>
 6/// Resolves the Langfuse OTLP/HTTP ingestion endpoints and authentication headers from a
 7/// <see cref="LangfuseOptions"/> instance.
 8/// </summary>
 9/// <remarks>
 10/// Langfuse exposes an OpenTelemetry-compatible endpoint at <c>/api/public/otel</c> and
 11/// authenticates with HTTP Basic auth over the base64 of <c>publicKey:secretKey</c>. Only
 12/// OTLP over HTTP is supported (gRPC is not), so the .NET exporter is always configured for
 13/// the <c>HttpProtobuf</c> protocol against the signal-specific paths produced here.
 14/// </remarks>
 15internal sealed class LangfuseEndpoints
 16{
 17    private const string OtelBasePath = "api/public/otel";
 18
 719    private LangfuseEndpoints(
 720        Uri baseUrl,
 721        Uri tracesEndpoint,
 722        Uri metricsEndpoint,
 723        Uri scoresEndpoint,
 724        string authorizationHeaderValue,
 725        string headers)
 26    {
 727        BaseUrl = baseUrl;
 728        TracesEndpoint = tracesEndpoint;
 729        MetricsEndpoint = metricsEndpoint;
 730        ScoresEndpoint = scoresEndpoint;
 731        AuthorizationHeaderValue = authorizationHeaderValue;
 732        Headers = headers;
 733    }
 34
 35    /// <summary>
 36    /// Gets the resolved Langfuse base URL with a trailing slash (for example
 37    /// <c>https://cloud.langfuse.com/</c>). Public REST API paths such as
 38    /// <c>api/public/dataset-run-items</c> are resolved relative to this.
 39    /// </summary>
 140    public Uri BaseUrl { get; }
 41
 42    /// <summary>Gets the full OTLP/HTTP traces endpoint (<c>.../api/public/otel/v1/traces</c>).</summary>
 643    public Uri TracesEndpoint { get; }
 44
 45    /// <summary>Gets the full OTLP/HTTP metrics endpoint (<c>.../api/public/otel/v1/metrics</c>).</summary>
 146    public Uri MetricsEndpoint { get; }
 47
 48    /// <summary>Gets the Langfuse public Scores API endpoint (<c>.../api/public/scores</c>).</summary>
 349    public Uri ScoresEndpoint { get; }
 50
 51    /// <summary>Gets the <c>Authorization</c> header value (<c>Basic &lt;base64&gt;</c>).</summary>
 352    public string AuthorizationHeaderValue { get; }
 53
 54    /// <summary>
 55    /// Gets the comma-separated header string consumed by the OTLP exporter, carrying the
 56    /// <c>Authorization</c> header and the Langfuse ingestion-version header.
 57    /// </summary>
 358    public string Headers { get; }
 59
 60    /// <summary>
 61    /// Resolves the ingestion endpoints and headers for the supplied options.
 62    /// </summary>
 63    /// <param name="options">A configured options instance.</param>
 64    /// <returns>The resolved <see cref="LangfuseEndpoints"/>.</returns>
 65    /// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
 66    /// <exception cref="InvalidOperationException">
 67    /// <paramref name="options"/> is missing the public or secret key, or its
 68    /// <see cref="LangfuseOptions.Host"/>/<see cref="LangfuseOptions.Region"/> cannot be resolved
 69    /// to an absolute HTTP(S) URL.
 70    /// </exception>
 71    public static LangfuseEndpoints Resolve(LangfuseOptions options)
 72    {
 1073        ArgumentNullException.ThrowIfNull(options);
 74
 1075        if (string.IsNullOrWhiteSpace(options.PublicKey) || string.IsNullOrWhiteSpace(options.SecretKey))
 76        {
 177            throw new InvalidOperationException(
 178                "Langfuse public and secret keys are required to resolve ingestion endpoints. " +
 179                "Check LangfuseOptions.IsConfigured before resolving endpoints.");
 80        }
 81
 982        var baseUrl = ResolveBaseUrl(options);
 83
 784        var auth = Convert.ToBase64String(
 785            Encoding.UTF8.GetBytes($"{options.PublicKey}:{options.SecretKey}"));
 786        var authorizationHeaderValue = $"Basic {auth}";
 787        var headers = $"Authorization={authorizationHeaderValue},x-langfuse-ingestion-version=4";
 88
 789        return new LangfuseEndpoints(
 790            baseUrl: baseUrl,
 791            tracesEndpoint: new Uri(baseUrl, $"{OtelBasePath}/v1/traces"),
 792            metricsEndpoint: new Uri(baseUrl, $"{OtelBasePath}/v1/metrics"),
 793            scoresEndpoint: new Uri(baseUrl, "api/public/scores"),
 794            authorizationHeaderValue: authorizationHeaderValue,
 795            headers: headers);
 96    }
 97
 98    private static Uri ResolveBaseUrl(LangfuseOptions options)
 99    {
 100        string raw;
 9101        if (!string.IsNullOrWhiteSpace(options.Host))
 102        {
 3103            raw = options.Host;
 104        }
 6105        else if (options.Region is { } region)
 106        {
 5107            raw = RegionBaseUrl(region);
 108        }
 109        else
 110        {
 1111            throw new InvalidOperationException(
 1112                "Langfuse export target is not set. Provide a Host (self-hosted) or a Region " +
 1113                "(Langfuse Cloud). Check LangfuseOptions.IsConfigured before resolving endpoints.");
 114        }
 115
 8116        if (!Uri.TryCreate(EnsureTrailingSlash(raw), UriKind.Absolute, out var uri)
 8117            || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
 118        {
 1119            throw new InvalidOperationException(
 1120                $"Langfuse host '{raw}' is not a valid absolute HTTP(S) URL.");
 121        }
 122
 7123        return uri;
 124    }
 125
 5126    private static string RegionBaseUrl(LangfuseRegion region) => region switch
 5127    {
 2128        LangfuseRegion.Eu => "https://cloud.langfuse.com",
 1129        LangfuseRegion.Us => "https://us.cloud.langfuse.com",
 1130        LangfuseRegion.Jp => "https://jp.cloud.langfuse.com",
 1131        LangfuseRegion.Hipaa => "https://hipaa.cloud.langfuse.com",
 0132        _ => throw new InvalidOperationException($"Unknown Langfuse region '{region}'."),
 5133    };
 134
 135    private static string EnsureTrailingSlash(string url) =>
 8136        url.EndsWith('/') ? url : url + "/";
 137}