/* * Copyright (c) 2024 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using Newtonsoft.Json.Linq; using Newtonsoft.Json; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System; using ProtonVPN.UI.Tests.TestsHelper; using System.Linq; using FlaUI.Core.Tools; namespace ProtonVPN.UI.Tests.ApiClient.TestEnv; public class LokiPusher { private readonly string _lokiPushEndpoint = Environment.GetEnvironmentVariable("LOKI_ENDPOINT") + "loki/api/v1/push"; private HttpClient _httpClient; public LokiPusher() { _httpClient = new LokiApiClient().GetHttpClient(); } public void PushMetrics() { if (SliHelper.MetricsList.Count == 0) { throw new Exception("Pushing empty metric list is not allowed."); } if (SliHelper.RunId is null || SliHelper.SliName is null || SliHelper.Workflow is null) { throw new Exception("Run id or sli name or workflow is null."); } try { JArray fullMetrics = BaseMetricsJsonBody(GetMetadata(SliHelper.RunId), SliHelper.MetricsList); JObject requestBody = BaseLokiRequestJsonBody(fullMetrics, GetMetricsLabels(SliHelper.SliName, SliHelper.Workflow)); PushToLokiWithRetry(requestBody); } catch (Exception) { SliHelper.Reset(); throw; } } public void PushLogs(string logsPath, string logType) { if (SliHelper.RunId is null || SliHelper.Workflow is null) { throw new Exception("Run id or sli name or workflow is null."); } JObject requestBody = AddLogsToRequestJson(logsPath, logType, SliHelper.Workflow, GetMetadata(SliHelper.RunId)); PushToLokiWithRetry(requestBody); } public void PushAllLogs() { PushLogs(TestConstants.ClientLogsPath, "windows_client_logs"); PushLogs(TestEnvironment.GetServiceLogsPath(), "windows_service_logs"); } private JObject AddLogsToRequestJson(string pathToLogs, string logType, string workflow, JObject metadata) { List logs = []; FileStream fileStream = new(pathToLogs, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using (StreamReader sr = new(fileStream)) { string? line; while ((line = sr.ReadLine()) != null) { List logLines = new(line.Split(" | ")); string timestamp = ConvertTimeToUnixNanosecond(logLines.First()); logLines.RemoveAt(0); string formattedLogs = string.Join(" | ", logLines); JArray element = new JArray(timestamp, formattedLogs, metadata); logs.Add(element); } } return BaseLokiRequestJsonBody(logs, GetLogsLabels(workflow, logType)); } private void PushToLokiWithRetry(JObject requestBody) { RetryResult retry = Retry.WhileNull( () => { return PushToLokiAsync(requestBody).Result; }, TestConstants.TenSecondsTimeout, TestConstants.ApiRetryInterval, ignoreException: true); if (!retry.Success) { throw new Exception($"Failed to push to loki:\n{retry.LastException}"); } } private async Task PushToLokiAsync(JObject requestBody) { string jsonContent = JsonConvert.SerializeObject(requestBody); StringContent httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); HttpResponseMessage response = await _httpClient.PostAsync(_lokiPushEndpoint, httpContent); string responseBody = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new Exception(responseBody); } response.EnsureSuccessStatusCode(); return responseBody; } private string ConvertTimeToUnixNanosecond(string timestampString) { string? timestamp; try { DateTime logTimeStamp = DateTime.Parse(timestampString, null, System.Globalization.DateTimeStyles.RoundtripKind); long unixTimestampNanoSeconds = (long)(logTimeStamp.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds * 1000000; timestamp = unixTimestampNanoSeconds.ToString(); } catch (FormatException) { timestamp = GetCurrentUnixTimeInNanoseconds().ToString(); } return timestamp; } private JArray BaseMetricsJsonBody(JObject metadata, object metric) { return new JArray( GetCurrentUnixTimeInNanoseconds().ToString(), new JObject(metric).ToString(), metadata); } private JObject BaseLokiRequestJsonBody(object data, JObject labels) { return new JObject( new JProperty("streams", new JArray( new JObject( new JProperty("stream", labels), new JProperty("values", new JArray(data)) )) )); } private JObject GetMetadata(string runId) { return new JObject( new List { new JProperty("id", runId), new JProperty("app_version", TestEnvironment.GetAppVersion()), new JProperty("build_commit_sha1", TestEnvironment.GetCommitHash()) }); } private JObject GetMetricsLabels(string measurementGroup, string workflow) { JObject labels = GetCommonLabels(workflow); labels.Add(new JProperty("sli", measurementGroup)); return labels; } private JObject GetLogsLabels(string workflow, string logType) { JObject labels = GetCommonLabels(workflow); labels.Add(new JProperty("logType", logType)); return labels; } private JObject GetCommonLabels(string workflow) { return new JObject ( new JProperty("os_version", TestEnvironment.GetOperatingSystem()), new JProperty("workflow", workflow), new JProperty("environment", "prod"), new JProperty("platform", "windows"), new JProperty("product", "VPN") ); } private long GetCurrentUnixTimeInNanoseconds() { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1000000; } }