|
| 1 | +// <copyright file="GenerateIntegrationDefinitions.cs" company="Datadog"> |
| 2 | +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. |
| 3 | +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. |
| 4 | +// </copyright> |
| 5 | + |
| 6 | +using System; |
| 7 | +using System.Collections.Generic; |
| 8 | +using System.Diagnostics.CodeAnalysis; |
| 9 | +using System.Linq; |
| 10 | +using System.Net.Http; |
| 11 | +using System.Text; |
| 12 | +using System.Threading.Tasks; |
| 13 | +using Serilog; |
| 14 | + |
| 15 | +public static class MetricHelper |
| 16 | +{ |
| 17 | + public static Task SendReportableErrorMetrics(ILogger log, Dictionary<string, int> errors) |
| 18 | + { |
| 19 | + if (errors.Count == 0) |
| 20 | + { |
| 21 | + return Task.CompletedTask; |
| 22 | + } |
| 23 | + |
| 24 | + const string metricName = "dd_trace_dotnet.ci.smoke_tests.reportable_errors"; |
| 25 | + |
| 26 | + return SendMetric(log, metricName: metricName, errors.Select(kvp => CreatePoint(kvp.Key, kvp.Value))); |
| 27 | + |
| 28 | + static string CreatePoint(string errorReason, int count) |
| 29 | + { |
| 30 | + var tags = $$""" |
| 31 | + "ci.stage:{{SanitizeTagValue(Environment.GetEnvironmentVariable("DD_LOGGER_SYSTEM_STAGEDISPLAYNAME"))}}", |
| 32 | + "ci.job:{{SanitizeTagValue(Environment.GetEnvironmentVariable("DD_LOGGER_SYSTEM_JOBDISPLAYNAME"))}}", |
| 33 | + "git.branch:{{SanitizeTagValue(Environment.GetEnvironmentVariable("DD_LOGGER_BUILD_SOURCEBRANCH"))}}", |
| 34 | + "error_reason:{{SanitizeTagValue(errorReason)}}" |
| 35 | + """; |
| 36 | + |
| 37 | + return $$""" |
| 38 | + { |
| 39 | + "metric": "{{metricName}}", |
| 40 | + "type": 1, |
| 41 | + "points": [{ |
| 42 | + "timestamp": {{((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds()}}, |
| 43 | + "value": {{count}} |
| 44 | + }], |
| 45 | + "tags": [ |
| 46 | + {{tags}} |
| 47 | + ] |
| 48 | + } |
| 49 | + """; |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + private static async Task SendMetric(ILogger log, string metricName, IEnumerable<string> metrics) |
| 54 | + { |
| 55 | + var envKey = Environment.GetEnvironmentVariable("DD_LOGGER_DD_API_KEY"); |
| 56 | + if (string.IsNullOrEmpty(envKey)) |
| 57 | + { |
| 58 | + // We're probably not in CI |
| 59 | + log.Debug("No CI API Key found, skipping {MetricName} metric submission", metricName); |
| 60 | + return; |
| 61 | + } |
| 62 | + |
| 63 | + var payload = $$"""{ "series": [{{string.Join(",", metrics)}}] }"""; |
| 64 | + |
| 65 | + try |
| 66 | + { |
| 67 | + using var client = new HttpClient(); |
| 68 | + client.DefaultRequestHeaders.Add("DD-API-KEY", envKey); |
| 69 | + |
| 70 | + var content = new StringContent(payload, Encoding.UTF8, "application/json"); |
| 71 | + var response = await client.PostAsync("https://api.datadoghq.com/api/v2/series", content); |
| 72 | + var responseContent = await response.Content.ReadAsStringAsync(); |
| 73 | + |
| 74 | + var result = response.IsSuccessStatusCode |
| 75 | + ? "Successfully submitted metric" |
| 76 | + : "Failed to submit metric"; |
| 77 | + log.Warning("{Result} {MetricName}. Response was: Code: {ResponseStatusCode}. Response: {ResponseContent}. Payload sent was: \"{Payload}\"", result, metricName, response.StatusCode, responseContent, payload); |
| 78 | + } |
| 79 | + catch (Exception ex) |
| 80 | + { |
| 81 | + log.Error(ex, "Error sending {MetricName} metric to backend with payload \"{Payload}\"", metricName, payload); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + private static string SanitizeTagValue(string tag) |
| 86 | + { |
| 87 | + // Copied from |
| 88 | + // SpanTagHelper.TryNormalizeTagName(tag, normalizeSpaces: true, out var normalizedTag); |
| 89 | + return TryNormalizeTagName(tag, normalizeSpaces: true, out var normalizedTag) ? normalizedTag : tag; |
| 90 | + |
| 91 | + static bool TryNormalizeTagName( |
| 92 | + string value, |
| 93 | + bool normalizeSpaces, |
| 94 | + [NotNullWhen(returnValue: true)] out string? normalizedTagName) |
| 95 | + { |
| 96 | + normalizedTagName = null; |
| 97 | + |
| 98 | + if (!IsValidTagName(value, out var trimmedValue)) |
| 99 | + { |
| 100 | + return false; |
| 101 | + } |
| 102 | + |
| 103 | + var sb = new StringBuilder(trimmedValue.Length); |
| 104 | + sb.Append(trimmedValue.ToLowerInvariant()); |
| 105 | + |
| 106 | + for (var x = 0; x < sb.Length; x++) |
| 107 | + { |
| 108 | + switch (sb[x]) |
| 109 | + { |
| 110 | + case (>= 'a' and <= 'z') or (>= '0' and <= '9') or '_' or ':' or '/' or '-': |
| 111 | + continue; |
| 112 | + case ' ' when !normalizeSpaces: |
| 113 | + continue; |
| 114 | + default: |
| 115 | + sb[x] = '_'; |
| 116 | + break; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + normalizedTagName = sb.ToString(); |
| 121 | + return true; |
| 122 | + } |
| 123 | + |
| 124 | + static bool IsValidTagName( |
| 125 | + string value, |
| 126 | + [NotNullWhen(returnValue: true)] out string? trimmedValue) |
| 127 | + { |
| 128 | + trimmedValue = null; |
| 129 | + |
| 130 | + if (string.IsNullOrWhiteSpace(value)) |
| 131 | + { |
| 132 | + return false; |
| 133 | + } |
| 134 | + |
| 135 | + var trimmedTemp = value.Trim(); |
| 136 | + |
| 137 | + if (!char.IsLetter(trimmedTemp[0]) || trimmedTemp.Length > 200) |
| 138 | + { |
| 139 | + return false; |
| 140 | + } |
| 141 | + |
| 142 | + trimmedValue = trimmedTemp; |
| 143 | + return true; |
| 144 | + } |
| 145 | + } |
| 146 | +} |
0 commit comments