Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements.
// Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md).

using System;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Amazon.SQS.Model;
using KurrentDB.Core.Certificates;
using KurrentDB.Core.Metrics;
using KurrentDB.Core.Time;
using Xunit;


namespace KurrentDB.Core.XUnit.Tests.Metrics;

public class CertificatesMetricTests : IDisposable {
private readonly TestMeterListener<double> _doubleListener;

private readonly Meter _meter;
private readonly DateTimeOffset _now;

public CertificatesMetricTests() {
_meter = new Meter($"{typeof(CertificatesMetricTests)}");
_doubleListener = new TestMeterListener<double>(_meter);
_doubleListener.Observe();
_now = DateTimeOffset.FromUnixTimeSeconds(Clock.Instance.SecondsSinceEpoch);
}

public void Dispose() => _doubleListener.Dispose();

[Fact]
public void When_Expired() {


var b = new CertificateProviderTest(_now.AddDays(-10), _now.AddDays(-9));
var sut = new CertificatesMetric(_meter, "cert_apocalypse", b);


sut.Measure();
var measurements = _doubleListener.RetrieveMeasurements("cert_apocalypse-day");

Assert.Contains(
measurements,
m => {

Assert.True(Math.Abs(m.Value + 9) < 0.01);


Assert.Equal(CertificatesMetric.Tags.All, m.Tags.Select(t => t.Key));
Assert.Contains(
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ChainLevel).Value,
CertificatesMetric.ChainLevels.All
);
Assert.Equal(
CertificatesMetric.ValidityPeriods.Expired,
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ValidityPeriod).Value);


return true;
});
}
[Fact]
public void When_Ok() {


var b = new CertificateProviderTest(_now.AddDays(-10), _now.AddDays(10));
var sut = new CertificatesMetric(_meter, "cert_will_be_apocalypse", b);


sut.Measure();
var measurements = _doubleListener.RetrieveMeasurements("cert_will_be_apocalypse-day");

Assert.Contains(
measurements,
m => {

Assert.True(Math.Abs(m.Value -10)< 0.01);


Assert.Equal(CertificatesMetric.Tags.All, m.Tags.Select(t => t.Key));
Assert.Contains(
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ChainLevel).Value,
CertificatesMetric.ChainLevels.All
);
Assert.Equal(
CertificatesMetric.ValidityPeriods.InPeriod,
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ValidityPeriod).Value);

return true;
});
}

[Fact]
public void When_Not_Yet_Valid() {


var b = new CertificateProviderTest(_now.AddDays(10), _now.AddDays(11));
var sut = new CertificatesMetric(_meter, "cert_future_apocalypse", b);


sut.Measure();
var measurements = _doubleListener.RetrieveMeasurements("cert_future_apocalypse-day");

Assert.Contains(
measurements,
m => {

Assert.True(Math.Abs(m.Value -11 ) < 0.01);

Assert.Equal(CertificatesMetric.Tags.All, m.Tags.Select(t => t.Key));
Assert.Contains(
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ChainLevel).Value,
CertificatesMetric.ChainLevels.All
);
Assert.Equal(
CertificatesMetric.ValidityPeriods.NotValidYet,
m.Tags.Single(t => t.Key == CertificatesMetric.Tags.ValidityPeriod).Value);
return true;
});
}


public class CertificateProviderTest : CertificateProvider {


public CertificateProviderTest(DateTimeOffset notBefore, DateTimeOffset notAfter) {
using RSA rsa = RSA.Create();
var request = new CertificateRequest("CN=SelfSignedCert", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);


Certificate=request.CreateSelfSigned(notBefore, notAfter);
IntermediateCerts = new X509Certificate2Collection(

request.CreateSelfSigned(notBefore, notAfter)
);
TrustedRootCerts = new X509Certificate2Collection(request.CreateSelfSigned(notBefore, notAfter));
}

public override LoadCertificateResult LoadCertificates(ClusterVNodeOptions options) =>
throw new UnsupportedOperationException("");

public override string GetReservedNodeCommonName() =>
throw new UnsupportedOperationException("");
}

}
97 changes: 97 additions & 0 deletions src/KurrentDB.Core/Metrics/CertificatesMetric.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements.
// Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md).

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Security.Cryptography.X509Certificates;
using KurrentDB.Core.Certificates;
using KurrentDB.Core.Time;

namespace KurrentDB.Core.Metrics;

public class CertificatesMetric {
private readonly Meter _meter;
private readonly string _name;
private readonly CertificateProvider _certificateProvider;
private readonly List<CertificateExpiration> _subMetric= [];
private readonly IClock _clock;

public static class ChainLevels {
public const string Node="node";
public const string Intermediate = "intermediate";
public const string Root = "root";
public static readonly string[] All = [Node, Intermediate, Root];
}
public static class Tags {
public const string ChainLevel = "chain_level";
public const string SerialNumber = "serial_number";
public const string Thumbprint = "thumbprint";
public const string ValidityPeriod = "validity_period";
public static readonly string[] All = [ChainLevel, SerialNumber, Thumbprint, ValidityPeriod];
}
public static class ValidityPeriods {
public const string InPeriod="0";
public const string NotValidYet = "1";
public const string Expired = "-1";
}



public CertificatesMetric(Meter meter, string name,
CertificateProvider certificateProvider,
IClock clock = null) {
_meter = meter;
_name = name;
_certificateProvider = certificateProvider;
_clock = clock ?? Clock.Instance;

MeasureAllCerts(meter, name, certificateProvider);
}

public void Measure() {
foreach (var subMetric in _subMetric)
subMetric.Measure();
}

public void Renewed() => MeasureAllCerts(_meter, _name, _certificateProvider);

private void MeasureAllCerts(Meter meter, string name, CertificateProvider certificateProvider)
{
_subMetric.Clear();
_subMetric.Add(new CertificateExpiration(certificateProvider.Certificate, ChainLevels.Node, meter, name,_clock));
foreach (var intermediate in certificateProvider.IntermediateCerts)
_subMetric.Add(new CertificateExpiration(intermediate, ChainLevels.Intermediate, meter, name,_clock));

foreach (var root in certificateProvider.TrustedRootCerts)
_subMetric.Add(new CertificateExpiration(root, ChainLevels.Root, meter, name,_clock));
}


public class CertificateExpiration(X509Certificate2 cert, string type, Meter meter, string name, IClock clock ) {

private readonly Gauge<double> _gauge = meter.CreateGauge<double>(name, "day", "Days before the certificate expires");

private readonly KeyValuePair<string, object>[] _tags = [
new(Tags.ChainLevel, type),
new(Tags.SerialNumber, cert.GetSerialNumberString()),
new(Tags.Thumbprint, cert.Thumbprint),
new(Tags.ValidityPeriod, ValidityPeriod(DateTimeOffset.FromUnixTimeSeconds(clock.SecondsSinceEpoch).DateTime, cert.NotBefore, cert.NotAfter).ToString())

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please reformat the file (add empty lines between classes, remove redundant empty lines)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applies to all files


];

private static string ValidityPeriod(DateTime now, DateTime notBefore, DateTime notAfter) =>
notBefore <= now && now <= notAfter
? ValidityPeriods.InPeriod
: now <= notBefore
? ValidityPeriods.NotValidYet
: ValidityPeriods.Expired;

public void Measure() {
var certApocalypseIn = (cert.NotAfter - DateTime.Now).TotalDays;
_gauge.Record(certApocalypseIn, _tags);

}
}
}
Loading