using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Svrnty.Sample.AI;
///
/// Simple HTTP client for sending traces directly to Langfuse ingestion API
///
public class LangfuseHttpClient
{
private readonly HttpClient _httpClient;
private readonly string _publicKey;
private readonly string _secretKey;
private readonly bool _enabled;
public LangfuseHttpClient(HttpClient httpClient, IConfiguration configuration)
{
_httpClient = httpClient;
_publicKey = configuration["Langfuse:PublicKey"] ?? "";
_secretKey = configuration["Langfuse:SecretKey"] ?? "";
_enabled = !string.IsNullOrEmpty(_publicKey) && !string.IsNullOrEmpty(_secretKey);
_ = Console.Out.WriteLineAsync($"[Langfuse] Initialized: Enabled={_enabled}, PublicKey={(_publicKey.Length > 0 ? "present" : "missing")}, SecretKey={(_secretKey.Length > 0 ? "present" : "missing")}");
}
public bool IsEnabled => _enabled;
public async Task CreateTraceAsync(string name, string userId = "system")
{
return new LangfuseTrace(this, name, userId);
}
internal async Task SendBatchAsync(List events)
{
// File-based debug logging
try
{
await File.AppendAllTextAsync("/tmp/langfuse_debug.log",
$"{DateTime.UtcNow:O} [SendBatchAsync] Called: Enabled={_enabled}, Events={events.Count}\n");
}
catch { }
if (!_enabled || events.Count == 0) return;
try
{
var batch = new { batch = events };
var json = JsonSerializer.Serialize(batch, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
_ = Console.Out.WriteLineAsync($"[Langfuse] Sending {events.Count} events to {_httpClient.BaseAddress}/api/public/ingestion");
var request = new HttpRequestMessage(HttpMethod.Post, "/api/public/ingestion")
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
// Basic Auth with public/secret keys
var authBytes = Encoding.UTF8.GetBytes($"{_publicKey}:{_secretKey}");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
"Basic", Convert.ToBase64String(authBytes));
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
_ = Console.Out.WriteLineAsync($"[Langfuse] Successfully sent batch, status: {response.StatusCode}");
}
catch (Exception ex)
{
// Log but don't throw - tracing shouldn't break the application
_ = Console.Out.WriteLineAsync($"[Langfuse] Failed to send trace: {ex.Message}");
_ = Console.Out.WriteLineAsync($"[Langfuse] Stack trace: {ex.StackTrace}");
}
}
}
///
/// Represents a Langfuse trace that can contain multiple observations
///
public class LangfuseTrace
{
private readonly LangfuseHttpClient _client;
private readonly string _traceId;
private readonly List _events = new();
private string? _input;
private string? _output;
private Dictionary? _metadata;
internal LangfuseTrace(LangfuseHttpClient client, string name, string userId)
{
_client = client;
_traceId = Guid.NewGuid().ToString();
_events.Add(new LangfuseEvent
{
Id = _traceId,
Type = "trace-create",
Timestamp = DateTime.UtcNow,
Body = new Dictionary
{
["id"] = _traceId,
["name"] = name,
["userId"] = userId,
["timestamp"] = DateTime.UtcNow
}
});
}
public string TraceId => _traceId;
public void SetInput(object input)
{
_input = input is string s ? s : JsonSerializer.Serialize(input);
}
public void SetOutput(object output)
{
_output = output is string s ? s : JsonSerializer.Serialize(output);
}
public void SetMetadata(Dictionary metadata)
{
_metadata = metadata;
}
public LangfuseSpan CreateSpan(string name)
{
return new LangfuseSpan(this, name);
}
public LangfuseGeneration CreateGeneration(string name, string model = "qwen2.5-coder:7b")
{
return new LangfuseGeneration(this, name, model);
}
internal void AddEvent(LangfuseEvent evt)
{
_events.Add(evt);
}
public async Task FlushAsync()
{
// File-based debug logging
try
{
await File.AppendAllTextAsync("/tmp/langfuse_debug.log",
$"{DateTime.UtcNow:O} [FlushAsync] Called: Events={_events.Count}, HasInput={_input != null}, HasOutput={_output != null}, Enabled={_client.IsEnabled}\n");
}
catch { }
// Update trace with final input/output
if (_input != null || _output != null || _metadata != null)
{
var updateBody = new Dictionary { ["id"] = _traceId };
if (_input != null) updateBody["input"] = _input;
if (_output != null) updateBody["output"] = _output;
if (_metadata != null) updateBody["metadata"] = _metadata;
_events.Add(new LangfuseEvent
{
Id = Guid.NewGuid().ToString(),
Type = "trace-create", // Langfuse uses same type for updates
Timestamp = DateTime.UtcNow,
Body = updateBody
});
}
await _client.SendBatchAsync(_events);
}
}
///
/// Represents a span (operation) within a trace
///
public class LangfuseSpan : IDisposable
{
private readonly LangfuseTrace _trace;
private readonly string _spanId;
private readonly DateTime _startTime;
private object? _output;
private Dictionary? _metadata;
internal LangfuseSpan(LangfuseTrace trace, string name)
{
_trace = trace;
_spanId = Guid.NewGuid().ToString();
_startTime = DateTime.UtcNow;
_trace.AddEvent(new LangfuseEvent
{
Id = _spanId,
Type = "span-create",
Timestamp = _startTime,
Body = new Dictionary
{
["id"] = _spanId,
["traceId"] = trace.TraceId,
["name"] = name,
["startTime"] = _startTime
}
});
}
public void SetOutput(object output)
{
_output = output;
}
public void SetMetadata(Dictionary metadata)
{
_metadata = metadata;
}
public void Dispose()
{
var updateBody = new Dictionary
{
["id"] = _spanId,
["endTime"] = DateTime.UtcNow
};
if (_output != null)
updateBody["output"] = _output is string s ? s : JsonSerializer.Serialize(_output);
if (_metadata != null)
updateBody["metadata"] = _metadata;
_trace.AddEvent(new LangfuseEvent
{
Id = Guid.NewGuid().ToString(),
Type = "span-update",
Timestamp = DateTime.UtcNow,
Body = updateBody
});
}
}
///
/// Represents an LLM generation within a trace
///
public class LangfuseGeneration : IDisposable
{
private readonly LangfuseTrace _trace;
private readonly string _generationId;
private readonly DateTime _startTime;
private readonly string _model;
private object? _input;
private object? _output;
private Dictionary? _metadata;
internal LangfuseGeneration(LangfuseTrace trace, string name, string model)
{
_trace = trace;
_generationId = Guid.NewGuid().ToString();
_startTime = DateTime.UtcNow;
_model = model;
_trace.AddEvent(new LangfuseEvent
{
Id = _generationId,
Type = "generation-create",
Timestamp = _startTime,
Body = new Dictionary
{
["id"] = _generationId,
["traceId"] = trace.TraceId,
["name"] = name,
["model"] = model,
["startTime"] = _startTime
}
});
}
public void SetInput(object input)
{
_input = input;
}
public void SetOutput(object output)
{
_output = output;
}
public void SetMetadata(Dictionary metadata)
{
_metadata = metadata;
}
public void Dispose()
{
var updateBody = new Dictionary
{
["id"] = _generationId,
["endTime"] = DateTime.UtcNow
};
if (_input != null)
updateBody["input"] = _input is string s ? s : JsonSerializer.Serialize(_input);
if (_output != null)
updateBody["output"] = _output is string o ? o : JsonSerializer.Serialize(_output);
if (_metadata != null)
updateBody["metadata"] = _metadata;
_trace.AddEvent(new LangfuseEvent
{
Id = Guid.NewGuid().ToString(),
Type = "generation-update",
Timestamp = DateTime.UtcNow,
Body = updateBody
});
}
}
///
/// Internal event format for Langfuse ingestion API
///
internal class LangfuseEvent
{
[JsonPropertyName("id")]
public string Id { get; set; } = "";
[JsonPropertyName("type")]
public string Type { get; set; } = "";
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; }
[JsonPropertyName("body")]
public Dictionary Body { get; set; } = new();
}