| 847 | } |
| 848 | |
| 849 | void LlmProxyService::WriteLogEntry(const std::string& request_body, |
| 850 | const std::string& response_body, |
| 851 | const std::string& alias, |
| 852 | const std::string& model, |
| 853 | bool is_streaming, int status_code, |
| 854 | const StreamEndInfo& stream_end) { |
| 855 | std::lock_guard<std::mutex> lock(log_mutex_); |
| 856 | if (!log_file_) return; |
| 857 | |
| 858 | auto today = TodayDateStr(); |
| 859 | if (today != current_log_date_) { |
| 860 | fclose(log_file_); |
| 861 | current_log_date_ = today; |
| 862 | auto path = (fs::path(log_dir_) / ("llm_" + today + ".jsonl")).string(); |
| 863 | log_file_ = _fsopen(path.c_str(), "ab", _SH_DENYWR); |
| 864 | if (!log_file_) return; |
| 865 | } |
| 866 | |
| 867 | json entry; |
| 868 | entry["timestamp"] = IsoTimestamp(); |
| 869 | entry["alias"] = alias; |
| 870 | entry["model"] = model; |
| 871 | entry["stream"] = is_streaming; |
| 872 | entry["status"] = status_code; |
| 873 | |
| 874 | // Diagnostics: who closed the connection and why. Always present so log |
| 875 | // readers can filter on stream_end.reason != "upstream_done". |
| 876 | json end = { |
| 877 | {"reason", stream_end.reason}, |
| 878 | {"duration_ms", stream_end.duration_ms}, |
| 879 | }; |
| 880 | if (!stream_end.detail.empty()) end["detail"] = stream_end.detail; |
| 881 | if (stream_end.winhttp_error) end["winhttp_error"] = stream_end.winhttp_error; |
| 882 | if (stream_end.wsa_error) end["wsa_error"] = stream_end.wsa_error; |
| 883 | if (is_streaming) { |
| 884 | end["upstream_bytes"] = stream_end.upstream_bytes; |
| 885 | end["chunk_count"] = stream_end.chunk_count; |
| 886 | end["saw_done_marker"] = stream_end.saw_done_marker; |
| 887 | } |
| 888 | entry["stream_end"] = std::move(end); |
| 889 | |
| 890 | // Parse request to extract messages and params |
| 891 | try { |
| 892 | auto req = json::parse(request_body, nullptr, false); |
| 893 | if (!req.is_discarded() && req.is_object()) { |
| 894 | if (req.contains("messages")) entry["messages"] = req["messages"]; |
| 895 | if (req.contains("temperature")) entry["temperature"] = req["temperature"]; |
| 896 | if (req.contains("max_tokens")) entry["max_tokens"] = req["max_tokens"]; |
| 897 | if (req.contains("top_p")) entry["top_p"] = req["top_p"]; |
| 898 | } |
| 899 | } catch (...) {} |
| 900 | |
| 901 | // Extract response content |
| 902 | if (status_code >= 200 && status_code < 300) { |
| 903 | if (is_streaming) { |
| 904 | entry["response"] = ExtractSseContent(response_body); |
| 905 | } else { |
| 906 | try { |
nothing calls this directly
no test coverage detected