Skip to content

Commit

Permalink
Provide configuration & health check for resource discovery integrati…
Browse files Browse the repository at this point in the history
…on (#1051)
  • Loading branch information
tomkerkhove authored May 26, 2020
1 parent 277d3bb commit f95968b
Show file tree
Hide file tree
Showing 18 changed files with 490 additions and 54 deletions.
5 changes: 4 additions & 1 deletion config/promitor/scraper/runtime.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ telemetry:
azureMonitor:
logging:
informationLevel: Headers
isEnabled: true
isEnabled: true
resourceDiscovery:
host: promitor.agents.resourcediscovery
port: 88
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Swashbuckle.AspNetCore.Filters;

// ReSharper disable once CheckNamespace
Expand All @@ -31,14 +32,17 @@ public static IServiceCollection UseWebApi(this IServiceCollection services)
options.RespectBrowserAcceptHeader = true;
RestrictToJsonContentType(options);
AddEnumAsStringRepresentation(options);
})
.AddJsonOptions(jsonOptions =>
{
jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
jsonOptions.JsonSerializerOptions.IgnoreNullValues = true;
})
.AddNewtonsoftJson();
.AddNewtonsoftJson(jsonOptions =>
{
jsonOptions.SerializerSettings.Converters.Add(new StringEnumConverter());
jsonOptions.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});

return services;
}
Expand Down Expand Up @@ -100,28 +104,13 @@ private static void RestrictToJsonContentType(MvcOptions options)
options.OutputFormatters.RemoveType<StringOutputFormatter>();
}

private static void AddEnumAsStringRepresentation(MvcOptions options)
{
var onlyJsonInputFormatters = options.InputFormatters.OfType<SystemTextJsonInputFormatter>();
foreach (SystemTextJsonInputFormatter inputFormatter in onlyJsonInputFormatters)
{
inputFormatter.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
}

var onlyJsonOutputFormatters = options.OutputFormatters.OfType<SystemTextJsonOutputFormatter>();
foreach (SystemTextJsonOutputFormatter outputFormatter in onlyJsonOutputFormatters)
{
outputFormatter.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
}
}

private static string GetXmlDocumentationPath(IServiceCollection services, string docFileName = "Open-Api.xml")
{
var hostingEnvironment = services.FirstOrDefault(service => service.ServiceType == typeof(IWebHostEnvironment));
if (hostingEnvironment == null)
return string.Empty;

var contentRootPath = ((IWebHostEnvironment)hostingEnvironment.ImplementationInstance).ContentRootPath;
var contentRootPath = ((IWebHostEnvironment) hostingEnvironment.ImplementationInstance).ContentRootPath;
var xmlDocumentationPath = $"{contentRootPath}/Docs/{docFileName}";

return File.Exists(xmlDocumentationPath) ? xmlDocumentationPath : string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Promitor.Agents.Core.Serialization
{
/// <summary>
/// JSON converter to deserialize <see cref="HealthReportEntry"/> <c>struct</c>.
/// The <see cref="HealthReportEntry"/> <c>struct</c>s is not correctly deserialized since it was not especially made to be deserialized;
/// also <c>struct</c>s are naturally not made for deserialization using Newtonsoft.Json.
/// </summary>
public class HealthReportEntryConverter : JsonConverter
{
/// <summary>Writes the JSON representation of the object.</summary>
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter" /> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}

/// <summary>Reads the JSON representation of the object.</summary>
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader" /> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>The object value.</returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);

var healthStatus = token["status"].ToObject<HealthStatus>();
var description = token["description"]?.ToObject<string>();
var duration = token["duration"].ToObject<TimeSpan>();
var exception = token["exception"]?.ToObject<Exception>();
var data = token["data"]?.ToObject<Dictionary<string, object>>();
var readOnlyDictionary = new ReadOnlyDictionary<string, object>(data ?? new Dictionary<string, object>());
var tags = token["tags"]?.ToObject<string[]>();

return new HealthReportEntry(healthStatus, description, duration, exception, readOnlyDictionary, tags);
}

/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
return typeof(HealthReportEntry) == objectType;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Promitor.Agents.Scraper.Configuration
{
public class ResourceDiscoveryConfiguration
{
public string Host { get; set; }
public int? Port { get; set; } = 80;

public bool IsConfigured => string.IsNullOrWhiteSpace(Host) == false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ namespace Promitor.Agents.Scraper.Configuration
{
public class ScraperRuntimeConfiguration : RuntimeConfiguration
{
public PrometheusLegacyConfiguration Prometheus { get; set; } = new PrometheusLegacyConfiguration();
public MetricSinkConfiguration MetricSinks { get; set; } = new MetricSinkConfiguration();
public MetricsConfiguration MetricsConfiguration { get; set; } = new MetricsConfiguration();
public AzureMonitorConfiguration AzureMonitor { get; set; } = new AzureMonitorConfiguration();
public MetricsConfiguration MetricsConfiguration { get; set; } = new MetricsConfiguration();
public MetricSinkConfiguration MetricSinks { get; set; } = new MetricSinkConfiguration();
public PrometheusLegacyConfiguration Prometheus { get; set; } = new PrometheusLegacyConfiguration();
public ResourceDiscoveryConfiguration ResourceDiscovery { get; set; }
}
}
89 changes: 89 additions & 0 deletions src/Promitor.Agents.Scraper/Discovery/ResourceDiscoveryClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Arcus.Observability.Telemetry.Core;
using GuardNet;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Promitor.Agents.Core.Serialization;
using Promitor.Agents.Scraper.Configuration;

namespace Promitor.Agents.Scraper.Discovery
{
public class ResourceDiscoveryClient
{
private readonly IOptionsMonitor<ResourceDiscoveryConfiguration> _configuration;
private readonly ILogger<ResourceDiscoveryClient> _logger;
private readonly IHttpClientFactory _httpClientFactory;

public ResourceDiscoveryClient(IHttpClientFactory httpClientFactory, IOptionsMonitor<ResourceDiscoveryConfiguration> configuration, ILogger<ResourceDiscoveryClient> logger)
{
Guard.NotNull(httpClientFactory, nameof(httpClientFactory));
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(logger, nameof(logger));
Guard.For<Exception>(() => configuration.CurrentValue.IsConfigured == false, "Resource Discovery is not configured");

_logger = logger;
_configuration = configuration;
_httpClientFactory = httpClientFactory;
}

public async Task<List<object>> GetAsync(string resourceCollectionName)
{
var uri = $"/api/v1/resources/collections/{resourceCollectionName}/discovery";
var rawResponse = await SendGetRequestAsync(uri);
var foundResources = JsonConvert.DeserializeObject<List<object>>(rawResponse);
return foundResources;
}

public async Task<HealthReport> GetHealthAsync()
{
var rawResponse = await SendGetRequestAsync("/api/v1/health");
var healthReport = JsonConvert.DeserializeObject<HealthReport>(rawResponse, new HealthReportEntryConverter());
return healthReport;
}

private async Task<string> SendGetRequestAsync(string uri)
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);

var response = await SendRequestToApiAsync(request);
response.EnsureSuccessStatusCode();

var rawResponse = await response.Content.ReadAsStringAsync();
return rawResponse;
}

private async Task<HttpResponseMessage> SendRequestToApiAsync(HttpRequestMessage request)
{
var client = CreateHttpClient();
using (var dependencyMeasurement = DependencyMeasurement.Start())
{
HttpResponseMessage response = null;
try
{
response = await client.SendAsync(request);
_logger.LogRequest(request, response, dependencyMeasurement.Elapsed);

return response;
}
finally
{
var statusCode = response?.StatusCode ?? HttpStatusCode.InternalServerError;
_logger.LogHttpDependency(request, statusCode, dependencyMeasurement);
}
}
}

private HttpClient CreateHttpClient()
{
var httpClient = _httpClientFactory.CreateClient("Promitor Resource Discovery");
httpClient.BaseAddress = new Uri($"http://{_configuration.CurrentValue.Host}:{_configuration.CurrentValue.Port}");
return httpClient;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using GuardNet;

namespace Promitor.Agents.Scraper.Discovery
{
public class ResourceDiscoveryRepository
{
private readonly ResourceDiscoveryClient _resourceDiscoveryClient;

public ResourceDiscoveryRepository(ResourceDiscoveryClient resourceDiscoveryClient)
{
Guard.NotNull(resourceDiscoveryClient, nameof(resourceDiscoveryClient));

_resourceDiscoveryClient = resourceDiscoveryClient;
}

public async Task<List<object>> GetResourceCollectionAsync(string resourceCollectionName)
{
Guard.NotNullOrWhitespace(resourceCollectionName,nameof(resourceCollectionName));

var resources = await _resourceDiscoveryClient.GetAsync(resourceCollectionName);
return resources;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Promitor.Agents.Core.Configuration.Telemetry.Sinks;
using Promitor.Agents.Scraper.Configuration;
using Promitor.Agents.Scraper.Configuration.Sinks;
using Promitor.Agents.Scraper.Discovery;
using Promitor.Core.Scraping.Configuration.Providers;
using Promitor.Core.Scraping.Configuration.Providers.Interfaces;
using Promitor.Core.Scraping.Configuration.Serialization;
Expand Down Expand Up @@ -92,6 +93,8 @@ public static IServiceCollection ScheduleMetricScraping(this IServiceCollection
/// <param name="services">Collections of services in application</param>
public static IServiceCollection DefineDependencies(this IServiceCollection services)
{
services.AddTransient<ResourceDiscoveryClient>();
services.AddTransient<ResourceDiscoveryRepository>();
services.AddTransient<IMetricsDeclarationProvider, MetricsDeclarationProvider>();
services.AddTransient<IRuntimeMetricsCollector, RuntimeMetricsCollector>();
services.AddTransient<MetricScraperFactory>();
Expand Down Expand Up @@ -175,6 +178,7 @@ public static IServiceCollection ConfigureYamlConfiguration(this IServiceCollect
{
services.Configure<ScraperRuntimeConfiguration>(configuration);
services.Configure<MetricsConfiguration>(configuration.GetSection("metricsConfiguration"));
services.Configure<ResourceDiscoveryConfiguration>(configuration.GetSection("resourceDiscovery"));
services.Configure<TelemetryConfiguration>(configuration.GetSection("telemetry"));
services.Configure<ServerConfiguration>(configuration.GetSection("server"));
services.Configure<PrometheusScrapingEndpointSinkConfiguration>(configuration.GetSection("prometheus"));
Expand Down
40 changes: 40 additions & 0 deletions src/Promitor.Agents.Scraper/Health/ResourceDiscoveryHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GuardNet;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Promitor.Agents.Scraper.Discovery;

namespace Promitor.Agents.Scraper.Health
{
public class ResourceDiscoveryHealthCheck : IHealthCheck
{
private readonly ResourceDiscoveryClient _resourceDiscoveryClient;

public ResourceDiscoveryHealthCheck(ResourceDiscoveryClient resourceDiscoveryClient)
{
Guard.NotNull(resourceDiscoveryClient, nameof(resourceDiscoveryClient));

_resourceDiscoveryClient = resourceDiscoveryClient;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
try
{
var resourceDiscoveryHealthReport = await _resourceDiscoveryClient.GetHealthAsync();

if (resourceDiscoveryHealthReport.Status == HealthStatus.Healthy)
{
return HealthCheckResult.Healthy("Successfully contacted Promitor Resource Discovery");
}

return HealthCheckResult.Degraded("Successfully contacted Promitor Resource Discovery but it's not in healthy state.");
}
catch (Exception exception)
{
return HealthCheckResult.Unhealthy("Unable to contacted Promitor Resource Discovery.", exception: exception);
}
}
}
}
Loading

0 comments on commit f95968b

Please sign in to comment.